137 lines
5.3 KiB
TypeScript
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>
|
|
)
|
|
},
|
|
})
|