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

137 lines
5.3 KiB
TypeScript

// The notes module's page, on the shell's F10 contract (docs/reference/app-kit.md): a loader the web
// shell runs on the server under `/<org>/<workspace>/<project>/notes`, and the view of what it returned.
//
// /notes the list, on `ResourcePage`: a note opens, and a new one starts, in the right panel
// /notes/<noteId> one note in full: its text, its attachments, deleting it
//
// Every read is a procedure through the same caller the API uses (spec §6.2: one API path), and every
// label is translated here, on the server: the view receives strings and rows, never a caller or a
// catalogue. Every write is a client component that calls a procedure through the shell's transport.
// This is the worked example the other modules' pages follow (apps/web/ROUTES.md).
import { type Container, notFound, type TransportCaller } from "@conusai-kit/core"
import type { I18n } from "@conusai-kit/i18n"
import { type ProjectPage, type ProjectPageViewProps, projectPage } from "@conusai-kit/ui"
import { Page, PageHeader, PageSection } from "@conusai-kit/ui/patterns"
import { Button } from "@conusai-kit/ui/primitives"
import type { ReactElement } from "react"
import { ATTACHMENT_CONTENT_TYPES, type AttachmentView, type NoteView, noteGet, noteList } from "../contract/index.js"
import { FeedbackButton } from "../guides/feedback-button.js" // kit:guides
import { type FeedbackButtonLabels, feedbackLabels } from "../guides/feedback-labels.js" // kit:guides
import { type AttachmentLabels, Attachments } from "./attachments.js"
import { DeleteNote, type DeleteNoteLabels } from "./delete-note.js"
import { attachmentLabels, deleteLabels, type NotesResourceLabels, resourceLabels } from "./labels.js"
import { NotesResource } from "./notes-resource.js"
/** The list: its labels and the first page of notes, the resource's seed. */
export interface NotesListData {
readonly kind: "list"
readonly intro: string
readonly feedback: FeedbackButtonLabels // kit:guides
readonly labels: NotesResourceLabels
readonly notes: readonly NoteView[]
}
/** One note, with its attachments as the live list's seed. */
export interface NoteDetailData {
readonly kind: "note"
readonly back: string
readonly changed: string
readonly note: { readonly noteId: string; readonly title: string; readonly body: string }
/** What the link to the note's panel on the list says. */
readonly edit: string
readonly attachments: readonly AttachmentView[]
readonly attachmentsEnabled: boolean
readonly attachmentLabels: AttachmentLabels
readonly remove: DeleteNoteLabels
}
export type NotesPageData = NotesListData | NoteDetailData
export type NotesPage = ProjectPage<TransportCaller, I18n, Container, NotesPageData>
const listHrefOf = ({ org, workspace, project }: ProjectPageViewProps<unknown>["slugs"]): string =>
`/${org}/${workspace}/${project}/notes`
export const notesPage: NotesPage = projectPage({
async load({ slugs, segments, caller, i18n }): Promise<NotesPageData> {
const t = (key: string, values?: Record<string, string | number>) => i18n.message(`notes.${key}`, values)
if (segments.length === 0) {
const { notes } = await caller.call(noteList, slugs)
return {
kind: "list",
intro: t("intro"),
feedback: feedbackLabels(t), // kit:guides
labels: resourceLabels(t),
notes,
}
}
const [noteId, ...rest] = segments
if (noteId === undefined || rest.length > 0) throw notFound("no such page")
const { note, attachments, attachmentsEnabled } = await caller.call(noteGet, { ...slugs, noteId })
return {
kind: "note",
back: t("detail.back"),
changed: t("detail.changed", { date: i18n.date(new Date(note.updatedAt)) }),
note: { noteId: note.noteId, title: note.title, body: note.body },
edit: t("detail.edit"),
attachments,
attachmentsEnabled,
attachmentLabels: attachmentLabels(t),
remove: deleteLabels(t),
}
},
View({ slugs, locale, data }: ProjectPageViewProps<NotesPageData>): ReactElement {
const listHref = listHrefOf(slugs)
if (data.kind === "list") {
return (
<NotesResource
slugs={slugs}
seed={data.notes}
labels={data.labels}
locale={locale}
intro={
<>
{data.intro}
<FeedbackButton labels={data.feedback} /* kit:guides */ />
</>
}
/>
)
}
const { note } = data
return (
<Page>
<PageHeader
title={note.title}
description={data.changed}
breadcrumb={<a href={listHref}>{data.back}</a>}
actions={
<>
<Button asChild variant="outline">
<a href={`${listHref}?record=${note.noteId}`}>{data.edit}</a>
</Button>
<DeleteNote slugs={slugs} noteId={note.noteId} listHref={listHref} labels={data.remove} />
</>
}
/>
<PageSection>
<p>{note.body}</p>
</PageSection>
<Attachments
slugs={slugs}
noteId={note.noteId}
seed={data.attachments}
enabled={data.attachmentsEnabled}
accept={ATTACHMENT_CONTENT_TYPES}
labels={data.attachmentLabels}
locale={locale}
/>
</Page>
)
},
})