Integrate docs editor with frontend and add peer cursor labels#72
Conversation
DocsDetail wraps the document editor with DocumentProvider for Yorkie collaboration, handling auth and creating the initial document root. DocsView mounts the Canvas editor via YorkieDocStore and wires remote changes to trigger re-renders. Also fixes pre-existing lint errors in yorkie-doc-store files. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Define YorkieDocsRoot type for the docs editor Yorkie document. Add @wafflebase/docs resolution to test hooks and workspace dependency. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Lazy-load DocsDetail for collaborative document editing at /docs/:id. Add Doc.refresh() call before render() in onRemoteChange to ensure the editor's cached document is updated from the store. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Archive completed 20260322-docs-collaboration-todo.md and add new task plan for YorkieDocStore + frontend integration. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
The /docs path conflicts with the Vite dev server proxy that forwards /docs/* to the VitePress documentation site on port 5174. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add Canvas-based document editor alongside sheets: formatting toolbar (bold, italic, underline, strikethrough, text color, alignment), share dialog, user presence, and sidebar layout. Fix multi-line block style application, sidebar resize/scroll issues, and share link routing for docs type. Standardize naming: product "Sheets"/"Docs", singular "Sheet"/"Document", internal type values "sheet"/"doc". Add /s/:id route for sheets matching /d/:id for docs. Update @yorkie-js/sdk and @yorkie-js/react to 0.7.3-alpha. Co-Authored-By: Claude Opus 4.6 <[email protected]>
The docs editor integration and Yorkie 0.7.3-alpha update pushed chunk count to 65 and the Yorkie vendor bundle to 671 KB. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds Canvas-based docs editor integration: DB migration for Document.type, backend API changes to accept/return type, frontend types/routing/UI updates, Yorkie-backed DocStore and editor wiring, theming/runtime theme API, tests, resolver/config, and docs/task updates. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Frontend as Frontend UI
participant API as Backend API
participant DB as Database
participant Yorkie as Yorkie (CRDT)
participant Editor as Docs Editor
User->>Frontend: Create new document (select type)
Frontend->>API: POST /documents { title, type }
API->>DB: INSERT Document (type)
DB-->>API: Created document
API-->>Frontend: Document response (includes type)
alt type == "doc"
User->>Frontend: Navigate to /d/:id
Frontend->>Yorkie: Initialize doc-{id}
Yorkie-->>Frontend: Document ready
Frontend->>Editor: initialize(container, YorkieDocStore, theme)
Editor-->>Frontend: EditorAPI ready
User->>Editor: Edit / format
Editor->>Yorkie: doc.update() (Tree edits)
Yorkie-->>Editor: remote-change event
Editor->>Frontend: onRemoteChange -> refresh + render
else type == "sheet"
User->>Frontend: Navigate to /s/:id
Frontend->>Editor: Initialize spreadsheet editor
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Resolve conflict in docs/design/README.md by keeping both entries. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Verification: verify:selfResult: ✅ PASS in 102.5s
Verification: verify:integrationResult: ✅ PASS |
The frontend test resolve hook falls back to @wafflebase/docs source when dist is missing, but Node's strip-only mode cannot parse TypeScript parameter properties. Building docs dist first resolves this in CI. Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/docs/src/view/ruler.ts (1)
77-86:⚠️ Potential issue | 🟡 MinorCorner element background won't update on theme change.
The corner element's
backgroundis set once during construction at line 85. WhensetThemeMode()switches themes, therender()method repaints the canvases with fresh theme colors, but the corner div retains its original background color.🐛 Proposed fix: Update corner background in render()
Add a line to update the corner background at the start of
render():render( paginatedLayout: PaginatedLayout, scrollY: number, canvasWidth: number, viewportHeight: number, cursorBlockStyle: BlockStyle | null, cursorPageIndex: number = 0, ): void { if (paginatedLayout.pages.length === 0) return; + + // Update corner background for theme changes + this.corner.style.background = marginBg(); const page = paginatedLayout.pages[cursorPageIndex] ?? paginatedLayout.pages[0];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/docs/src/view/ruler.ts` around lines 77 - 86, The corner div's background is only set in the constructor (corner) so it doesn't update when themes change; update the corner's background at the start of render() by calling marginBg() and assigning it to this.corner.style.background (or updating cssText) so the corner reflects the current theme whenever render() runs; locate the corner initialization in the constructor and add the background update inside the render() method (which is invoked by setThemeMode()) to keep styling in sync.
🧹 Nitpick comments (12)
packages/backend/prisma/schema.prisma (1)
35-35: LGTM!The
typefield with@default("sheet")ensures backward compatibility for existing documents.Consider using a Prisma
enuminstead ofStringif the valid types ("sheet","doc") are fixed:♻️ Optional: Use enum for type safety
+enum DocumentType { + sheet + doc +} + model Document { id String `@default`(uuid()) `@id` title String - type String `@default`("sheet") + type DocumentType `@default`(sheet) ... }This provides database-level validation and better type safety in generated Prisma Client code.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/backend/prisma/schema.prisma` at line 35, Replace the String-typed "type" field with a Prisma enum to enforce allowed values and DB-level validation: add an enum (e.g., DocumentType with members sheet and doc), change the "type" field's type from String to that enum (e.g., DocumentType) and set its `@default` to the enum member (e.g., sheet), then run Prisma migrate/generate so Prisma Client types are updated; target the "type" field and the schema's enums when making the change.packages/frontend/vite.config.ts (1)
60-80: Consider adding chunk configuration for docs package.The
manualChunksfunction has dedicated chunks forsheet-formula-parser,sheet-formula-eval, andsheet-core. The new@wafflebase/docspackage doesn't have similar chunking rules, so its code will be bundled into the main application chunk. If the docs package is substantial, you may want to add adocs-corechunk for better caching and parallel loading.💡 Optional: Add docs package chunking
if ( normalizedId.includes("/packages/sheets/src/view/") || normalizedId.includes("/packages/sheets/src/model/") || normalizedId.includes("/packages/sheets/src/store/") ) { return "sheet-core"; } + if (normalizedId.includes("/packages/docs/src/")) { + return "docs-core"; + } + return undefined; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/vite.config.ts` around lines 60 - 80, Add a manualChunks rule to split the `@wafflebase/docs` package into its own chunk: detect normalizedId.includes("@wafflebase/docs") (or the package path under /packages/docs/) in the same manualChunks function that currently returns "sheet-formula-parser", "sheet-formula-eval", and "sheet-core", and return a new chunk name like "docs-core" so docs code is bundled separately for better caching and parallel loading.packages/frontend/src/api/workspaces.ts (1)
227-238: Consider adding return type annotation for consistency.Other workspace functions like
createWorkspace()andupdateWorkspace()have explicit return type annotations (e.g.,Promise<Workspace>). This function lacks one, which reduces type safety at call sites.♻️ Proposed fix
+import type { Document } from "@/types/documents"; + export async function createWorkspaceDocument( workspaceId: string, data: { title: string; type?: "sheet" | "doc" }, -) { +): Promise<Document> { const res = await fetchWithAuth(`${BASE}/${workspaceId}/documents`, {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/api/workspaces.ts` around lines 227 - 238, createWorkspaceDocument lacks an explicit return type which reduces type safety; update the function signature for createWorkspaceDocument to include an explicit Promise-return type (e.g., Promise<Document> or the project-specific document type you use) and ensure that type is imported or defined where necessary so the returned res.json() result matches the annotated type.packages/backend/src/api/v1/documents.controller.ts (1)
34-38: Consider stricter type validation or a comment explaining the default behavior.Using
type?: stringallows any string value, which is then silently converted to'sheet'if not exactly'doc'. This is a valid fail-safe approach, but consider either:
- Adding a brief comment explaining the intentional defaulting, or
- Using a DTO with validation (e.g.,
class-validator) to reject invalid type values♻️ Option 1: Add clarifying comment
`@Post`() async create( `@Param`('workspaceId') workspaceId: string, `@Req`() req: AuthenticatedRequest, `@Body`() body: { title: string; type?: string }, ) { return this.documentService.createDocument({ title: body.title, + // Default to 'sheet' for missing or unrecognized type values type: body.type === 'doc' ? 'doc' : 'sheet',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/backend/src/api/v1/documents.controller.ts` around lines 34 - 38, The current controller accepts `@Body`() body: { title: string; type?: string } and maps body.type === 'doc' ? 'doc' : 'sheet' which silently coerces any invalid string to 'sheet'; update this by either (A) replacing the loose inline type with a dedicated DTO (e.g., DocumentCreateDto) that declares type: 'doc' | 'sheet' and add class-validator decorators to enforce allowed values before calling documentService.createDocument, or (B) at minimum add a clarifying comment next to the mapping (in the create handler) explaining the intentional default-to-'sheet' behavior and consider narrowing the parameter type to type?: 'doc' | 'sheet' so invalid values are flagged by TypeScript; reference body.type, the controller create handler, and documentService.createDocument when making the change.packages/frontend/src/app/shared/shared-document.tsx (1)
194-202: Type annotation should usePartial<YorkieDocsRoot>for consistency.The
YorkieDocsRoottype requires acontent: Treeproperty, butinitialRoot={{}}provides an empty object. While this works at runtime (Yorkie initializes the Tree lazily), the type annotation creates a mismatch. The pattern indocs-detail.tsx(context snippet 3) correctly usesPartial<YorkieDocsRoot>for this case.♻️ Proposed fix
{isDocs ? ( - <DocumentProvider<YorkieDocsRoot> + <DocumentProvider<Partial<YorkieDocsRoot>> docKey={docKey} initialRoot={{}} initialPresence={presence} enableDevtools={import.meta.env.DEV} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/shared/shared-document.tsx` around lines 194 - 202, Change the generic type passed to DocumentProvider from YorkieDocsRoot to Partial<YorkieDocsRoot> so the provided initialRoot={{}} matches the type; update the DocumentProvider<...> usage in the component (the instance surrounding initialRoot and presence props that renders SharedDocsLayout) to DocumentProvider<Partial<YorkieDocsRoot>> to align with the pattern used in docs-detail.tsx and avoid the required content: Tree property mismatch.packages/frontend/src/app/documents/document-list.tsx (1)
295-323: Consider extracting the dropdown menu into a shared component.The "New Sheet / New Document" dropdown menu is duplicated between the header area (lines 295-323) and the empty state (lines 378-408). Extracting this into a reusable component would reduce duplication and ensure consistent behavior.
♻️ Example extraction
function NewDocumentDropdown({ onCreateSheet, onCreateDoc, buttonSize = "default" }: { onCreateSheet: () => void; onCreateDoc: () => void; buttonSize?: "default" | "sm"; }) { return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button size={buttonSize} className="flex items-center gap-2"> <Plus className="w-4 h-4" /> New </Button> </DropdownMenuTrigger> <DropdownMenuContent> <DropdownMenuItem onClick={onCreateSheet}> <Sheet className="mr-2 h-4 w-4 text-green-600" /> New Sheet </DropdownMenuItem> <DropdownMenuItem onClick={onCreateDoc}> <FileText className="mr-2 h-4 w-4 text-blue-500" /> New Document </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ); }Also applies to: 378-408
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/documents/document-list.tsx` around lines 295 - 323, Extract the duplicated "New" dropdown into a reusable component (e.g., NewDocumentDropdown) and replace both occurrences with it; implement NewDocumentDropdown to accept onCreateSheet and onCreateDoc callbacks (and an optional buttonSize prop) and internally render the DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, and two DropdownMenuItem entries with the Sheet and FileText icons, then update the usages that currently call createDocumentMutation.mutate(...) to pass wrapper callbacks like () => createDocumentMutation.mutate({ title: "New Sheet" }) and () => createDocumentMutation.mutate({ title: "New Document", type: "doc" }) so behavior remains identical.packages/frontend/src/app/docs/docs-formatting-toolbar.tsx (1)
117-171: Toggle buttons don't reflect current selection state.The
Togglecomponents useonPressedChangefor toggling but don't set thepressedprop to reflect whether the current selection has that style applied. This means the buttons won't visually indicate when text is already bold, italic, etc.♻️ Proposed fix to reflect active state
+ // Get current selection style for toggle states + const currentStyle = editor?.getSelectionStyle() ?? {}; + return ( <div className="flex items-center gap-0.5 overflow-x-auto border-b bg-background px-2 py-1 whitespace-nowrap"> {/* ... */} <Tooltip> <TooltipTrigger asChild> <Toggle size="sm" + pressed={!!currentStyle.bold} onPressedChange={toggleBold} className="h-7 w-7 cursor-pointer" aria-label="Bold" > <IconBold size={16} /> </Toggle> </TooltipTrigger> <TooltipContent>Bold ({modKey}+B)</TooltipContent> </Tooltip> {/* Similar for italic, underline, strikethrough */}Note: You may need to trigger re-renders when selection changes to keep the toggle states in sync.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/docs/docs-formatting-toolbar.tsx` around lines 117 - 171, The Toggle buttons in docs-formatting-toolbar.tsx must reflect the editor selection state: add the pressed prop to each Toggle (the ones using toggleBold, toggleItalic, toggleUnderline, toggleStrikethrough) and set it from the editor's active state (e.g., editor.isActive('bold'), editor.isActive('italic'), 'underline', 'strike' or equivalent command names used by your editor instance). Ensure the toolbar component subscribes to editor selection/transaction updates (or uses a reactive hook from your editor wrapper) so the component re-renders when selection/style state changes, keeping the pressed props in sync with the current selection.docs/tasks/active/20260323-docs-frontend-integration-todo.md (1)
1-25: Consider adding the companion lessons file.Per coding guidelines, non-trivial task documentation should use paired files:
YYYYMMDD-<slug>-todo.mdandYYYYMMDD-<slug>-lessons.md. If there are any lessons learned from this integration (e.g., the Yorkie Tree re-export workaround, route naming due to proxy conflicts), consider documenting them in20260323-docs-frontend-integration-lessons.md.Based on learnings: "Non-trivial task documentation uses paired files in
docs/tasks/active/:YYYYMMDD-<slug>-todo.mdandYYYYMMDD-<slug>-lessons.md."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/tasks/active/20260323-docs-frontend-integration-todo.md` around lines 1 - 25, Add a companion lessons file named 20260323-docs-frontend-integration-lessons.md alongside the existing 20260323-docs-frontend-integration-todo.md; in that file summarize post-work learnings (e.g., the Yorkie Tree re-export workaround, route naming/proxy conflicts, verification notes such as the frontend test failure), reference the spec docs/design/docs-frontend-integration.md, and follow the same header/metadata conventions used by other paired task files in docs/tasks/active so the two files are discoverable and consistent.packages/frontend/src/app/docs/docs-detail.tsx (2)
106-113: Consider adding error handling for rename operation.The
handleRenameDocumentfunction doesn't handle errors fromrenameDocument. If the API call fails, the user won't see feedback, and queries will still be invalidated.♻️ Suggested improvement
const handleRenameDocument = useCallback( async (newTitle: string) => { + try { await renameDocument(documentId, newTitle); queryClient.invalidateQueries({ queryKey: ["document", documentId] }); queryClient.invalidateQueries({ queryKey: ["documents"] }); + } catch (error) { + console.error("Failed to rename document:", error); + // Optionally show a toast notification to the user + } }, [documentId, queryClient], );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/docs/docs-detail.tsx` around lines 106 - 113, The rename handler handleRenameDocument should catch errors from renameDocument and avoid invalidating queries on failure; update handleRenameDocument to wrap the async call in try/catch, call renameDocument(documentId, newTitle) inside try, run queryClient.invalidateQueries(...) only on success, and in catch call an error reporting UI/notification or processLogger (e.g., show toast or set an error state) so the user gets feedback; ensure the function still rethrows or returns a boolean if calling code depends on success/failure.
148-150: Add explicit guard for missingidparameter.The
idparameter fromuseParamsis typed asstring | undefined, but Line 185 uses a non-null assertionid!. While the render logic would show a loader indefinitely ifidwere undefined, an explicit early return with an error message would be clearer.♻️ Suggested improvement
export function DocsDetail() { const { id } = useParams(); + + if (!id) { + return ( + <div className="flex h-full w-full items-center justify-center"> + <div className="text-red-500">Document ID is required</div> + </div> + ); + } const { data: currentUser,Then Line 185 becomes safe without the assertion:
- <DocsLayout documentId={id!} /> + <DocsLayout documentId={id} />Also applies to: 185-185
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/docs/docs-detail.tsx` around lines 148 - 150, The component DocsDetail reads the route param id (const { id } = useParams()) but later uses id! (on the value at the render/loader location around the current id usage on line 185); add an explicit guard early in DocsDetail that checks if id is undefined and immediately returns a clear error UI (or redirects) so the rest of the component can assume a present id, and then remove the non-null assertion (id!) wherever used (e.g., the usage around line 185) so code is safe and types align.packages/frontend/src/app/docs/yorkie-doc-store.ts (1)
292-303: Cache update pattern callsgetDocument()unnecessarily.After
doc.update(), callinggetDocument()rebuilds the document from the Yorkie tree. Since you already have the block to insert, you could update the cached document directly likeupdateBlockdoes.♻️ Suggested optimization
insertBlock(index: number, block: Block): void { + // Get current doc before mutation for cache update + const currentDoc = this.cachedDoc ? cloneDocument(this.cachedDoc) : this.getDocument(); this.doc.update((root) => { const tree = root.content; if (!tree || typeof tree.getRootTreeNode !== 'function') return; tree.editByPath([index], [index], buildBlockNode(block)); }); // Update cache in-place - const currentDoc = this.getDocument(); currentDoc.blocks.splice(index, 0, block); this.cachedDoc = currentDoc; this.dirty = false; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/docs/yorkie-doc-store.ts` around lines 292 - 303, insertBlock currently calls getDocument() after doc.update which forces a rebuild; instead update the in-memory cache directly like updateBlock: ensure this.cachedDoc exists (initialize from this.getDocument() only if null), then do this.cachedDoc.blocks.splice(index, 0, block) and assign this.cachedDoc (or keep same ref if intended), remove the local currentDoc variable and the extra getDocument() call, and keep setting this.dirty = false; reference: insertBlock, updateBlock, this.cachedDoc, doc.update, buildBlockNode, blocks.packages/frontend/src/app/docs/docs-view.tsx (1)
55-57:onEditorReadycallback type should explicitly allownullfor cleanup.The cleanup code casts
null as unknown as EditorAPIwhich bypasses type safety. The prop type should explicitly allownullso consumers know to handle both states.♻️ Suggested fix
interface DocsViewProps { - onEditorReady?: (editor: EditorAPI) => void; + onEditorReady?: (editor: EditorAPI | null) => void; }Then the cleanup becomes type-safe:
return () => { editor.dispose(); editorRef.current = null; - onEditorReady?.(null as unknown as EditorAPI); + onEditorReady?.(null); };Also applies to: 100-104
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/docs/docs-view.tsx` around lines 55 - 57, The prop onEditorReady in DocsViewProps should allow null to avoid unsafe casting; change its type to onEditorReady?: (editor: EditorAPI | null) => void and update any calls (including the cleanup that currently uses null as unknown as EditorAPI) to pass null directly; search for DocsViewProps and the onEditorReady invocations (including the cleanup block around the editor teardown) and remove the null cast so consumers receive EditorAPI | null safely.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@package.json`:
- Around line 56-58: Remove the duplicate root dependency entry for
"@yorkie-js/react" from the root package.json so it only exists where consumed;
specifically delete the "@yorkie-js/react": "0.7.3-alpha" line from the root
"dependencies" object and keep the declaration in
packages/frontend/package.json, then run your workspace install/update (pnpm
install) to refresh the lockfile/resolution.
In `@packages/docs/src/view/editor.ts`:
- Around line 355-371: getSelectionStyle currently returns an empty object when
no block is found which lets callers like docs-formatting-toolbar.tsx treat
undefined style properties as true when toggling; change getSelectionStyle to
always return a fully-defined InlineStyle default (e.g., boolean fields like
bold/italic/underline set to false and any other expected properties) instead of
{} so consumers receive concrete false values, and keep the existing fallback
that clones the last inline's style (function name: getSelectionStyle;
referenced by docs-formatting-toolbar.tsx).
In `@packages/frontend/src/app/docs/yorkie-doc-store.ts`:
- Around line 314-325: Add explicit bounds checking to deleteBlockByIndex:
before calling this.doc.update or tree.editByPath, validate that index is an
integer within [0, this.getDocument().blocks.length - 1]; if out of range, throw
the same error used in MemDocStore (or a clear RangeError) to match behavior.
Ensure the check covers the case where getDocument() or blocks is undefined and
short-circuits before calling tree.editByPath([index],[index+1]) and before
mutating cachedDoc, so you only perform the update, splice, and cachedDoc
assignment when the index is valid.
In `@packages/frontend/tests/resolve-hooks.mjs`:
- Around line 66-74: The `@wafflebase/docs` resolver branch (when specifier ===
"@wafflebase/docs") lacks the virtual stub fallback present for
`@wafflebase/sheets`; update that branch to return a virtual stub URL when neither
DOCS_DIST nor DOCS_SRC_INDEX exists (similar to how the sheets branch uses
SHEET_DIST/SHEET_SRC_INDEX fallback), and add a matching entry in the load(...)
handler that recognizes the docs virtual id and returns a minimal module (e.g.,
"export {}") so tests won't fail when the docs package hasn't been built or
checked out; use the same naming pattern as the sheets stub and keep behavior
consistent with nextResolve and the existing sheets virtual id handling.
---
Outside diff comments:
In `@packages/docs/src/view/ruler.ts`:
- Around line 77-86: The corner div's background is only set in the constructor
(corner) so it doesn't update when themes change; update the corner's background
at the start of render() by calling marginBg() and assigning it to
this.corner.style.background (or updating cssText) so the corner reflects the
current theme whenever render() runs; locate the corner initialization in the
constructor and add the background update inside the render() method (which is
invoked by setThemeMode()) to keep styling in sync.
---
Nitpick comments:
In `@docs/tasks/active/20260323-docs-frontend-integration-todo.md`:
- Around line 1-25: Add a companion lessons file named
20260323-docs-frontend-integration-lessons.md alongside the existing
20260323-docs-frontend-integration-todo.md; in that file summarize post-work
learnings (e.g., the Yorkie Tree re-export workaround, route naming/proxy
conflicts, verification notes such as the frontend test failure), reference the
spec docs/design/docs-frontend-integration.md, and follow the same
header/metadata conventions used by other paired task files in docs/tasks/active
so the two files are discoverable and consistent.
In `@packages/backend/prisma/schema.prisma`:
- Line 35: Replace the String-typed "type" field with a Prisma enum to enforce
allowed values and DB-level validation: add an enum (e.g., DocumentType with
members sheet and doc), change the "type" field's type from String to that enum
(e.g., DocumentType) and set its `@default` to the enum member (e.g., sheet), then
run Prisma migrate/generate so Prisma Client types are updated; target the
"type" field and the schema's enums when making the change.
In `@packages/backend/src/api/v1/documents.controller.ts`:
- Around line 34-38: The current controller accepts `@Body`() body: { title:
string; type?: string } and maps body.type === 'doc' ? 'doc' : 'sheet' which
silently coerces any invalid string to 'sheet'; update this by either (A)
replacing the loose inline type with a dedicated DTO (e.g., DocumentCreateDto)
that declares type: 'doc' | 'sheet' and add class-validator decorators to
enforce allowed values before calling documentService.createDocument, or (B) at
minimum add a clarifying comment next to the mapping (in the create handler)
explaining the intentional default-to-'sheet' behavior and consider narrowing
the parameter type to type?: 'doc' | 'sheet' so invalid values are flagged by
TypeScript; reference body.type, the controller create handler, and
documentService.createDocument when making the change.
In `@packages/frontend/src/api/workspaces.ts`:
- Around line 227-238: createWorkspaceDocument lacks an explicit return type
which reduces type safety; update the function signature for
createWorkspaceDocument to include an explicit Promise-return type (e.g.,
Promise<Document> or the project-specific document type you use) and ensure that
type is imported or defined where necessary so the returned res.json() result
matches the annotated type.
In `@packages/frontend/src/app/docs/docs-detail.tsx`:
- Around line 106-113: The rename handler handleRenameDocument should catch
errors from renameDocument and avoid invalidating queries on failure; update
handleRenameDocument to wrap the async call in try/catch, call
renameDocument(documentId, newTitle) inside try, run
queryClient.invalidateQueries(...) only on success, and in catch call an error
reporting UI/notification or processLogger (e.g., show toast or set an error
state) so the user gets feedback; ensure the function still rethrows or returns
a boolean if calling code depends on success/failure.
- Around line 148-150: The component DocsDetail reads the route param id (const
{ id } = useParams()) but later uses id! (on the value at the render/loader
location around the current id usage on line 185); add an explicit guard early
in DocsDetail that checks if id is undefined and immediately returns a clear
error UI (or redirects) so the rest of the component can assume a present id,
and then remove the non-null assertion (id!) wherever used (e.g., the usage
around line 185) so code is safe and types align.
In `@packages/frontend/src/app/docs/docs-formatting-toolbar.tsx`:
- Around line 117-171: The Toggle buttons in docs-formatting-toolbar.tsx must
reflect the editor selection state: add the pressed prop to each Toggle (the
ones using toggleBold, toggleItalic, toggleUnderline, toggleStrikethrough) and
set it from the editor's active state (e.g., editor.isActive('bold'),
editor.isActive('italic'), 'underline', 'strike' or equivalent command names
used by your editor instance). Ensure the toolbar component subscribes to editor
selection/transaction updates (or uses a reactive hook from your editor wrapper)
so the component re-renders when selection/style state changes, keeping the
pressed props in sync with the current selection.
In `@packages/frontend/src/app/docs/docs-view.tsx`:
- Around line 55-57: The prop onEditorReady in DocsViewProps should allow null
to avoid unsafe casting; change its type to onEditorReady?: (editor: EditorAPI |
null) => void and update any calls (including the cleanup that currently uses
null as unknown as EditorAPI) to pass null directly; search for DocsViewProps
and the onEditorReady invocations (including the cleanup block around the editor
teardown) and remove the null cast so consumers receive EditorAPI | null safely.
In `@packages/frontend/src/app/docs/yorkie-doc-store.ts`:
- Around line 292-303: insertBlock currently calls getDocument() after
doc.update which forces a rebuild; instead update the in-memory cache directly
like updateBlock: ensure this.cachedDoc exists (initialize from
this.getDocument() only if null), then do this.cachedDoc.blocks.splice(index, 0,
block) and assign this.cachedDoc (or keep same ref if intended), remove the
local currentDoc variable and the extra getDocument() call, and keep setting
this.dirty = false; reference: insertBlock, updateBlock, this.cachedDoc,
doc.update, buildBlockNode, blocks.
In `@packages/frontend/src/app/documents/document-list.tsx`:
- Around line 295-323: Extract the duplicated "New" dropdown into a reusable
component (e.g., NewDocumentDropdown) and replace both occurrences with it;
implement NewDocumentDropdown to accept onCreateSheet and onCreateDoc callbacks
(and an optional buttonSize prop) and internally render the DropdownMenu,
DropdownMenuTrigger, DropdownMenuContent, and two DropdownMenuItem entries with
the Sheet and FileText icons, then update the usages that currently call
createDocumentMutation.mutate(...) to pass wrapper callbacks like () =>
createDocumentMutation.mutate({ title: "New Sheet" }) and () =>
createDocumentMutation.mutate({ title: "New Document", type: "doc" }) so
behavior remains identical.
In `@packages/frontend/src/app/shared/shared-document.tsx`:
- Around line 194-202: Change the generic type passed to DocumentProvider from
YorkieDocsRoot to Partial<YorkieDocsRoot> so the provided initialRoot={{}}
matches the type; update the DocumentProvider<...> usage in the component (the
instance surrounding initialRoot and presence props that renders
SharedDocsLayout) to DocumentProvider<Partial<YorkieDocsRoot>> to align with the
pattern used in docs-detail.tsx and avoid the required content: Tree property
mismatch.
In `@packages/frontend/vite.config.ts`:
- Around line 60-80: Add a manualChunks rule to split the `@wafflebase/docs`
package into its own chunk: detect normalizedId.includes("@wafflebase/docs") (or
the package path under /packages/docs/) in the same manualChunks function that
currently returns "sheet-formula-parser", "sheet-formula-eval", and
"sheet-core", and return a new chunk name like "docs-core" so docs code is
bundled separately for better caching and parallel loading.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 44061889-e0f9-42bc-9b9f-da9711b3d585
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (37)
docs/design/README.mddocs/design/docs-frontend-integration.mddocs/tasks/README.mddocs/tasks/active/20260323-docs-frontend-integration-lessons.mddocs/tasks/active/20260323-docs-frontend-integration-todo.mddocs/tasks/active/20260323-yorkie-doc-store-todo.mddocs/tasks/archive/2026/03/20260322-docs-collaboration-todo.mddocs/tasks/archive/README.mdharness.config.jsonpackage.jsonpackages/backend/package.jsonpackages/backend/prisma/migrations/20260323091757_add_document_type_field/migration.sqlpackages/backend/prisma/schema.prismapackages/backend/src/api/v1/documents.controller.tspackages/backend/src/document/document.controller.tspackages/backend/src/share-link/share-link.controller.tspackages/docs/src/index.tspackages/docs/src/view/editor.tspackages/docs/src/view/ruler.tspackages/docs/src/view/theme.tspackages/frontend/package.jsonpackages/frontend/src/App.tsxpackages/frontend/src/PrivateRoute.tsxpackages/frontend/src/api/documents.tspackages/frontend/src/api/share-links.tspackages/frontend/src/api/workspaces.tspackages/frontend/src/app/docs/docs-detail.tsxpackages/frontend/src/app/docs/docs-formatting-toolbar.tsxpackages/frontend/src/app/docs/docs-view.tsxpackages/frontend/src/app/docs/yorkie-doc-store.tspackages/frontend/src/app/documents/document-list.tsxpackages/frontend/src/app/shared/shared-document.tsxpackages/frontend/src/types/docs-document.tspackages/frontend/src/types/documents.tspackages/frontend/tests/app/docs/yorkie-doc-store.test.tspackages/frontend/tests/resolve-hooks.mjspackages/frontend/vite.config.ts
| getSelectionStyle: (): Partial<InlineStyle> => { | ||
| const block = doc.document.blocks.find( | ||
| (b) => b.id === cursor.position.blockId, | ||
| ); | ||
| if (!block) return {}; | ||
| let pos = 0; | ||
| for (const inline of block.inlines) { | ||
| const inlineEnd = pos + inline.text.length; | ||
| if (cursor.position.offset <= inlineEnd) { | ||
| return { ...inline.style }; | ||
| } | ||
| pos = inlineEnd; | ||
| } | ||
| // Fallback: return style of last inline | ||
| const last = block.inlines[block.inlines.length - 1]; | ||
| return last ? { ...last.style } : {}; | ||
| }, |
There was a problem hiding this comment.
Empty object return can cause incorrect toggle behavior in toolbar.
When getSelectionStyle() returns {} (no block found), the toolbar code in docs-formatting-toolbar.tsx negates undefined values: !current.bold becomes !undefined which is true. This causes the first toggle to always enable the style rather than reflecting the actual state.
Consider returning a defined default state or documenting that consumers must handle undefined properties.
🛡️ Suggested defensive default
getSelectionStyle: (): Partial<InlineStyle> => {
const block = doc.document.blocks.find(
(b) => b.id === cursor.position.blockId,
);
- if (!block) return {};
+ if (!block) return { bold: false, italic: false, underline: false, strikethrough: false };
let pos = 0;Alternatively, the toolbar should guard:
const current = editor.getSelectionStyle();
editor.applyStyle({ bold: !(current.bold ?? false) });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/docs/src/view/editor.ts` around lines 355 - 371, getSelectionStyle
currently returns an empty object when no block is found which lets callers like
docs-formatting-toolbar.tsx treat undefined style properties as true when
toggling; change getSelectionStyle to always return a fully-defined InlineStyle
default (e.g., boolean fields like bold/italic/underline set to false and any
other expected properties) instead of {} so consumers receive concrete false
values, and keep the existing fallback that clones the last inline's style
(function name: getSelectionStyle; referenced by docs-formatting-toolbar.tsx).
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
docs/tasks/archive/2026/03/20260323-yorkie-doc-store-todo.md (4)
840-846: Consider expanding Design Notes with serialization patterns.The Design Notes section effectively documents key implementation decisions. Consider adding one more note about boolean attribute serialization:
- **Boolean attribute serialization**: Yorkie Tree attributes are stored as strings. Boolean style properties (bold, italic, etc.) are serialized as the string `'true'` when true, and omitted when false. This requires string comparison during deserialization (e.g., `attrs.bold === 'true'`).This would complement the existing note about numeric serialization and provide complete documentation of the attribute conversion patterns.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/tasks/archive/2026/03/20260323-yorkie-doc-store-todo.md` around lines 840 - 846, Add a Design Notes bullet documenting boolean attribute serialization: state that Yorkie Tree node attributes are strings, boolean style properties (e.g., bold, italic) are serialized as the string 'true' when true and omitted when false, and deserialization must check string equality (for example using attrs.bold === 'true'); place this note alongside the existing numeric serialization note in the Design Notes section to complete attribute conversion documentation.
726-726: Block ID generation may have collision risk.Using
Date.now()for block ID generation (line 726:id: \block-${Date.now()}-0``) can cause collisions if multiple blocks are created within the same millisecond, especially during programmatic initialization or batch operations.Consider using a more robust ID generation strategy like
crypto.randomUUID()or thegenerateBlockId()utility that's already imported in the tests (line 439).Alternative using existing utility
+import { generateBlockId } from '@wafflebase/document'; + function initialDocsRoot(): YorkieDocsRoot { return { content: new yorkie.Tree({ type: 'doc', children: [ { type: 'block', attributes: { - id: `block-${Date.now()}-0`, + id: generateBlockId(), type: 'paragraph',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/tasks/archive/2026/03/20260323-yorkie-doc-store-todo.md` at line 726, Replace the fragile timestamp-based block ID (`id: \`block-${Date.now()}-0\``) with a robust ID generator; locate the block creation that sets id using Date.now() and use the existing generateBlockId() utility (already referenced in tests) or crypto.randomUUID() to eliminate millisecond-collision risk, ensuring the new ID format matches other consumers of block IDs.
354-354: Consider throwing an error for missing block IDs.Line 354 falls back to an empty string when a block's
idattribute is missing:id: attrs.id ?? ''. This could hide data integrity issues. Since every block should have a valid ID, consider throwing an error instead:- return { id: attrs.id ?? '', type: 'paragraph', inlines, style }; + if (!attrs.id) { + throw new Error('Block node missing required id attribute'); + } + return { id: attrs.id, type: 'paragraph', inlines, style };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/tasks/archive/2026/03/20260323-yorkie-doc-store-todo.md` at line 354, The code currently returns a block with a fallback empty id using "id: attrs.id ?? ''", which can hide missing-ID bugs; update the function that constructs this block to validate attrs.id and throw a descriptive error (e.g., throw new Error('Missing block id') or use an assert) when attrs.id is null/undefined instead of defaulting to ''. Replace the fallback expression with a runtime check before returning the object so every block always has a valid id, and adjust any callers/tests to expect the thrown error where appropriate.
423-627: Consider adding edge case tests.The test suite provides good coverage of happy paths. Consider adding tests for error conditions and edge cases:
- Error handling: Test that
updateBlock()anddeleteBlock()throw errors for non-existent block IDs (mentioned in line 526-528, but could be expanded)- Concurrent modifications: Test behavior when tree is modified externally between operations
- Empty inline handling: Test that empty inline arrays are handled correctly (line 350-352 adds default empty inline)
- pageSetup defaults: Test
getPageSetup()when no pageSetup was ever set (should return defaults fromresolvePageSetup)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/tasks/archive/2026/03/20260323-yorkie-doc-store-todo.md` around lines 423 - 627, Add focused unit tests in packages/frontend/src/app/docs/yorkie-doc-store.test.ts that cover the suggested edge cases: (1) assert YorkieDocStore.updateBlock and deleteBlock throw for non-existent IDs (use updateBlock and deleteBlock directly and expect exceptions), (2) simulate concurrent modifications by calling the underlying yorkie.Document.update between store operations and verify YorkieDocStore reflects external tree changes, (3) verify handling of empty inlines by setting a block with an empty inlines array and ensuring getDocument/getBlock returns an empty array (and that insert/update preserve empty inlines), and (4) assert getPageSetup returns the defaults from resolvePageSetup when no pageSetup was set (call getPageSetup on a fresh YorkieDocStore and compare to resolvePageSetup()). Ensure tests reference the YorkieDocStore instance and functions updateBlock, deleteBlock, getPageSetup, and resolvePageSetup so they run deterministically in-memory.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/tasks/archive/2026/03/20260323-yorkie-doc-store-todo.md`:
- Around line 277-289: The undo()/redo() implementation currently captures state
via getDocument() and restores via setDocument(), which performs a full tree
replacement (see undo(), redo(), getDocument(), setDocument(), and
undoStack/redoStack) and causes expensive Yorkie operations; update the Design
Notes section to document this performance limitation and recommended
mitigations: suggest storing native Yorkie document snapshots instead of
serialized Documents, evaluate using Yorkie’s built-in undo/redo if available,
or switch to applying minimal diffs rather than full replacements, and mention
this is acceptable for Phase 1 but should be revisited for performance
improvements.
- Around line 718-745: initialDocsRoot() currently returns a plain object cast
with as any for the content field; replace that with an actual Yorkie Tree
instance so YorkieDocsRoot.content is a proper Tree. In the initialDocsRoot
function, construct a new Tree (using the Yorkie Tree constructor used in your
codebase) populated with the same root document structure instead of the object
literal, ensuring the content property is typed as Tree and removing the as any
cast; update any imports to reference the Tree class and use Tree
methods/constructor to initialize the children/inline/text nodes so the returned
YorkieDocsRoot contains a real Tree instance.
---
Nitpick comments:
In `@docs/tasks/archive/2026/03/20260323-yorkie-doc-store-todo.md`:
- Around line 840-846: Add a Design Notes bullet documenting boolean attribute
serialization: state that Yorkie Tree node attributes are strings, boolean style
properties (e.g., bold, italic) are serialized as the string 'true' when true
and omitted when false, and deserialization must check string equality (for
example using attrs.bold === 'true'); place this note alongside the existing
numeric serialization note in the Design Notes section to complete attribute
conversion documentation.
- Line 726: Replace the fragile timestamp-based block ID (`id:
\`block-${Date.now()}-0\``) with a robust ID generator; locate the block
creation that sets id using Date.now() and use the existing generateBlockId()
utility (already referenced in tests) or crypto.randomUUID() to eliminate
millisecond-collision risk, ensuring the new ID format matches other consumers
of block IDs.
- Line 354: The code currently returns a block with a fallback empty id using
"id: attrs.id ?? ''", which can hide missing-ID bugs; update the function that
constructs this block to validate attrs.id and throw a descriptive error (e.g.,
throw new Error('Missing block id') or use an assert) when attrs.id is
null/undefined instead of defaulting to ''. Replace the fallback expression with
a runtime check before returning the object so every block always has a valid
id, and adjust any callers/tests to expect the thrown error where appropriate.
- Around line 423-627: Add focused unit tests in
packages/frontend/src/app/docs/yorkie-doc-store.test.ts that cover the suggested
edge cases: (1) assert YorkieDocStore.updateBlock and deleteBlock throw for
non-existent IDs (use updateBlock and deleteBlock directly and expect
exceptions), (2) simulate concurrent modifications by calling the underlying
yorkie.Document.update between store operations and verify YorkieDocStore
reflects external tree changes, (3) verify handling of empty inlines by setting
a block with an empty inlines array and ensuring getDocument/getBlock returns an
empty array (and that insert/update preserve empty inlines), and (4) assert
getPageSetup returns the defaults from resolvePageSetup when no pageSetup was
set (call getPageSetup on a fresh YorkieDocStore and compare to
resolvePageSetup()). Ensure tests reference the YorkieDocStore instance and
functions updateBlock, deleteBlock, getPageSetup, and resolvePageSetup so they
run deterministically in-memory.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 567f8db2-e886-4540-accc-851572bc1f63
📒 Files selected for processing (7)
docs/design/README.mddocs/tasks/README.mddocs/tasks/archive/2026/03/20260323-docs-frontend-integration-lessons.mddocs/tasks/archive/2026/03/20260323-docs-frontend-integration-todo.mddocs/tasks/archive/2026/03/20260323-yorkie-doc-store-todo.mddocs/tasks/archive/README.mdscripts/verify-self.mjs
✅ Files skipped from review due to trivial changes (5)
- docs/tasks/archive/2026/03/20260323-docs-frontend-integration-lessons.md
- docs/design/README.md
- docs/tasks/README.md
- docs/tasks/archive/2026/03/20260323-docs-frontend-integration-todo.md
- docs/tasks/archive/README.md
| undo(): void { | ||
| if (!this.canUndo()) return; | ||
| this.redoStack.push(this.getDocument()); | ||
| const prev = this.undoStack.pop()!; | ||
| this.setDocument(prev); | ||
| } | ||
|
|
||
| redo(): void { | ||
| if (!this.canRedo()) return; | ||
| this.undoStack.push(this.getDocument()); | ||
| const next = this.redoStack.pop()!; | ||
| this.setDocument(next); | ||
| } |
There was a problem hiding this comment.
Undo/redo triggers expensive full tree replacement.
Both undo() and redo() call getDocument() to capture current state, then setDocument() to restore previous state. The setDocument() method (lines 168-193) performs a full tree replacement: it deletes all existing blocks and inserts the new ones. This is expensive and generates Yorkie operations for every undo/redo action.
For better performance, consider:
- Storing Yorkie document snapshots directly rather than serialized Documents
- Using Yorkie's native undo/redo if available
- Applying minimal diffs rather than full replacement
The current approach is acceptable for Phase 1 (as noted in line 844), but this performance concern should be documented in the Design Notes section.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/tasks/archive/2026/03/20260323-yorkie-doc-store-todo.md` around lines
277 - 289, The undo()/redo() implementation currently captures state via
getDocument() and restores via setDocument(), which performs a full tree
replacement (see undo(), redo(), getDocument(), setDocument(), and
undoStack/redoStack) and causes expensive Yorkie operations; update the Design
Notes section to document this performance limitation and recommended
mitigations: suggest storing native Yorkie document snapshots instead of
serialized Documents, evaluate using Yorkie’s built-in undo/redo if available,
or switch to applying minimal diffs rather than full replacements, and mention
this is acceptable for Phase 1 but should be revisited for performance
improvements.
- Add type field to share-link resolve E2E assertion - Remove duplicate @yorkie-js/react from root package.json - Add bounds check to YorkieDocStore.deleteBlockByIndex - Add virtual stub fallback for @wafflebase/docs in test hooks - Fix onEditorReady callback type to accept null - Remove dead findFocusedPage method and unused vars in Ruler - Add TODO for readOnly in SharedDocsLayout - Update design doc non-goals to reflect delivered scope Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (7)
packages/backend/test/authenticated-http.e2e-spec.ts (1)
145-150: Add a complementary resolve assertion fortype: 'doc'.This verifies the new
typefield for the default"sheet"path, but it doesn’t protect the"doc"share-link resolve path introduced in this PR. Please add a second case creating a doc-typed document and assertingtype: 'doc'in/share-links/:token/resolve.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/backend/test/authenticated-http.e2e-spec.ts` around lines 145 - 150, Add a second test case that mirrors the existing resolve assertion but creates a document with type 'doc' and a share-link for that document, then call the same /share-links/:token/resolve flow and assert the returned body includes type: 'doc' along with documentId, role, and title; locate the existing assertion using resolveResponse.body and doc in the test and duplicate the setup/resolve/assert steps for a doc-typed document to validate the "doc" resolve path.docs/design/docs-frontend-integration.md (2)
184-196: Consider adding the migration file to the file map.The file map lists the Prisma schema file but not the migration file. While migrations are auto-generated, explicitly listing it could make the implementation plan clearer.
📋 Suggested addition
Add a row after the schema.prisma entry:
| File | Action | Description | |------|--------|-------------| | `packages/backend/prisma/schema.prisma` | Modify | Add `type` field to Document | +| `packages/backend/prisma/migrations/...` | Create | Generate migration for `type` field | | `packages/backend/src/document/document.service.ts` | Modify | Pass `type` to Prisma create |🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/design/docs-frontend-integration.md` around lines 184 - 196, Update the "File Map" table to include the generated Prisma migration file alongside the `packages/backend/prisma/schema.prisma` entry: add a row (immediately after the `schema.prisma` row) that names the migration (e.g., `packages/backend/prisma/migrations/<timestamp>_add_document_type/migration.sql` or the migration folder), Action "Add" or "Create", and Description "Add `type` field to Document (Prisma migration)"; this makes the plan explicit and ties the migration to the schema change referenced in the File Map.
136-140: Add language specifiers to fenced code blocks.Two fenced code blocks are missing language specifiers, which can affect rendering and violates markdown best practices.
📝 Proposed fix
For the dropdown menu visual (lines 136-140):
-``` +```text [+ New ▾] ├── New Sheet └── New DocumentFor the implementation phases (lines 199-213): ```diff -``` +```text Phase 1: Backend (type field) 1. Prisma schema + migration 2. Document service + controller updatesAlso applies to: 199-213
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/design/docs-frontend-integration.md` around lines 136 - 140, The two fenced code blocks showing the dropdown menu visual ("[+ New ▾] ├── New Sheet └── New Document") and the implementation phases ("Phase 1: Backend (type field) 1. Prisma schema + migration 2. Document service + controller updates") are missing language specifiers; update each triple-backtick fence to include a language (use "text") so the blocks become ```text ... ``` to ensure correct rendering and compliance with markdown best practices.packages/frontend/src/app/docs/yorkie-doc-store.ts (4)
46-57: Potential NaN propagation inparseInlineStyle.
Number(attrs.fontSize)returnsNaNif the attribute is an empty string or invalid. UnlikeparseBlockStylewhich usesnormalizeBlockStyleto provide defaults,parseInlineStylepasses through the raw parsed value.Consider adding a guard:
🛡️ Suggested fix
- if ('fontSize' in attrs) style.fontSize = Number(attrs.fontSize); + if ('fontSize' in attrs) { + const parsed = Number(attrs.fontSize); + if (!Number.isNaN(parsed)) style.fontSize = parsed; + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/docs/yorkie-doc-store.ts` around lines 46 - 57, parseInlineStyle currently sets fontSize using Number(attrs.fontSize) which can produce NaN for empty/invalid values; update parseInlineStyle to parse attrs.fontSize (e.g., with parseFloat or Number) and only assign style.fontSize when the parsed value is a finite number (use Number.isFinite or !Number.isNaN) so NaN is not propagated into InlineStyle; keep all other attribute handling unchanged and ensure fontSize falls back to undefined (or omits the property) when parsing fails.
166-179: Potential NaN values if sub-objects are malformed.If
proxy.paperSizeorproxy.marginsare undefined or missing properties,Number(undefined)returnsNaN. WhileresolvePageSetupat the call site may normalize these, the function itself could produce invalid values.Consider adding fallbacks:
🛡️ Suggested fix
function readPageSetup(proxy: any): PageSetup { const ps = proxy.paperSize; const m = proxy.margins; return { - paperSize: { name: ps?.name, width: Number(ps?.width), height: Number(ps?.height) }, + paperSize: { name: ps?.name, width: Number(ps?.width) || 0, height: Number(ps?.height) || 0 }, orientation: proxy.orientation ?? 'portrait', margins: { - top: Number(m?.top), - bottom: Number(m?.bottom), - left: Number(m?.left), - right: Number(m?.right), + top: Number(m?.top) || 0, + bottom: Number(m?.bottom) || 0, + left: Number(m?.left) || 0, + right: Number(m?.right) || 0, }, }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/docs/yorkie-doc-store.ts` around lines 166 - 179, readPageSetup currently calls Number(...) on potentially undefined sub-properties (proxy.paperSize and proxy.margins) which yields NaN; update readPageSetup to defensively handle missing/malformed sub-objects by providing safe fallbacks for paperSize (name defaulting to empty string, width/height defaulting to 0) and margins (top/bottom/left/right defaulting to 0) before coercion, e.g., use ps?.width ?? 0 and m?.top ?? 0 patterns when building the PageSetup return so all numeric fields are always valid numbers; keep the existing orientation defaulting behavior.
292-303: Consider adding bounds validation toinsertBlock.Unlike
deleteBlockByIndexwhich now validates bounds,insertBlockdoesn't check thatindexis within valid range[0, blocks.length]. While Yorkie may handle invalid indices gracefully, explicit validation would provide clearer error messages and consistent behavior with other methods.🛡️ Suggested fix
insertBlock(index: number, block: Block): void { + const currentDoc = this.getDocument(); + if (index < 0 || index > currentDoc.blocks.length) { + throw new Error(`Block index out of bounds: ${index}`); + } this.doc.update((root) => { const tree = root.content; if (!tree || typeof tree.getRootTreeNode !== 'function') return; tree.editByPath([index], [index], buildBlockNode(block)); }); // Update cache in-place - const currentDoc = this.getDocument(); currentDoc.blocks.splice(index, 0, block); this.cachedDoc = currentDoc; this.dirty = false; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/docs/yorkie-doc-store.ts` around lines 292 - 303, insertBlock lacks bounds checking for the index; before mutating the doc call this.getDocument() to read currentDoc.blocks.length and validate index is between 0 and currentDoc.blocks.length (inclusive), and if out of range throw a RangeError with a clear message (consistent with deleteBlockByIndex behavior). Perform this validation before calling this.doc.update and before splicing the cachedDoc so you fail fast and keep behavior consistent; leave the existing calls to buildBlockNode(block), this.doc.update, and the in-place cache splice unchanged otherwise.
190-192: Consider bounding undo/redo stack size.The undo and redo stacks grow unbounded, which could cause memory pressure during long editing sessions with large documents. Since the comment notes this is "Phase 1," consider adding a maximum stack size.
🛠️ Suggested approach
+ private static readonly MAX_UNDO_STACK_SIZE = 100; + // Local snapshot-based undo/redo (Phase 1) private undoStack: Document[] = []; private redoStack: Document[] = [];snapshot(): void { const current = this.getDocument(); this.undoStack.push(cloneDocument(current)); + // Limit stack size to prevent unbounded memory growth + if (this.undoStack.length > YorkieDocStore.MAX_UNDO_STACK_SIZE) { + this.undoStack.shift(); + } this.redoStack = []; }Also applies to: 348-352
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/docs/yorkie-doc-store.ts` around lines 190 - 192, The undoStack and redoStack are unbounded; add a MAX_STACK_SIZE constant (e.g., 50 or configurable) in the yorkie-doc-store class and enforce it whenever pushing onto undoStack or redoStack (truncate oldest entries or shift when length > MAX_STACK_SIZE). Update the push logic used by methods that save snapshots (referencing undoStack and redoStack) and also apply the same trimming when moving entries between stacks (the code around the current snapshot push/restore logic that manipulates undoStack/redoStack). Make MAX_STACK_SIZE configurable via constructor or a class-level constant and ensure both stacks are trimmed consistently to prevent unbounded memory growth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/design/docs-frontend-integration.md`:
- Line 56: The frontend Document type has a type mismatch: backend Prisma
defines id as a UUID string (id String `@default`(uuid())), but the frontend's
Document type declares id: number; — update the Document type's id field to be a
string to match the backend (locate the Document type definition and change its
id property from number to string so API responses with UUIDs type-check
correctly).
In `@packages/docs/src/view/ruler.ts`:
- Line 85: The corner background color is being captured once in the constructor
via marginBg(), so it won't update on theme changes; change the code that builds
the corner style (the line that currently sets
`z-index:3;background:${marginBg()};margin-bottom:${-RULER_SIZE}px;`) to compute
marginBg() dynamically whenever the corner is rendered or when the theme change
handler runs (e.g., call marginBg() inside the paint/update method or reapply
the style in the theme-change callback), ensuring RULER_SIZE is still used for
margin-bottom and the background uses the fresh marginBg() value instead of the
one-time resolved value.
In `@packages/frontend/tests/resolve-hooks.mjs`:
- Around line 164-181: The virtual module "virtual:wafflebase-docs" in the
resolve-hooks.mjs stub is missing the initialize export used by docs-view.tsx;
add an exported initialize function (export function initialize(container,
store, theme) { /* no-op or minimal stub */ }) to the source array so the module
exports DEFAULT_BLOCK_STYLE, createEmptyBlock, resolvePageSetup, etc., plus
initialize, matching the signature used by initialize(container, store, theme)
in packages/frontend/src/app/docs/docs-view.tsx.
---
Nitpick comments:
In `@docs/design/docs-frontend-integration.md`:
- Around line 184-196: Update the "File Map" table to include the generated
Prisma migration file alongside the `packages/backend/prisma/schema.prisma`
entry: add a row (immediately after the `schema.prisma` row) that names the
migration (e.g.,
`packages/backend/prisma/migrations/<timestamp>_add_document_type/migration.sql`
or the migration folder), Action "Add" or "Create", and Description "Add `type`
field to Document (Prisma migration)"; this makes the plan explicit and ties the
migration to the schema change referenced in the File Map.
- Around line 136-140: The two fenced code blocks showing the dropdown menu
visual ("[+ New ▾] ├── New Sheet └── New Document") and the implementation
phases ("Phase 1: Backend (type field) 1. Prisma schema + migration 2. Document
service + controller updates") are missing language specifiers; update each
triple-backtick fence to include a language (use "text") so the blocks become
```text ... ``` to ensure correct rendering and compliance with markdown best
practices.
In `@packages/backend/test/authenticated-http.e2e-spec.ts`:
- Around line 145-150: Add a second test case that mirrors the existing resolve
assertion but creates a document with type 'doc' and a share-link for that
document, then call the same /share-links/:token/resolve flow and assert the
returned body includes type: 'doc' along with documentId, role, and title;
locate the existing assertion using resolveResponse.body and doc in the test and
duplicate the setup/resolve/assert steps for a doc-typed document to validate
the "doc" resolve path.
In `@packages/frontend/src/app/docs/yorkie-doc-store.ts`:
- Around line 46-57: parseInlineStyle currently sets fontSize using
Number(attrs.fontSize) which can produce NaN for empty/invalid values; update
parseInlineStyle to parse attrs.fontSize (e.g., with parseFloat or Number) and
only assign style.fontSize when the parsed value is a finite number (use
Number.isFinite or !Number.isNaN) so NaN is not propagated into InlineStyle;
keep all other attribute handling unchanged and ensure fontSize falls back to
undefined (or omits the property) when parsing fails.
- Around line 166-179: readPageSetup currently calls Number(...) on potentially
undefined sub-properties (proxy.paperSize and proxy.margins) which yields NaN;
update readPageSetup to defensively handle missing/malformed sub-objects by
providing safe fallbacks for paperSize (name defaulting to empty string,
width/height defaulting to 0) and margins (top/bottom/left/right defaulting to
0) before coercion, e.g., use ps?.width ?? 0 and m?.top ?? 0 patterns when
building the PageSetup return so all numeric fields are always valid numbers;
keep the existing orientation defaulting behavior.
- Around line 292-303: insertBlock lacks bounds checking for the index; before
mutating the doc call this.getDocument() to read currentDoc.blocks.length and
validate index is between 0 and currentDoc.blocks.length (inclusive), and if out
of range throw a RangeError with a clear message (consistent with
deleteBlockByIndex behavior). Perform this validation before calling
this.doc.update and before splicing the cachedDoc so you fail fast and keep
behavior consistent; leave the existing calls to buildBlockNode(block),
this.doc.update, and the in-place cache splice unchanged otherwise.
- Around line 190-192: The undoStack and redoStack are unbounded; add a
MAX_STACK_SIZE constant (e.g., 50 or configurable) in the yorkie-doc-store class
and enforce it whenever pushing onto undoStack or redoStack (truncate oldest
entries or shift when length > MAX_STACK_SIZE). Update the push logic used by
methods that save snapshots (referencing undoStack and redoStack) and also apply
the same trimming when moving entries between stacks (the code around the
current snapshot push/restore logic that manipulates undoStack/redoStack). Make
MAX_STACK_SIZE configurable via constructor or a class-level constant and ensure
both stacks are trimmed consistently to prevent unbounded memory growth.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: eb896a10-23a4-4a02-b383-deffbac0e7a1
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (7)
docs/design/docs-frontend-integration.mdpackages/backend/test/authenticated-http.e2e-spec.tspackages/docs/src/view/ruler.tspackages/frontend/src/app/docs/docs-view.tsxpackages/frontend/src/app/docs/yorkie-doc-store.tspackages/frontend/src/app/shared/shared-document.tsxpackages/frontend/tests/resolve-hooks.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/frontend/src/app/docs/docs-view.tsx
- packages/frontend/src/app/shared/shared-document.tsx
The Prisma schema defines Document.id as String @default(uuid()), but the frontend type had it as number. This mismatch would cause type errors when receiving API responses with UUID strings. Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/design/docs-frontend-integration.md`:
- Around line 136-140: The markdown has fenced code blocks without language tags
(MD040); update the two blocks that start with the snippets "[+ New ▾] ..." and
"Phase 1: Backend (type field) ..." by adding a language identifier (e.g.,
```text) at the opening fence so both blocks become ```text ... ```; locate the
blocks in docs/design/docs-frontend-integration.md by searching for the exact
block contents and add the language tag to each opening ``` fence to satisfy
markdownlint.
- Around line 44-46: Update the "Current State" wording so it no longer states
that the Document model lacks a `type` field; rephrase the sentence that
mentions "The Document model has no `type` field" to present it as historical
context (e.g., "Before this change, the Document model did not include
`Document.type`"), and ensure any subsequent text refers to the implemented
`Document.type` field when describing current behavior so readers aren't misled.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 44c5428b-8f42-437c-8c19-d0ccf9d9e471
📒 Files selected for processing (2)
docs/design/docs-frontend-integration.mdpackages/frontend/src/types/documents.ts
✅ Files skipped from review due to trivial changes (1)
- packages/frontend/src/types/documents.ts
- Add missing docs exports to virtual stub in resolve-hooks.mjs - Fix ruler corner background not updating on theme change - Add language identifiers to fenced code blocks in design doc - Remove task-tracking sections from design doc (status table, implementation order) and rewrite as architecture overview Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/frontend/tests/resolve-hooks.mjs`:
- Around line 113-125: The resolver rewrites .js imports to .ts but only checks
a single tsCandidate (specifier.replace(/\.js$/, ".ts")) so it misses .tsx
sources; update the rewrite logic in the block that computes tsCandidate to also
probe a tsxCandidate (use pathResolve(parentDir, specifier.replace(/\.js$/,
".tsx")) and existsSync on it) and if found call
nextResolve(pathToFileURL(tsxCandidate).href, context); alternatively, replace
that single .ts check with a small loop over [".ts", ".tsx"] mirroring the later
loop to ensure specifier -> .tsx is discovered when present (refer to symbols
specifier, tsCandidate, parentDir, pathResolve, existsSync, pathToFileURL,
nextResolve).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bea97b7f-4794-4384-82da-31ed40424a41
📒 Files selected for processing (4)
docs/design/docs-frontend-integration.mddocs/tasks/active/20260324-pr72-review-fixes-todo.mdpackages/docs/src/view/ruler.tspackages/frontend/tests/resolve-hooks.mjs
✅ Files skipped from review due to trivial changes (2)
- docs/tasks/active/20260324-pr72-review-fixes-todo.md
- docs/design/docs-frontend-integration.md
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/docs/src/view/ruler.ts
| // Remap .js imports to .ts (TypeScript sources use .js extensions for ESM) | ||
| if (specifier.endsWith(".js")) { | ||
| const tsCandidate = pathResolve(parentDir, specifier.replace(/\.js$/, ".ts")); | ||
| if (existsSync(tsCandidate)) { | ||
| return nextResolve(pathToFileURL(tsCandidate).href, context); | ||
| } | ||
| } | ||
|
|
||
| for (const ext of [".ts", ".tsx"]) { | ||
| const candidate = pathResolve(parentDir, specifier + ext); | ||
| if (existsSync(candidate)) { | ||
| return nextResolve(pathToFileURL(candidate).href, context); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python - <<'PY'
from pathlib import Path
import re
roots = [
Path("packages/frontend/src"),
Path("packages/sheets/src"),
Path("packages/docs/src"),
]
pattern = re.compile(
r'''(?:from\s*["'](?P<from>\.{1,2}/[^"']+\.js)["']|
import\s*["'](?P<side>\.{1,2}/[^"']+\.js)["']|
import\s*\(\s*["'](?P<dynamic>\.{1,2}/[^"']+\.js)["']\s*\))''',
re.VERBOSE,
)
hits = []
for root in roots:
if not root.exists():
continue
for path in root.rglob("*"):
if path.suffix not in {".ts", ".tsx", ".js", ".mjs"}:
continue
text = path.read_text(encoding="utf-8")
for match in pattern.finditer(text):
spec = next(group for group in match.groups() if group)
tsx_target = (path.parent / spec).resolve().with_suffix(".tsx")
if tsx_target.exists():
hits.append((str(path), spec, str(tsx_target)))
if hits:
print("Found .js imports that point at .tsx files:")
for source_path, spec, target in hits:
print(f"{source_path}: {spec} -> {target}")
else:
print("No .js imports resolving to .tsx files were found.")
PYRepository: wafflebase/wafflebase
Length of output: 115
Also probe .tsx when rewriting .js imports.
Line 114 only checks .ts. While the current codebase doesn't have .js imports targeting .tsx files, the resolver logic has a correctness gap: if a workspace source used ESM-style ./Foo.js for a Foo.tsx module, it would fall through to Foo.js.ts[x] and miss the real file.
Possible fix
// Remap .js imports to .ts (TypeScript sources use .js extensions for ESM)
if (specifier.endsWith(".js")) {
- const tsCandidate = pathResolve(parentDir, specifier.replace(/\.js$/, ".ts"));
- if (existsSync(tsCandidate)) {
- return nextResolve(pathToFileURL(tsCandidate).href, context);
+ for (const mappedExt of [".ts", ".tsx"]) {
+ const candidate = pathResolve(
+ parentDir,
+ specifier.replace(/\.js$/, mappedExt),
+ );
+ if (existsSync(candidate)) {
+ return nextResolve(pathToFileURL(candidate).href, context);
+ }
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/frontend/tests/resolve-hooks.mjs` around lines 113 - 125, The
resolver rewrites .js imports to .ts but only checks a single tsCandidate
(specifier.replace(/\.js$/, ".ts")) so it misses .tsx sources; update the
rewrite logic in the block that computes tsCandidate to also probe a
tsxCandidate (use pathResolve(parentDir, specifier.replace(/\.js$/, ".tsx")) and
existsSync on it) and if found call
nextResolve(pathToFileURL(tsxCandidate).href, context); alternatively, replace
that single .ts check with a small loop over [".ts", ".tsx"] mirroring the later
loop to ensure specifier -> .tsx is discovered when present (refer to symbols
specifier, tsCandidate, parentDir, pathResolve, existsSync, pathToFileURL,
nextResolve).
Summary
typefield to Document model ("sheet"|"doc") with Prisma migration, backend API support, and frontend type/routing updates"sheet"/"doc"/s/:idroute for sheets matching/d/:idfor docs, with colored icons in document list (green for Sheet, blue for Document)@yorkie-js/sdkand@yorkie-js/reactto 0.7.3-alpha@wafflebase/docsESM.js→.tsremappingTest plan
/s/:id/d/:idpnpm verify:fast— all tests pass🤖 Generated with Claude Code
Summary by CodeRabbit