From 0e5a31866ff654c9dce86b4e6e2840d8367404e8 Mon Sep 17 00:00:00 2001 From: francy51 Date: Fri, 15 May 2026 00:27:09 -0400 Subject: [PATCH] feat(memo): add and delete sections via RPC Add memo.addSection and memo.deleteSection RPC methods with full-stack implementation: contract types, Zod schemas, DB queries, RPC handlers, UI buttons, and tests. - memo.addSection: creates a new empty section, supports positional insert after a given section via afterSectionId - memo.deleteSection: removes a section and cascades cleanup of its annotations, citations, and section reviews - Outline sidebar: per-section delete button, + Add Section button - Article area: + Add Section button after last section - 9 new tests (15 total memo tests, 48 project-wide) --- apps/web/src/ui/app/AppShell.tsx | 120 ++++++++++++++++++-- packages/contracts/src/rpc.ts | 15 +++ packages/contracts/src/rpcSchemas.ts | 15 +++ packages/shared/src/db/queries.ts | 42 +++++++ packages/shared/src/rpc/memoRpc.test.ts | 145 ++++++++++++++++++++++++ packages/shared/src/rpc/memoRpc.ts | 21 ++++ 6 files changed, 346 insertions(+), 12 deletions(-) diff --git a/apps/web/src/ui/app/AppShell.tsx b/apps/web/src/ui/app/AppShell.tsx index 439c09e..36c808d 100644 --- a/apps/web/src/ui/app/AppShell.tsx +++ b/apps/web/src/ui/app/AppShell.tsx @@ -1914,6 +1914,8 @@ function Memo({ >({}); const [lastSavedAt, setLastSavedAt] = useState(null); const [saveError, setSaveError] = useState(null); + const [addingSection, setAddingSection] = useState(false); + const [deletingSectionId, setDeletingSectionId] = useState(null); const saveTimers = useRef>({}); const sections = sectionsDraft; const activeIndex = Math.max( @@ -2123,6 +2125,64 @@ function Memo({ else setSaveError(result.error.message); } + async function addSection(afterSectionId?: string) { + if (!company) return; + setAddingSection(true); + try { + const index = afterSectionId + ? sections.findIndex((s) => s.id === afterSectionId) + : sections.length - 1; + const title = `New Section`; + const result = await rpc.call("memo.addSection", { + companyId: company.id, + title, + afterSectionId, + }); + if (result.ok) { + const newSection = result.data.section; + setSectionsDraft((prev) => { + const next = [...prev]; + next.splice(index + 1, 0, newSection); + return next; + }); + setActiveSectionId(newSection.id); + setSaveStateBySection((prev) => ({ ...prev, [newSection.id]: "saved" })); + addToast({ type: "success", title: "Section added", desc: `"${title}" added to memo` }); + } else { + addToast({ type: "error", title: "Could not add section", desc: result.error.message }); + } + } finally { + setAddingSection(false); + } + } + + async function deleteSection(sectionId: string) { + if (!company) return; + if (sections.length <= 1) { + addToast({ type: "warning", title: "Cannot delete", desc: "A memo must have at least one section." }); + return; + } + setDeletingSectionId(sectionId); + try { + const result = await rpc.call("memo.deleteSection", { + companyId: company.id, + sectionId, + }); + if (result.ok) { + setSectionsDraft((prev) => prev.filter((s) => s.id !== sectionId)); + if (activeSectionId === sectionId) { + const remaining = sections.filter((s) => s.id !== sectionId); + setActiveSectionId(remaining[0]?.id ?? ""); + } + addToast({ type: "success", title: "Section deleted" }); + } else { + addToast({ type: "error", title: "Could not delete section", desc: result.error.message }); + } + } finally { + setDeletingSectionId(null); + } + } + const activeCitations = memo.citations.filter( (c) => c.sectionId === activeSectionId, ); @@ -2176,22 +2236,43 @@ function Memo({ {!outlineCollapsed && sections.map((s, i) => ( - + {s.title} + {s.primaryAgent && ( + + [{s.primaryAgent}] + + )} + + + ))} + {!outlineCollapsed && ( + + )}
@@ -2309,6 +2390,13 @@ function Memo({ )} ))} +
{rightPanelCollapsed ? (