Files
template-app/modules/knowledge/ui/live-documents.tsx
T
2026-11-14 09:56:45 +00:00

97 lines
3.6 KiB
TypeScript

"use client"
// One collection's documents, live (umbrella TS6.4 and TS6.6): the page's view renders them and hands them
// over as the seed, so the page arrives complete; this keeps them current through the organization's live
// stream -- a document another member asks to index appears, and each moves from queued to indexed or
// failed as the worker and the platform answer, without a reload. The rows come from
// `knowledge.document.getMany`, under the same permission as the list; a topic is the whole project, so the
// rows of the project's other collections are filtered out here.
import type { ProjectSlugs } from "@conusai-kit/ui"
import { eq, liveCollection, useDataClient, useLive } from "@conusai-kit/ui/data"
import { type ResourceColumn, ResourceList } from "@conusai-kit/ui/patterns"
import { type ReactElement, useState } from "react"
import { DOCUMENT_COLLECTION, type DocumentView } from "../contract/index.js"
export interface DocumentListLabels {
readonly caption: string
readonly file: string
readonly status: string
readonly error: string
/** The message of each document status, by status. */
readonly statuses: Readonly<Record<DocumentView["status"], string>>
/** The words of each failure code a row can carry; an unknown code is shown as it is. */
readonly failures: Readonly<Record<string, string>>
}
export function useLiveDocuments(
slugs: ProjectSlugs,
collectionName: string,
seed: readonly DocumentView[],
): readonly DocumentView[] {
const client = useDataClient()
// One collection per knowledge collection for the life of the page; the seed is the first render's.
const [documents] = useState(() =>
liveCollection<DocumentView>({
client,
org: slugs.org,
topic: { ws: slugs.workspace, project: slugs.project },
entity: DOCUMENT_COLLECTION.entity,
key: DOCUMENT_COLLECTION.key,
id: `${DOCUMENT_COLLECTION.entity}:${slugs.org}:${slugs.workspace}:${slugs.project}:${collectionName}`,
seed,
list: {
procedure: DOCUMENT_COLLECTION.list,
input: { ...slugs, collectionName },
rows: (out) => (out as { documents: DocumentView[] }).documents,
},
getMany: {
procedure: DOCUMENT_COLLECTION.getMany,
input: { ...slugs },
rows: (out) => (out as { items: DocumentView[] }).items,
},
}),
)
const { data, isReady } = useLive(documents, (q, rows) =>
q
.from({ document: rows })
.where(({ document }: { document: DocumentView }) => eq(document.collectionName, collectionName)),
)
// Until the collection is ready the server's rows are the answer, so the first paint matches it.
return isReady ? data : seed
}
export function CollectionDocuments({
slugs,
collectionName,
seed,
labels,
}: {
slugs: ProjectSlugs
collectionName: string
seed: readonly DocumentView[]
labels: DocumentListLabels
}): ReactElement {
const rows = useLiveDocuments(slugs, collectionName, seed)
const columns: ResourceColumn<DocumentView>[] = [
{ id: "filename", header: labels.file, wrap: true },
{ id: "status", header: labels.status, cell: (document) => labels.statuses[document.status] },
{
id: "error",
header: labels.error,
wrap: true,
cell: (document) => (document.error === null ? "" : (labels.failures[document.error] ?? document.error)),
},
]
return (
<ResourceList
label={labels.caption}
columns={columns}
data={rows}
rowId={(document) => document.documentId}
rowLabel={(document) => document.filename}
empty={{ title: labels.caption }}
/>
)
}