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:
chriscrosstalk 2026-04-27 18:49:51 -07:00 committed by Jake Turner
parent cb129d2713
commit 95d0816d50
2 changed files with 77 additions and 3 deletions

View File

@ -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,

View File

@ -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>