97 lines
3.6 KiB
TypeScript
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 }}
|
|
/>
|
|
)
|
|
}
|