feat(content-manager): add sortable file size column (#698)
Closes #685 Content Manager now surfaces the on-disk size of each ZIM file alongside title/summary, and lets users sort the list by Size or Title. Defaults to Size descending so the largest files are visible first. - ZimService.list() now stats each file and returns size_bytes - Content Manager table adds a formatted Size column (via formatBytes) - Sortable headers for Title and Size with asc/desc toggle
This commit is contained in:
parent
cb129d2713
commit
95d0816d50
|
|
@ -40,7 +40,21 @@ export class ZimService {
|
|||
await ensureDirectoryExists(dirPath)
|
||||
|
||||
const all = await listDirectoryContents(dirPath)
|
||||
const files = all.filter((item) => item.name.endsWith('.zim'))
|
||||
const zimEntries = all.filter((item) => item.name.endsWith('.zim'))
|
||||
|
||||
const files = await Promise.all(
|
||||
zimEntries.map(async (entry) => {
|
||||
const filePath = entry.type === 'file' ? entry.key : join(dirPath, entry.name)
|
||||
const stats = await getFileStatsIfExists(filePath)
|
||||
return {
|
||||
...entry,
|
||||
title: null,
|
||||
summary: null,
|
||||
author: null,
|
||||
size_bytes: stats ? Number(stats.size) : null,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
files,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Head } from '@inertiajs/react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useMemo, useState } from 'react'
|
||||
import StyledTable from '~/components/StyledTable'
|
||||
import SettingsLayout from '~/layouts/SettingsLayout'
|
||||
import api from '~/lib/api'
|
||||
|
|
@ -10,11 +11,18 @@ import useServiceInstalledStatus from '~/hooks/useServiceInstalledStatus'
|
|||
import Alert from '~/components/Alert'
|
||||
import { ZimFileWithMetadata } from '../../../../types/zim'
|
||||
import { SERVICE_NAMES } from '../../../../constants/service_names'
|
||||
import { formatBytes } from '~/lib/util'
|
||||
import { IconArrowDown, IconArrowUp, IconArrowsSort } from '@tabler/icons-react'
|
||||
|
||||
type SortKey = 'name' | 'size'
|
||||
type SortDirection = 'asc' | 'desc'
|
||||
|
||||
export default function ZimPage() {
|
||||
const queryClient = useQueryClient()
|
||||
const { openModal, closeAllModals } = useModals()
|
||||
const { isInstalled } = useServiceInstalledStatus(SERVICE_NAMES.KIWIX)
|
||||
const [sortKey, setSortKey] = useState<SortKey>('size')
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>('desc')
|
||||
const { data, isLoading } = useQuery<ZimFileWithMetadata[]>({
|
||||
queryKey: ['zim-files'],
|
||||
queryFn: getFiles,
|
||||
|
|
@ -25,6 +33,49 @@ export default function ZimPage() {
|
|||
return res.data.files
|
||||
}
|
||||
|
||||
const sortedData = useMemo(() => {
|
||||
if (!data) return []
|
||||
const copy = [...data]
|
||||
copy.sort((a, b) => {
|
||||
let cmp = 0
|
||||
if (sortKey === 'size') {
|
||||
const aSize = a.size_bytes ?? 0
|
||||
const bSize = b.size_bytes ?? 0
|
||||
cmp = aSize - bSize
|
||||
} else {
|
||||
const aName = (a.title || a.name).toLowerCase()
|
||||
const bName = (b.title || b.name).toLowerCase()
|
||||
cmp = aName.localeCompare(bName)
|
||||
}
|
||||
return sortDirection === 'asc' ? cmp : -cmp
|
||||
})
|
||||
return copy
|
||||
}, [data, sortKey, sortDirection])
|
||||
|
||||
function toggleSort(key: SortKey) {
|
||||
if (sortKey === key) {
|
||||
setSortDirection((d) => (d === 'asc' ? 'desc' : 'asc'))
|
||||
} else {
|
||||
setSortKey(key)
|
||||
setSortDirection(key === 'size' ? 'desc' : 'asc')
|
||||
}
|
||||
}
|
||||
|
||||
function renderSortHeader(label: string, key: SortKey) {
|
||||
const active = sortKey === key
|
||||
const Icon = !active ? IconArrowsSort : sortDirection === 'asc' ? IconArrowUp : IconArrowDown
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSort(key)}
|
||||
className="flex items-center gap-1 font-semibold text-text-primary hover:text-desert-orange"
|
||||
>
|
||||
{label}
|
||||
<Icon className="size-4" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
async function confirmDeleteFile(file: ZimFileWithMetadata) {
|
||||
openModal(
|
||||
<StyledModal
|
||||
|
|
@ -83,7 +134,7 @@ export default function ZimPage() {
|
|||
columns={[
|
||||
{
|
||||
accessor: 'title',
|
||||
title: 'Title',
|
||||
title: renderSortHeader('Title', 'name'),
|
||||
render: (record) => (
|
||||
<span className="font-medium">
|
||||
{record.title || record.name}
|
||||
|
|
@ -99,6 +150,15 @@ export default function ZimPage() {
|
|||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: 'size_bytes',
|
||||
title: renderSortHeader('Size', 'size'),
|
||||
render: (record) => (
|
||||
<span className="text-text-secondary tabular-nums">
|
||||
{record.size_bytes ? formatBytes(record.size_bytes, 1) : '—'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: 'actions',
|
||||
title: 'Actions',
|
||||
|
|
@ -117,7 +177,7 @@ export default function ZimPage() {
|
|||
),
|
||||
},
|
||||
]}
|
||||
data={data || []}
|
||||
data={sortedData}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue