"use client" // A note's attachments: the list with a remove button on each, and the upload. // // An attachment is two steps, both the API's own: the browser sends the file to the files module // (the kit's `uploadFile`: `files.file.beginUpload`, one `files.file.putPart` per piece, and // `files.file.completeUpload`, which answer before anything that follows an upload has run), and // then links the stored file to the note (`notes.attachment.add`). A file uploaded and never linked // is an ordinary file of the project. import { downloadHref, type ProjectSlugs, uploadFile, useTransport } from "@conusai-kit/ui" import { type FormEvent, type ReactElement, useId, useState } from "react" export interface AttachmentRow { readonly attachmentId: string readonly fileId: string readonly filename: string readonly contentType: string /** Already formatted: a size is a message of the module, not arithmetic of the component. */ readonly size: string } export interface AttachmentLabels { readonly title: string readonly count: string readonly empty: string readonly file: string readonly type: string readonly sizeHeading: string readonly actions: string /** `Remove {filename}`: the button's accessible name, with the placeholder still in it. */ readonly remove: string /** The button's visible text, which its accessible name contains. */ readonly removeShort: string /** `Download {filename}`: a same-origin, project-scoped file stream. */ readonly download: string readonly off: string readonly uploadLabel: string readonly uploadHint: string readonly uploadSubmit: string readonly uploadBusy: string readonly uploadDone: string readonly failed: string } type State = { kind: "idle" | "busy" | "done" } | { kind: "failed"; detail: string } export function Attachments({ slugs, noteId, rows, enabled, accept, labels, }: { slugs: ProjectSlugs noteId: string rows: readonly AttachmentRow[] enabled: boolean accept: readonly string[] labels: AttachmentLabels }): ReactElement { const transport = useTransport() const { call, describe } = transport const inputId = useId() const hintId = useId() const [state, setState] = useState({ kind: "idle" }) const attach = async (event: FormEvent): Promise => { event.preventDefault() const form = event.currentTarget const file = new FormData(form).get("file") if (!(file instanceof File) || file.size === 0) return setState({ kind: "busy" }) try { const stored = await uploadFile(transport, slugs, file) await call("notes.attachment.add", { ...slugs, noteId, fileId: stored.fileId }) form.reset() setState({ kind: "done" }) window.location.reload() } catch (error) { setState({ kind: "failed", detail: describe(error) }) } } const remove = async (attachmentId: string): Promise => { setState({ kind: "busy" }) try { await call("notes.attachment.remove", { ...slugs, noteId, attachmentId }) window.location.reload() } catch (error) { setState({ kind: "failed", detail: describe(error) }) } } return (

{labels.title}

{labels.count}

{rows.length === 0 ? (

{labels.empty}

) : ( {rows.map((row) => ( ))}
{labels.title}
{labels.file} {labels.type} {labels.sizeHeading} {labels.actions}
{row.filename} {row.contentType} {row.size} {labels.download.replace("{filename}", row.filename)} {" "}
)} {enabled ? (

{labels.uploadHint}

{state.kind === "done" ? labels.uploadDone : ""}

) : (

{labels.off}

)} {state.kind === "failed" ? (

{labels.failed.replace("{detail}", state.detail)}

) : null}
) }