@@ -0,0 +1,70 @@
|
||||
"use client"
|
||||
|
||||
// The project's notes as one record type: the list, a new note and an edit in the right panel, all from this
|
||||
// declaration. Attachments need a page, so the panel offers it ("Open in full"). The rows are live (live-notes.ts);
|
||||
// a write resolves once the stream has seen it, so the list shows it the moment the panel closes.
|
||||
|
||||
import { emitAppEvent, type ProjectSlugs } from "@conusai-kit/ui"
|
||||
import { useDataClient, useWrite } from "@conusai-kit/ui/data"
|
||||
import { defineResource, ResourcePage } from "@conusai-kit/ui/patterns"
|
||||
import { type ReactElement, type ReactNode, useRef } from "react"
|
||||
import { z } from "zod"
|
||||
import type { NoteView } from "../contract/index.js"
|
||||
import type { NotesResourceLabels } from "./labels.js"
|
||||
import { useLiveNotes } from "./live-notes.js"
|
||||
|
||||
const schema = z.object({ title: z.string().min(1).max(200), body: z.string().max(20_000) })
|
||||
|
||||
export interface NotesProps {
|
||||
readonly slugs: ProjectSlugs
|
||||
/** The first page, rendered on the server. */
|
||||
readonly seed: readonly NoteView[]
|
||||
readonly labels: NotesResourceLabels
|
||||
readonly intro: ReactNode
|
||||
/** The locale the server formatted dates in, so the rows read as they were rendered. */
|
||||
readonly locale: string
|
||||
}
|
||||
|
||||
export function NotesResource({ slugs, seed, labels, intro, locale }: NotesProps): ReactElement {
|
||||
const client = useDataClient()
|
||||
const rows = useLiveNotes(slugs, seed)
|
||||
const create = useWrite<Record<string, unknown>, NoteView>("notes.note.create", { org: slugs.org })
|
||||
const update = useWrite<Record<string, unknown>, NoteView>("notes.note.update", { org: slugs.org })
|
||||
const remove = useWrite<Record<string, unknown>, unknown>("notes.note.delete", { org: slugs.org })
|
||||
// A save carries the version the panel read, so a second editor's is a 409 and never a silent overwrite.
|
||||
const read = useRef(new Map<string, number>())
|
||||
for (const { noteId, version } of rows) if (!read.current.has(noteId)) read.current.set(noteId, version)
|
||||
const day = new Intl.DateTimeFormat(locale).format
|
||||
const resource = defineResource<NoteView, z.output<typeof schema>>({
|
||||
id: "notes",
|
||||
label: labels.one,
|
||||
newLabel: labels.new,
|
||||
columns: [
|
||||
{ id: "title", header: labels.note, wrap: true, minWidth: "10rem", allowsSorting: true },
|
||||
{ id: "attachmentCount", header: labels.attachments, align: "end" },
|
||||
{ id: "updatedAt", header: labels.updated, allowsSorting: true, cell: (n) => day(new Date(n.updatedAt)) },
|
||||
],
|
||||
rowId: (note) => note.noteId,
|
||||
source: { rows },
|
||||
async get(noteId) {
|
||||
const { value } = await client.call<{ note: NoteView }>("notes.note.get", { ...slugs, noteId })
|
||||
read.current.set(noteId, value.note.version)
|
||||
return value.note
|
||||
},
|
||||
schema,
|
||||
fields: [
|
||||
{ name: "title", label: labels.titleField, kind: "text" },
|
||||
{ name: "body", label: labels.body, kind: "textarea" },
|
||||
],
|
||||
async create(input) {
|
||||
const note = await create.mutate({ ...slugs, ...input })
|
||||
await emitAppEvent("note_created") // what an in-app guide's checklist task listens for
|
||||
return note
|
||||
},
|
||||
update: (id, input) => update.mutate({ ...slugs, noteId: id, ...input, expectedVersion: read.current.get(id) }),
|
||||
remove: (noteId) => remove.mutate({ ...slugs, noteId }).then(() => undefined),
|
||||
href: (n) => `/${slugs.org}/${slugs.workspace}/${slugs.project}/notes/${n.noteId}`,
|
||||
permissions: { write: "notes.note.write", remove: "notes.note.delete" },
|
||||
})
|
||||
return <ResourcePage resource={resource} title={labels.title} description={intro} empty={{ title: labels.empty }} />
|
||||
}
|
||||
Reference in New Issue
Block a user