@@ -0,0 +1,136 @@
|
||||
// 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>
|
||||
)
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user