@@ -0,0 +1,299 @@
|
||||
// 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 `/<org>/<workspace>/<project>/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/<documentId> 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<Record<DocumentView["status"], string>>
|
||||
|
||||
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<Record<RetentionClassView["from"], string>>
|
||||
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<TransportCaller, I18n, Container, ArchivePageData>
|
||||
|
||||
/** 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<ArchivePageData> {
|
||||
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<ArchivePageData>): ReactElement {
|
||||
const listHref = `/${slugs.org}/${slugs.workspace}/${slugs.project}/archive`
|
||||
return data.kind === "state" ? (
|
||||
<ServiceStatePage title={data.title} notice={data.notice} />
|
||||
) : (
|
||||
<>
|
||||
<PageHeader title={data.title} />
|
||||
{data.kind === "list" ? (
|
||||
<>
|
||||
<p>{data.intro}</p>
|
||||
<p>
|
||||
<a href={`${listHref}/retention`}>{data.retentionLink}</a>
|
||||
</p>
|
||||
<Can permission={ARCHIVE_PERMISSIONS.write}>
|
||||
<RegisterForm slugs={slugs} classes={data.classes} listHref={listHref} labels={data.form} />
|
||||
</Can>
|
||||
<DocumentList
|
||||
locale={locale}
|
||||
slugs={slugs}
|
||||
seed={data.documents}
|
||||
statuses={data.statuses}
|
||||
listHref={listHref}
|
||||
labels={data.list}
|
||||
/>
|
||||
</>
|
||||
) : data.kind === "retention" ? (
|
||||
<>
|
||||
<p>
|
||||
<a href={listHref}>{data.back}</a>
|
||||
</p>
|
||||
<p>{data.intro}</p>
|
||||
<Can permission={ARCHIVE_PERMISSIONS.manage}>
|
||||
<RetentionForm slugs={slugs} labels={data.form} />
|
||||
</Can>
|
||||
<RetentionClasses
|
||||
classes={data.classes}
|
||||
due={data.due}
|
||||
bases={data.bases}
|
||||
listHref={listHref}
|
||||
labels={data.list}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
<a href={listHref}>{data.back}</a>
|
||||
</p>
|
||||
<h2>{data.document.title}</h2>
|
||||
<dl>
|
||||
<dt>{data.labels.status}</dt>
|
||||
<dd>{data.status}</dd>
|
||||
<dt>{data.labels.retentionClass}</dt>
|
||||
<dd>{data.document.retentionClass}</dd>
|
||||
<dt>{data.labels.created}</dt>
|
||||
<dd>{data.document.createdAt.slice(0, 10)}</dd>
|
||||
<dt>{data.labels.revision}</dt>
|
||||
<dd>{data.document.revision}</dd>
|
||||
<dt>{data.labels.hold}</dt>
|
||||
<dd>{data.labels.held}</dd>
|
||||
{data.document.disposedAt === null ? null : (
|
||||
<>
|
||||
<dt>{data.labels.disposedAt}</dt>
|
||||
<dd>{data.document.disposedAt.slice(0, 10)}</dd>
|
||||
</>
|
||||
)}
|
||||
</dl>
|
||||
<Versions versions={data.versions} labels={data.labels.versions} />
|
||||
{data.document.disposed ? null : (
|
||||
<>
|
||||
<Can permission={ARCHIVE_PERMISSIONS.write}>
|
||||
<VersionForm
|
||||
slugs={slugs}
|
||||
documentId={data.document.documentId}
|
||||
revision={data.document.revision}
|
||||
labels={data.labels.versionForm}
|
||||
/>
|
||||
</Can>
|
||||
<Can permission={ARCHIVE_PERMISSIONS.manage}>
|
||||
<DocumentActions
|
||||
slugs={slugs}
|
||||
documentId={data.document.documentId}
|
||||
revision={data.document.revision}
|
||||
legalHold={data.document.legalHold}
|
||||
labels={data.labels.actions}
|
||||
/>
|
||||
</Can>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user