feat: Expandable rows in Kiwix Library browser (#1060)
* feat: expandable rows in Kiwix Library browser (rebased onto dev) Click a row in the Content Explorer's 'Browse the Kiwix Library' table to expand it and reveal the full description (no longer truncated) along with additional metadata: author, publisher, language, category, article count, media count, issue date, file size, tags, and file name. Changes: - Extend RemoteZimFileEntry type with optional metadata fields (language, publisher, category, tags, article_count, media_count, issued) - Update zim_service.ts listRemote() to map these fields from the raw Kiwix API response in the paginated accumulator loop - Remove @tanstack/react-virtual virtualization from remote-explorer.tsx (12 items per page — virtualization not needed and incompatible with variable-height expanded rows) - Use StyledTable's built-in expandable prop with expandedRowRender to show full details when a row is clicked - Preserve upstream's custom libraries source selector and directory browser * fix(RemoteExplorer): guard against invalid dates being rendered --------- Co-authored-by: eizus <hello@cdr.xyz> Co-authored-by: jakeaturner <jturner@cosmistack.com>
This commit is contained in:
parent
d48dffc802
commit
9482f78486
|
|
@ -164,6 +164,13 @@ export class ZimService {
|
|||
download_url,
|
||||
author: entry.author.name,
|
||||
file_name,
|
||||
language: entry.language,
|
||||
category: entry.category,
|
||||
tags: entry.tags,
|
||||
article_count: entry.articleCount,
|
||||
media_count: entry.mediaCount,
|
||||
publisher: entry.publisher?.name,
|
||||
issued: entry['dc:issued'],
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
} from '@tanstack/react-query'
|
||||
import api from '~/lib/api'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
import StyledTable from '~/components/StyledTable'
|
||||
import SettingsLayout from '~/layouts/SettingsLayout'
|
||||
import { Head } from '@inertiajs/react'
|
||||
|
|
@ -191,13 +190,6 @@ export default function ZimRemoteExplorer() {
|
|||
[fetchNextPage, isFetching, hasMore]
|
||||
)
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: flatData.length,
|
||||
estimateSize: () => 48, // Estimate row height
|
||||
getScrollElement: () => tableParentRef.current,
|
||||
overscan: 5, // Number of items to render outside the visible area
|
||||
})
|
||||
|
||||
//a check on mount and after a fetch to see if the table is already scrolled to the bottom and immediately needs to fetch more data
|
||||
useEffect(() => {
|
||||
fetchOnBottomReached(tableParentRef.current)
|
||||
|
|
@ -591,14 +583,7 @@ export default function ZimRemoteExplorer() {
|
|||
/>
|
||||
</div>
|
||||
<StyledTable<RemoteZimFileEntry & { actions?: any }>
|
||||
data={flatData.map((i, idx) => {
|
||||
const row = virtualizer.getVirtualItems().find((v) => v.index === idx)
|
||||
return {
|
||||
...i,
|
||||
height: `${row?.size || 48}px`,
|
||||
translateY: row?.start || 0,
|
||||
}
|
||||
})}
|
||||
data={flatData}
|
||||
ref={tableParentRef}
|
||||
loading={isLoading}
|
||||
columns={[
|
||||
|
|
@ -610,6 +595,13 @@ export default function ZimRemoteExplorer() {
|
|||
},
|
||||
{
|
||||
accessor: 'summary',
|
||||
render(record) {
|
||||
return (
|
||||
<span className="block max-w-md truncate text-text-muted" title={record.summary}>
|
||||
{record.summary}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: 'updated',
|
||||
|
|
@ -644,11 +636,97 @@ export default function ZimRemoteExplorer() {
|
|||
},
|
||||
},
|
||||
]}
|
||||
className="relative overflow-x-auto overflow-y-auto h-[600px] w-full mt-4"
|
||||
tableBodyStyle={{
|
||||
position: 'relative',
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
expandable={{
|
||||
expandedRowRender(record) {
|
||||
const issuedDate = record.issued ? new Date(record.issued) : null
|
||||
const hasValidIssuedDate = issuedDate && !Number.isNaN(issuedDate.getTime())
|
||||
return (
|
||||
<div className="py-4 px-6">
|
||||
<p className="text-sm text-text-primary mb-4">{record.summary}</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 text-sm">
|
||||
{record.author && (
|
||||
<div>
|
||||
<span className="text-text-muted">Author: </span>
|
||||
<span className="text-text-primary">{record.author}</span>
|
||||
</div>
|
||||
)}
|
||||
{record.publisher && (
|
||||
<div>
|
||||
<span className="text-text-muted">Publisher: </span>
|
||||
<span className="text-text-primary">{record.publisher}</span>
|
||||
</div>
|
||||
)}
|
||||
{record.language && (
|
||||
<div>
|
||||
<span className="text-text-muted">Language: </span>
|
||||
<span className="text-text-primary">{record.language}</span>
|
||||
</div>
|
||||
)}
|
||||
{record.category && (
|
||||
<div>
|
||||
<span className="text-text-muted">Category: </span>
|
||||
<span className="text-text-primary">{record.category}</span>
|
||||
</div>
|
||||
)}
|
||||
{record.article_count != null && record.article_count > 0 && (
|
||||
<div>
|
||||
<span className="text-text-muted">Articles: </span>
|
||||
<span className="text-text-primary">
|
||||
{record.article_count.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{record.media_count != null && record.media_count > 0 && (
|
||||
<div>
|
||||
<span className="text-text-muted">Media: </span>
|
||||
<span className="text-text-primary">
|
||||
{record.media_count.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{hasValidIssuedDate && (
|
||||
<div>
|
||||
<span className="text-text-muted">Issued: </span>
|
||||
<span className="text-text-primary">
|
||||
{new Intl.DateTimeFormat('en-US', {
|
||||
dateStyle: 'medium',
|
||||
}).format(issuedDate!)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{record.size_bytes > 0 && (
|
||||
<div>
|
||||
<span className="text-text-muted">Size: </span>
|
||||
<span className="text-text-primary">{formatBytes(record.size_bytes)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{record.tags && (
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{record.tags
|
||||
.split(';')
|
||||
.filter(Boolean)
|
||||
.map((tag, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="inline-flex items-center rounded-full bg-surface-elevated px-2.5 py-0.5 text-xs font-medium text-text-muted"
|
||||
>
|
||||
{tag.trim()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{record.file_name && (
|
||||
<div className="mt-4">
|
||||
<span className="text-text-muted text-sm">File: </span>
|
||||
<code className="text-xs text-text-muted">{record.file_name}</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}}
|
||||
className="overflow-y-auto h-[600px] w-full mt-4"
|
||||
containerProps={{
|
||||
onScroll: (e) => fetchOnBottomReached(e.currentTarget as HTMLDivElement),
|
||||
}}
|
||||
|
|
@ -760,7 +838,6 @@ export default function ZimRemoteExplorer() {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ActiveDownloads filetype="zim" withHeader />
|
||||
|
||||
{/* Manage Custom Libraries Modal */}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,13 @@ export type RemoteZimFileEntry = {
|
|||
download_url: string
|
||||
author: string
|
||||
file_name: string
|
||||
language?: string
|
||||
category?: string
|
||||
tags?: string
|
||||
article_count?: number
|
||||
media_count?: number
|
||||
publisher?: string
|
||||
issued?: string
|
||||
}
|
||||
|
||||
export type ExtractZIMContentOptions = {
|
||||
|
|
@ -71,8 +78,8 @@ export type ExtractZIMContentOptions = {
|
|||
maxArticles?: number
|
||||
onProgress?: (processedArticles: number, totalArticles: number) => void
|
||||
// Batch processing options to avoid lock timeouts
|
||||
startOffset?: number // Article index to start from for resuming
|
||||
batchSize?: number // Max articles to process in this batch
|
||||
startOffset?: number // Article index to start from for resuming
|
||||
batchSize?: number // Max articles to process in this batch
|
||||
}
|
||||
|
||||
export type ExtractZIMChunkingStrategy = 'structured' | 'simple'
|
||||
|
|
@ -108,4 +115,4 @@ export type ZIMContentChunk = {
|
|||
|
||||
// Extraction metadata
|
||||
strategy: ExtractZIMChunkingStrategy
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue