diff --git a/packages/web/src/components/checklist/ChecklistYjsWrapper.tsx b/packages/web/src/components/checklist/ChecklistYjsWrapper.tsx index 133bcb43..d2ca709b 100644 --- a/packages/web/src/components/checklist/ChecklistYjsWrapper.tsx +++ b/packages/web/src/components/checklist/ChecklistYjsWrapper.tsx @@ -11,6 +11,7 @@ import type { ChecklistAnswerInput } from '@corates/shared/sync'; import { useProjectStore, selectConnectionPhase } from '@/stores/projectStore'; import { useAuthStore, selectUser } from '@/stores/authStore'; import { useStudyAnnotations } from '@/primitives/useProject/useStudyAnnotations'; +import { annotationTypeName } from '@/lib/annotationType'; import { ACCESS_DENIED_ERRORS } from '@/constants/errors.js'; import { CHECKLIST_STATUS, @@ -46,7 +47,8 @@ interface ChecklistYjsWrapperProps { /** The EmbedPDF payload shape the viewer hands back for annotation writes. */ interface AnnotationData { id?: string; - type?: string; + /** EmbedPDF's numeric subtype; the synced row stores its name. */ + type?: number | string; pageIndex?: number; pdfId?: string; embedPdfData?: string; @@ -281,7 +283,7 @@ export function ChecklistYjsWrapper({ projectId, studyId, checklistId }: Checkli studyId, checklistId, pdfId: selectedPdfId, - type: annotation.type ?? '', + type: annotationTypeName(annotation.type), pageIndex: annotation.pageIndex ?? 0, // The serialized EmbedPDF payload must carry the row's id. embedPdfData: JSON.stringify({ ...annotation, id }), @@ -298,7 +300,7 @@ export function ChecklistYjsWrapper({ projectId, studyId, checklistId }: Checkli void client?.mutate.annotation.update({ id: annotation.id, updates: { - type: annotation.type, + type: annotationTypeName(annotation.type), pageIndex: annotation.pageIndex, embedPdfData: JSON.stringify(annotation), }, diff --git a/packages/web/src/lib/__tests__/annotationType.test.ts b/packages/web/src/lib/__tests__/annotationType.test.ts new file mode 100644 index 00000000..6c8c2962 --- /dev/null +++ b/packages/web/src/lib/__tests__/annotationType.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import { PdfAnnotationSubtype } from '@embedpdf/models'; +import { annotationTypeName } from '@/lib/annotationType'; +import { syncApp } from '@corates/shared/sync'; + +describe('annotationTypeName', () => { + it('names the numeric subtypes EmbedPDF emits', () => { + expect(annotationTypeName(PdfAnnotationSubtype.HIGHLIGHT)).toBe('highlight'); + expect(annotationTypeName(PdfAnnotationSubtype.INK)).toBe('ink'); + expect(annotationTypeName(999)).toBe('999'); + }); + + it('passes strings through and falls back to empty', () => { + expect(annotationTypeName('highlight')).toBe('highlight'); + expect(annotationTypeName(undefined)).toBe(''); + }); + + it('produces args the annotation mutators accept', () => { + const base = { + id: 'a1', + studyId: 's1', + checklistId: 'c1', + pdfId: 'p1', + type: annotationTypeName(PdfAnnotationSubtype.HIGHLIGHT), + pageIndex: 0, + embedPdfData: '{}', + createdBy: 'u1', + now: 1_700_000_000_000, + }; + + expect(syncApp.mutators['annotation.add'].args.safeParse(base).success).toBe(true); + expect( + syncApp.mutators['annotation.update'].args.safeParse({ + id: base.id, + updates: { type: base.type, pageIndex: 0, embedPdfData: '{}' }, + now: base.now, + }).success, + ).toBe(true); + + // The pre-fix payload: the raw numeric subtype is what the engine rejected. + expect( + syncApp.mutators['annotation.add'].args.safeParse({ + ...base, + type: PdfAnnotationSubtype.HIGHLIGHT, + }).success, + ).toBe(false); + }); +}); diff --git a/packages/web/src/lib/annotationType.ts b/packages/web/src/lib/annotationType.ts new file mode 100644 index 00000000..4112e8ec --- /dev/null +++ b/packages/web/src/lib/annotationType.ts @@ -0,0 +1,13 @@ +import { PdfAnnotationSubtypeName, type PdfAnnotationSubtype } from '@embedpdf/models'; + +/** + * EmbedPDF reports an annotation's subtype as a numeric enum (HIGHLIGHT = 9), + * but the synced `annotations.type` column is a string, so an uncoerced value + * fails the mutator's args schema and the write is rejected outright. + */ +export function annotationTypeName(type: unknown): string { + if (typeof type === 'number') { + return PdfAnnotationSubtypeName[type as PdfAnnotationSubtype] ?? String(type); + } + return typeof type === 'string' ? type : ''; +}