Files
2026-11-14 09:56:45 +00:00

75 lines
2.7 KiB
TypeScript

"use client"
import { Button } from "@conusai-kit/ui/primitives"
// Adds a version: the browser stores the file through the files module, then asks this module to append
// it over the revision the page read. A version nobody else has overtaken lands; a stale one is refused.
import type { ProjectSlugs } from "@conusai-kit/ui"
import { describeError, useUpload, useWrite } from "@conusai-kit/ui/data"
import { serviceNotice, usePageNavigation } from "@conusai-kit/ui/patterns"
import { Alert, AlertDescription, Field, FieldLabel, Input } from "@conusai-kit/ui/primitives"
import { type FormEvent, type ReactElement, useState } from "react"
export interface VersionFormLabels {
readonly heading: string
readonly file: string
readonly submit: string
readonly busy: 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 VersionForm({
slugs,
documentId,
revision,
labels,
}: {
slugs: ProjectSlugs
documentId: string
revision: string
labels: VersionFormLabels
}): ReactElement {
const navigation = usePageNavigation()
const upload = useUpload()
const add = useWrite<Record<string, unknown>, unknown>("archive.version.add")
const [requestId, setRequestId] = 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 file = new FormData(event.currentTarget).get("file")
if (!(file instanceof File) || file.size === 0) return
setState({ kind: "busy" })
try {
const stored = await upload(slugs, file)
await add.mutate({ ...slugs, documentId, revision, fileId: stored.fileId, requestId })
await navigation.refresh()
setRequestId(crypto.randomUUID())
setState({ kind: "idle" })
} catch (error) {
setState({ kind: "failed", detail: serviceNotice(error, labels.refusals)?.text ?? describeError(error) })
}
}
return (
<form key={requestId} onSubmit={submit} aria-busy={state.kind === "busy"}>
<h2>{labels.heading}</h2>
<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>
)
}