"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, NoteView>("notes.note.create", { org: slugs.org }) const update = useWrite, NoteView>("notes.note.update", { org: slugs.org }) const remove = useWrite, 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()) 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>({ 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 }