Files
template-app/modules/archive/ui/register-form.tsx
T
2026-11-14 09:56:45 +00:00

107 lines
3.8 KiB
TypeScript

"use client"
import { Button } from "@conusai-kit/ui/primitives"
// Registers a document: the browser stores the file through the files module, then asks this module to
// register it, which takes the version's hash, size and type from the stored file. Then it opens the
// document.
import type { ProjectSlugs } from "@conusai-kit/ui"
import { describeError, useUpload, useWrite } from "@conusai-kit/ui/data"
import { SelectField, serviceNotice, usePageNavigation } from "@conusai-kit/ui/patterns"
import { Alert, AlertDescription, Field, FieldDescription, FieldLabel, Input } from "@conusai-kit/ui/primitives"
import { type FormEvent, type ReactElement, useState } from "react"
import type { RetentionClassView } from "../contract/index.js"
export interface RegisterFormLabels {
readonly heading: string
readonly title: string
readonly kind: string
readonly kindHint: string
readonly retentionClass: string
readonly file: string
readonly submit: string
readonly busy: string
readonly noClass: string
readonly failed: string
/** The text of each business refusal by problem code (`businessRefusalNotices` of `@conusai-kit/business/refusals`). */
readonly refusals: Readonly<Record<string, string>>
}
export function RegisterForm({
slugs,
classes,
listHref,
labels,
}: {
slugs: ProjectSlugs
classes: readonly RetentionClassView[]
listHref: string
labels: RegisterFormLabels
}): ReactElement {
const navigation = usePageNavigation()
const upload = useUpload()
const register = useWrite<Record<string, unknown>, { documentId: string }>("archive.document.register")
const [requestId] = useState(() => crypto.randomUUID())
const [state, setState] = useState<{ kind: "idle" | "busy" } | { kind: "failed"; detail: string }>({ kind: "idle" })
const submit = async (event: FormEvent<HTMLFormElement>): Promise<void> => {
event.preventDefault()
const form = new FormData(event.currentTarget)
const file = form.get("file")
if (!(file instanceof File) || file.size === 0) return
const kind = String(form.get("kind") ?? "").trim()
setState({ kind: "busy" })
try {
const stored = await upload(slugs, file)
const created = await register.mutate({
...slugs,
title: String(form.get("title") ?? "").trim(),
...(kind ? { kind } : {}),
retentionClass: String(form.get("retentionClass") ?? ""),
fileId: stored.fileId,
requestId,
})
navigation.navigate(`${listHref}/${encodeURIComponent(created.documentId)}`)
} catch (error) {
setState({ kind: "failed", detail: serviceNotice(error, labels.refusals)?.text ?? describeError(error) })
}
}
return classes.length === 0 ? (
<p>{labels.noClass}</p>
) : (
<form onSubmit={submit} aria-busy={state.kind === "busy"}>
<h2>{labels.heading}</h2>
<Field>
<FieldLabel>{labels.title}</FieldLabel>
<Input name="title" type="text" required maxLength={200} />
</Field>
<Field>
<FieldLabel>{labels.kind}</FieldLabel>
<Input name="kind" type="text" pattern="[0-9]{1,4}" />
<FieldDescription>{labels.kindHint}</FieldDescription>
</Field>
<SelectField
name="retentionClass"
label={labels.retentionClass}
required
defaultValue={classes[0]?.code}
options={classes.map((retention) => ({ value: retention.code, label: retention.code }))}
/>
<Field>
<FieldLabel>{labels.file}</FieldLabel>
<Input name="file" type="file" required />
</Field>
<Button type="submit" disabled={state.kind === "busy"}>
{state.kind === "busy" ? labels.busy : labels.submit}
</Button>
{state.kind === "failed" ? (
<Alert variant="destructive">
<AlertDescription>{labels.failed.replace("{detail}", state.detail)}</AlertDescription>
</Alert>
) : null}
</form>
)
}