// The archive module's pages, on the shell's F10 contract (docs/reference/app-kit.md): a loader the web // shell runs on the server under `////archive`, and the view of what it returned. // // /archive the organization's documents, and the form that registers one // /archive/retention the retention classes, what is due for disposal, and the form that adds a class // /archive/ one document: its versions, the form that adds one, its legal hold and disposal // // Every read is a procedure through the same caller the API uses, and every label is translated in the // loader. A refusal that is the platform's -- the service is not enabled for the organization, the // deployment lacks a scope, the service did not answer -- is the page's own state, not an error page. import { businessRefusalNotices, businessServiceNotices } from "@conusai-kit/business/refusals" import { type Container, notFound, type TransportCaller } from "@conusai-kit/core" import type { I18n, Locale } from "@conusai-kit/i18n" import { type ProjectPage, type ProjectPageViewProps, projectPage } from "@conusai-kit/ui" import { Can } from "@conusai-kit/ui/data" import { PageHeader, ServiceStatePage, serviceNotice } from "@conusai-kit/ui/patterns" import type { ReactElement } from "react" import { ARCHIVE_PERMISSIONS, type DocumentView, disposalList, documentGet, documentList, type RetentionClassView, retentionList, type VersionView, } from "../contract/index.js" import { DocumentActions, type DocumentActionsLabels } from "./document-actions.js" import { DocumentList, type DocumentListLabels } from "./document-list.js" import { baseNames, documentActionsLabels, documentListLabels, registerFormLabels, retentionFormLabels, retentionLabels, type Translate, versionFormLabels, versionsLabels, } from "./labels.js" import { RegisterForm, type RegisterFormLabels } from "./register-form.js" import { RetentionClasses, type RetentionLabels } from "./retention-classes.js" import { RetentionForm, type RetentionFormLabels } from "./retention-form.js" import { VersionForm, type VersionFormLabels } from "./version-form.js" import { Versions, type VersionsLabels } from "./versions.js" /** A platform refusal as the page shows it: the problem's code and its text in the reader's language. */ export interface ServiceNotice { readonly code: string readonly text: string } type StatusNames = Readonly> export interface ArchiveListData { readonly kind: "list" readonly title: string readonly intro: string readonly retentionLink: string readonly form: RegisterFormLabels readonly list: DocumentListLabels readonly statuses: StatusNames readonly classes: readonly RetentionClassView[] readonly documents: { rows: DocumentView[]; after?: string | undefined } } export interface ArchiveDocumentData { readonly kind: "document" readonly title: string readonly back: string readonly document: DocumentView readonly status: string readonly versions: readonly VersionView[] readonly labels: { readonly retentionClass: string readonly status: string readonly revision: string readonly created: string readonly hold: string readonly held: string readonly disposedAt: string readonly versions: VersionsLabels readonly versionForm: VersionFormLabels readonly actions: DocumentActionsLabels } } export interface ArchiveRetentionData { readonly kind: "retention" readonly title: string readonly intro: string readonly back: string readonly list: RetentionLabels readonly form: RetentionFormLabels readonly bases: Readonly> readonly classes: readonly RetentionClassView[] readonly due: readonly DocumentView[] } /** The service cannot answer: the notice in place of the page. */ export interface ArchiveStateData { readonly kind: "state" readonly title: string readonly notice: ServiceNotice } export type ArchivePageData = ArchiveListData | ArchiveDocumentData | ArchiveRetentionData | ArchiveStateData export type ArchivePage = ProjectPage /** A document id as the service mints it: one path segment. */ const DOCUMENT_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/ /** The document a path segment names; a segment that does not decode names none. */ function documentIdOf(segment: string): string { try { return decodeURIComponent(segment) } catch { throw notFound("no such page") } } /** The notice for a platform refusal, or undefined for any other error (which the shell answers). */ export function serviceState(error: unknown, locale: Locale): ServiceNotice | undefined { return serviceNotice(error, businessServiceNotices(locale)) } const statusNames = (t: Translate): StatusNames => ({ active: t("statuses.active"), disposed: t("statuses.disposed"), }) export const archivePage: ArchivePage = projectPage({ async load({ slugs, segments, caller, i18n }): Promise { const t: Translate = (key, values) => i18n.message(`archive.${key}`, values) const failed = i18n.message("app.failed") const refusals = businessRefusalNotices(i18n.locale) const title = t("title") const [segment, ...rest] = segments if (rest.length > 0) throw notFound("no such page") const documentId = segment === undefined || segment === "retention" ? undefined : documentIdOf(segment) if (documentId !== undefined && !DOCUMENT_ID.test(documentId)) throw notFound("no such page") try { if (segment === "retention") { const [classes, due] = await Promise.all([caller.call(retentionList, slugs), caller.call(disposalList, slugs)]) return { kind: "retention", title: t("retention.title"), intro: t("retention.intro"), back: t("nav.documents"), list: retentionLabels(t), form: retentionFormLabels(t, failed, refusals), bases: baseNames(t), classes: classes.rows, due: due.rows, } } if (documentId === undefined) { const [documents, classes] = await Promise.all([ caller.call(documentList, slugs), caller.call(retentionList, slugs), ]) return { kind: "list", title, intro: t("intro"), retentionLink: t("nav.retention"), form: registerFormLabels(t, failed, refusals), list: documentListLabels(t, failed, refusals), statuses: statusNames(t), classes: classes.rows, documents, } } const { document, versions } = await caller.call(documentGet, { ...slugs, documentId }) return { kind: "document", title, back: t("document.back"), document, status: statusNames(t)[document.status], versions, labels: { retentionClass: t("document.retentionClass"), status: t("document.status"), revision: t("document.revision"), created: t("document.created"), hold: t("document.hold"), held: document.legalHold ? t("document.yes") : t("document.no"), disposedAt: t("document.disposedAt"), versions: versionsLabels(t, versions.length), versionForm: versionFormLabels(t, failed, refusals), actions: documentActionsLabels(t, failed, refusals), }, } } catch (error) { const notice = serviceState(error, i18n.locale) if (!notice) throw error return { kind: "state", title, notice } } }, View({ slugs, data, locale }: ProjectPageViewProps): ReactElement { const listHref = `/${slugs.org}/${slugs.workspace}/${slugs.project}/archive` return data.kind === "state" ? ( ) : ( <> {data.kind === "list" ? ( <>

{data.intro}

{data.retentionLink}

) : data.kind === "retention" ? ( <>

{data.back}

{data.intro}

) : ( <>

{data.back}

{data.document.title}

{data.labels.status}
{data.status}
{data.labels.retentionClass}
{data.document.retentionClass}
{data.labels.created}
{data.document.createdAt.slice(0, 10)}
{data.labels.revision}
{data.document.revision}
{data.labels.hold}
{data.labels.held}
{data.document.disposedAt === null ? null : ( <>
{data.labels.disposedAt}
{data.document.disposedAt.slice(0, 10)}
)}
{data.document.disposed ? null : ( <> )} )} ) }, })