From c40643231bdb89787ecbb60b0ebabfe4c46abea3 Mon Sep 17 00:00:00 2001 From: Jacob Maynard Date: Sat, 18 Apr 2026 10:53:16 -0500 Subject: [PATCH 1/9] add new textref --- .../reconcile-tab/ReconciliationWrapper.tsx | 24 +++++++- .../primitives/useProject/checklists/index.ts | 55 ++++++++++--------- 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx b/packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx index 0a20b824f..49d3a3bb6 100644 --- a/packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx +++ b/packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx @@ -7,6 +7,7 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; import { useNavigate } from '@tanstack/react-router'; import { useProjectContext } from '@/components/project/ProjectContext'; import { connectionPool } from '@/project/ConnectionPool'; +import type { TextRef } from '@/primitives/useProject/checklists'; import { useProjectStore, selectMembers, @@ -452,15 +453,32 @@ export function ReconciliationWrapper({ ], ); - // Set a Y.Text field by key path without direct Y.Text manipulation + // Set a Y.Text field by key path without direct Y.Text manipulation. + // Bridges the legacy loose-params shape used by reconciliation adapters into + // the primitive's typed TextRef. Will be replaced when adapters migrate. const setTextValue = useCallback( (params: { sectionKey?: string; fieldKey?: string; questionKey?: string }, text: string) => { if (!reconciledChecklistId) return; const poolOps = connectionPool.getOps(projectId); if (!poolOps) throw new Error(`No connection for project ${projectId}`); - poolOps.checklist.setTextValue(studyId, reconciledChecklistId, params, text); + const ref: TextRef = isRobinsI + ? { + type: 'ROBINS_I', + sectionKey: params.sectionKey ?? '', + fieldKey: params.fieldKey ?? '', + questionKey: params.questionKey ?? null, + } + : isRob2 + ? { + type: 'ROB2', + sectionKey: params.sectionKey ?? '', + fieldKey: params.fieldKey ?? '', + questionKey: params.questionKey ?? null, + } + : { type: 'AMSTAR2', questionKey: params.questionKey ?? '' }; + poolOps.checklist.setTextValue(studyId, reconciledChecklistId, ref, text); }, - [studyId, reconciledChecklistId, projectId], + [studyId, reconciledChecklistId, projectId, isRobinsI, isRob2], ); // Shared props for all reconciliation types diff --git a/packages/web/src/primitives/useProject/checklists/index.ts b/packages/web/src/primitives/useProject/checklists/index.ts index f61263b87..5982809f4 100644 --- a/packages/web/src/primitives/useProject/checklists/index.ts +++ b/packages/web/src/primitives/useProject/checklists/index.ts @@ -16,11 +16,10 @@ import { ROB2Handler } from './handlers/rob2'; import type { ChecklistHandler } from './handlers/base'; import { applyYTextDiff } from '@/hooks/useYText'; -interface TextRefParams { - sectionKey?: string; - fieldKey?: string; - questionKey?: string; -} +export type TextRef = + | { type: 'AMSTAR2'; questionKey: string } + | { type: 'ROBINS_I'; sectionKey: string; fieldKey: string; questionKey?: string | null } + | { type: 'ROB2'; sectionKey: string; fieldKey: string; questionKey?: string | null }; export interface ChecklistOperations { createChecklist: ( @@ -54,11 +53,11 @@ export interface ChecklistOperations { fieldKey: string, questionKey?: string | null, ) => Y.Text | null; - getTextRef: (studyId: string, checklistId: string, params?: TextRefParams) => Y.Text | null; + getTextRef: (studyId: string, checklistId: string, ref: TextRef) => Y.Text | null; setTextValue: ( studyId: string, checklistId: string, - params: TextRefParams, + ref: TextRef, text: string, maxLength?: number, ) => void; @@ -334,35 +333,37 @@ export function createChecklistOperations( return textGetter(studyId, checklistId, sectionKey, fieldKey, questionKey); } - function getTextRef( - studyId: string, - checklistId: string, - params: TextRefParams = {}, - ): Y.Text | null { - const result = commonOps.getChecklistYMap(studyId, checklistId); - if (!result) return null; - - const { checklistType } = result; - const { sectionKey, fieldKey, questionKey } = params; - - if (checklistType === 'AMSTAR2') { - return getQuestionNote(studyId, checklistId, questionKey || ''); - } else if (checklistType === 'ROBINS_I') { - return getRobinsText(studyId, checklistId, sectionKey || '', fieldKey || '', questionKey); - } else if (checklistType === 'ROB2') { - return getRob2Text(studyId, checklistId, sectionKey || '', fieldKey || '', questionKey); + function getTextRef(studyId: string, checklistId: string, ref: TextRef): Y.Text | null { + switch (ref.type) { + case 'AMSTAR2': + return getQuestionNote(studyId, checklistId, ref.questionKey); + case 'ROBINS_I': + return getRobinsText( + studyId, + checklistId, + ref.sectionKey, + ref.fieldKey, + ref.questionKey ?? null, + ); + case 'ROB2': + return getRob2Text( + studyId, + checklistId, + ref.sectionKey, + ref.fieldKey, + ref.questionKey ?? null, + ); } - return null; } function setTextValue( studyId: string, checklistId: string, - params: TextRefParams, + ref: TextRef, text: string, maxLength = 2000, ): void { - const yText = getTextRef(studyId, checklistId, params); + const yText = getTextRef(studyId, checklistId, ref); if (!yText) return; const str = (typeof text === 'string' ? text : '').slice(0, maxLength); if (yText.toString() === str) return; From 7cae2c9d5fcaaf38c715b306a00fcedd4c7fcf1a Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Sat, 18 Apr 2026 15:54:02 +0000 Subject: [PATCH 2/9] Apply Prettier formatting --- .../reconcile-tab/ReconciliationWrapper.tsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx b/packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx index 49d3a3bb6..6aaa7d9cd 100644 --- a/packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx +++ b/packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx @@ -461,21 +461,22 @@ export function ReconciliationWrapper({ if (!reconciledChecklistId) return; const poolOps = connectionPool.getOps(projectId); if (!poolOps) throw new Error(`No connection for project ${projectId}`); - const ref: TextRef = isRobinsI - ? { + const ref: TextRef = + isRobinsI ? + { type: 'ROBINS_I', sectionKey: params.sectionKey ?? '', fieldKey: params.fieldKey ?? '', questionKey: params.questionKey ?? null, } - : isRob2 - ? { - type: 'ROB2', - sectionKey: params.sectionKey ?? '', - fieldKey: params.fieldKey ?? '', - questionKey: params.questionKey ?? null, - } - : { type: 'AMSTAR2', questionKey: params.questionKey ?? '' }; + : isRob2 ? + { + type: 'ROB2', + sectionKey: params.sectionKey ?? '', + fieldKey: params.fieldKey ?? '', + questionKey: params.questionKey ?? null, + } + : { type: 'AMSTAR2', questionKey: params.questionKey ?? '' }; poolOps.checklist.setTextValue(studyId, reconciledChecklistId, ref, text); }, [studyId, reconciledChecklistId, projectId, isRobinsI, isRob2], From ebcbdbf4f3e2a29d4db06e1637492c8ad83fa190 Mon Sep 17 00:00:00 2001 From: Jacob Maynard Date: Sat, 18 Apr 2026 10:56:42 -0500 Subject: [PATCH 3/9] expose textref as project action --- packages/web/src/project/actions.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/web/src/project/actions.ts b/packages/web/src/project/actions.ts index 70b54b218..9d5cc610b 100644 --- a/packages/web/src/project/actions.ts +++ b/packages/web/src/project/actions.ts @@ -11,6 +11,7 @@ import { pdfActions } from './actions/pdfs'; import { projectActions } from './actions/project'; import { memberActions } from './actions/members'; import type { ReconciliationProgressData } from '@/primitives/useProject/reconciliation.js'; +import type { TextRef } from '@/primitives/useProject/checklists'; export const project = { study: studyActions, @@ -99,10 +100,22 @@ export const project = { ops.checklist.updateChecklistAnswer(studyId, checklistId, questionId, data); }, - getQuestionNote(studyId: string, checklistId: string, questionId: string): unknown { + getTextRef(studyId: string, checklistId: string, ref: TextRef) { const ops = connectionPool.getActiveOps(); if (!ops) throw new Error('No active project connection'); - return ops.checklist.getQuestionNote(studyId, checklistId, questionId); + return ops.checklist.getTextRef(studyId, checklistId, ref); + }, + + setTextValue( + studyId: string, + checklistId: string, + ref: TextRef, + text: string, + maxLength?: number, + ): void { + const ops = connectionPool.getActiveOps(); + if (!ops) throw new Error('No active project connection'); + ops.checklist.setTextValue(studyId, checklistId, ref, text, maxLength); }, }, From 8f17a9a34928e578413d8c26aea5219a5f1d7c5c Mon Sep 17 00:00:00 2001 From: Jacob Maynard Date: Sat, 18 Apr 2026 11:29:29 -0500 Subject: [PATCH 4/9] hook up textref --- .../AMSTAR2Checklist/AMSTAR2Checklist.tsx | 50 ++++++++++--------- .../components/checklist/ChecklistWithPdf.tsx | 8 +-- .../checklist/ChecklistYjsWrapper.tsx | 4 +- .../components/checklist/GenericChecklist.tsx | 8 +-- .../checklist/LocalChecklistView.tsx | 9 ++-- .../completed-tab/PreviousReviewersView.tsx | 6 +-- 6 files changed, 46 insertions(+), 39 deletions(-) diff --git a/packages/web/src/components/checklist/AMSTAR2Checklist/AMSTAR2Checklist.tsx b/packages/web/src/components/checklist/AMSTAR2Checklist/AMSTAR2Checklist.tsx index 9709bd6d8..01f3130dc 100644 --- a/packages/web/src/components/checklist/AMSTAR2Checklist/AMSTAR2Checklist.tsx +++ b/packages/web/src/components/checklist/AMSTAR2Checklist/AMSTAR2Checklist.tsx @@ -10,11 +10,13 @@ */ import { useState, useCallback, useMemo } from 'react'; +import type * as Y from 'yjs'; import { InfoIcon } from 'lucide-react'; import { AMSTAR_CHECKLIST } from './checklist-map'; import { createChecklist as createAMSTAR2Checklist } from './checklist.js'; import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'; import { NoteEditor } from '@/components/checklist/common/NoteEditor'; +import type { TextRef } from '@/primitives/useProject/checklists'; // -- Shared internal components -- @@ -130,7 +132,7 @@ function StandardQuestion({ question, handleChange, onUpdate, - getQuestionNote, + getTextRef, readOnly, width, }: { @@ -138,7 +140,7 @@ function StandardQuestion({ question: any; handleChange: (_colIdx: number, _optIdx: number) => void; onUpdate: (_newState: any) => void; - getQuestionNote?: (_questionKey: string) => any; + getTextRef?: (_ref: TextRef) => Y.Text | null; readOnly?: boolean; width?: string; }) { @@ -149,9 +151,9 @@ function StandardQuestion({ }, [question]); const noteYText = useMemo(() => { - if (!questionKey || !getQuestionNote) return null; - return getQuestionNote(questionKey); - }, [questionKey, getQuestionNote]); + if (!questionKey || !getTextRef) return null; + return getTextRef({ type: 'AMSTAR2', questionKey }); + }, [questionKey, getTextRef]); return (
@@ -167,7 +169,7 @@ function StandardQuestion({ handleChange={handleChange} width={width} /> - {getQuestionNote && } + {getTextRef && }
); } @@ -344,12 +346,12 @@ const QUESTION_CONFIGS: QuestionConfig[] = [ function Question9({ checklist, onUpdate, - getQuestionNote, + getTextRef, readOnly, }: { checklist: any; onUpdate: (_patch: Record) => void; - getQuestionNote?: (_key: string) => any; + getTextRef?: (_ref: TextRef) => Y.Text | null; readOnly?: boolean; }) { const stateA = checklist.q9a; @@ -417,8 +419,8 @@ function Question9({ ); const noteYText = useMemo( - () => (getQuestionNote ? getQuestionNote('q9') : null), - [getQuestionNote], + () => (getTextRef ? getTextRef({ type: 'AMSTAR2', questionKey: 'q9' }) : null), + [getTextRef], ); return ( @@ -442,7 +444,7 @@ function Question9({ columns={question.columns2} handleChange={handleChangeB} /> - {getQuestionNote && } + {getTextRef && } ); } @@ -451,12 +453,12 @@ function Question9({ function Question11({ checklist, onUpdate, - getQuestionNote, + getTextRef, readOnly, }: { checklist: any; onUpdate: (_patch: Record) => void; - getQuestionNote?: (_key: string) => any; + getTextRef?: (_ref: TextRef) => Y.Text | null; readOnly?: boolean; }) { const stateA = checklist.q11a; @@ -519,8 +521,8 @@ function Question11({ ); const noteYText = useMemo( - () => (getQuestionNote ? getQuestionNote('q11') : null), - [getQuestionNote], + () => (getTextRef ? getTextRef({ type: 'AMSTAR2', questionKey: 'q11' }) : null), + [getTextRef], ); return ( @@ -546,7 +548,7 @@ function Question11({ handleChange={handleChangeB} width='w-48' /> - {getQuestionNote && } + {getTextRef && } ); } @@ -557,14 +559,14 @@ interface AMSTAR2ChecklistProps { externalChecklist?: any; onExternalUpdate?: (_patch: Record) => void; readOnly?: boolean; - getQuestionNote?: (_questionKey: string) => any; + getTextRef?: (_ref: TextRef) => Y.Text | null; } export function AMSTAR2Checklist({ externalChecklist, onExternalUpdate, readOnly, - getQuestionNote, + getTextRef, }: AMSTAR2ChecklistProps) { // Local fallback state for standalone mode (no Yjs) const [localChecklist, setLocalChecklist] = useState(() => { @@ -621,7 +623,7 @@ export function AMSTAR2Checklist({ handleChecklistChange({ q1: { ...state, answers: newAnswers } }); }} onUpdate={(newQ: any) => handleChecklistChange({ q1: newQ })} - getQuestionNote={getQuestionNote} + getTextRef={getTextRef} readOnly={readOnly} /> @@ -636,7 +638,7 @@ export function AMSTAR2Checklist({ handleChecklistChange({ [cfg.qKey]: newQ }); }} onUpdate={(newQ: any) => handleChecklistChange({ [cfg.qKey]: newQ })} - getQuestionNote={getQuestionNote} + getTextRef={getTextRef} readOnly={readOnly} width={cfg.width} /> @@ -646,7 +648,7 @@ export function AMSTAR2Checklist({ @@ -659,7 +661,7 @@ export function AMSTAR2Checklist({ handleChecklistChange({ q10: newQ }); }} onUpdate={(newQ: any) => handleChecklistChange({ q10: newQ })} - getQuestionNote={getQuestionNote} + getTextRef={getTextRef} readOnly={readOnly} /> @@ -667,7 +669,7 @@ export function AMSTAR2Checklist({ @@ -682,7 +684,7 @@ export function AMSTAR2Checklist({ handleChecklistChange({ [cfg.qKey]: newQ }); }} onUpdate={(newQ: any) => handleChecklistChange({ [cfg.qKey]: newQ })} - getQuestionNote={getQuestionNote} + getTextRef={getTextRef} readOnly={readOnly} width={cfg.width} /> diff --git a/packages/web/src/components/checklist/ChecklistWithPdf.tsx b/packages/web/src/components/checklist/ChecklistWithPdf.tsx index b5ae04fa5..88bda5169 100644 --- a/packages/web/src/components/checklist/ChecklistWithPdf.tsx +++ b/packages/web/src/components/checklist/ChecklistWithPdf.tsx @@ -6,8 +6,10 @@ */ import { lazy, Suspense } from 'react'; +import type * as Y from 'yjs'; import { GenericChecklist } from '@/components/checklist/GenericChecklist'; import { SplitScreenLayout } from '@/components/checklist/SplitScreenLayout'; +import type { TextRef } from '@/primitives/useProject/checklists'; const EmbedPdfViewer = lazy(() => import('@/components/pdf/EmbedPdfViewer')); @@ -22,7 +24,7 @@ interface ChecklistWithPdfProps { pdfs?: any[]; selectedPdfId?: string | null; onPdfSelect?: (_pdfId: string) => void; - getQuestionNote?: (_questionKey: string) => any; + getTextRef?: (_ref: TextRef) => Y.Text | null; getRobinsText?: (_sectionKey: string, _fieldKey: string, _questionKey?: string) => any; getRob2Text?: (_sectionKey: string, _fieldKey: string, _questionKey?: string) => any; pdfUrl?: string | null; @@ -46,7 +48,7 @@ export function ChecklistWithPdf({ pdfs, selectedPdfId, onPdfSelect, - getQuestionNote, + getTextRef, getRobinsText, getRob2Text, pdfUrl, @@ -74,7 +76,7 @@ export function ChecklistWithPdf({ checklist={checklist} onUpdate={onUpdate} readOnly={readOnly} - getQuestionNote={getQuestionNote} + getTextRef={getTextRef} getRobinsText={getRobinsText} getRob2Text={getRob2Text} /> diff --git a/packages/web/src/components/checklist/ChecklistYjsWrapper.tsx b/packages/web/src/components/checklist/ChecklistYjsWrapper.tsx index ddbf397a0..fee9c6805 100644 --- a/packages/web/src/components/checklist/ChecklistYjsWrapper.tsx +++ b/packages/web/src/components/checklist/ChecklistYjsWrapper.tsx @@ -83,7 +83,7 @@ export function ChecklistYjsWrapper({ projectId, studyId, checklistId }: Checkli const ops = connectionPool.getOps(projectId); if (!ops) throw new Error(`No connection for project ${projectId}`); - const { updateChecklistAnswer, updateChecklist, getQuestionNote, getRobinsText, getRob2Text } = + const { updateChecklistAnswer, updateChecklist, getTextRef, getRobinsText, getRob2Text } = ops.checklist; const { addPdfToStudy } = ops.pdf; const { addAnnotation, updateAnnotation, deleteAnnotation } = ops.annotation; @@ -439,7 +439,7 @@ export function ChecklistYjsWrapper({ projectId, studyId, checklistId }: Checkli pdfs={studyPdfs} selectedPdfId={selectedPdfId} onPdfSelect={handlePdfSelect} - getQuestionNote={(questionKey: string) => getQuestionNote(studyId, checklistId, questionKey)} + getTextRef={ref => getTextRef(studyId, checklistId, ref)} getRobinsText={(sectionKey: string, fieldKey: string, questionKey?: string) => getRobinsText(studyId, checklistId, sectionKey, fieldKey, questionKey) } diff --git a/packages/web/src/components/checklist/GenericChecklist.tsx b/packages/web/src/components/checklist/GenericChecklist.tsx index ca8e71b3b..e675ed5d6 100644 --- a/packages/web/src/components/checklist/GenericChecklist.tsx +++ b/packages/web/src/components/checklist/GenericChecklist.tsx @@ -6,6 +6,7 @@ */ import { useMemo } from 'react'; +import type * as Y from 'yjs'; import { getChecklistTypeFromState, DEFAULT_CHECKLIST_TYPE, @@ -14,13 +15,14 @@ import { import { AMSTAR2Checklist } from '@/components/checklist/AMSTAR2Checklist/AMSTAR2Checklist'; import { ROBINSIChecklist } from '@/components/checklist/ROBINSIChecklist/ROBINSIChecklist'; import { ROB2Checklist } from '@/components/checklist/ROB2Checklist/ROB2Checklist'; +import type { TextRef } from '@/primitives/useProject/checklists'; interface GenericChecklistProps { checklistType?: string; checklist: any; onUpdate: (_patch: Record) => void; readOnly?: boolean; - getQuestionNote?: (_questionKey: string) => any; + getTextRef?: (_ref: TextRef) => Y.Text | null; getRobinsText?: (_sectionKey: string, _fieldKey: string, _questionKey?: string) => any; getRob2Text?: (_sectionKey: string, _fieldKey: string, _questionKey?: string) => any; } @@ -30,7 +32,7 @@ export function GenericChecklist({ checklist, onUpdate, readOnly, - getQuestionNote, + getTextRef, getRobinsText, getRob2Text, }: GenericChecklistProps) { @@ -47,7 +49,7 @@ export function GenericChecklist({ externalChecklist={checklist} onExternalUpdate={onUpdate} readOnly={readOnly} - getQuestionNote={getQuestionNote} + getTextRef={getTextRef} /> )} {checklistType === CHECKLIST_TYPES.ROBINS_I && ( diff --git a/packages/web/src/components/checklist/LocalChecklistView.tsx b/packages/web/src/components/checklist/LocalChecklistView.tsx index 64565065f..155785f8a 100644 --- a/packages/web/src/components/checklist/LocalChecklistView.tsx +++ b/packages/web/src/components/checklist/LocalChecklistView.tsx @@ -16,6 +16,7 @@ import { connectionPool } from '@/project/ConnectionPool'; import { LOCAL_PROJECT_ID } from '@/project/localProject'; import { useProjectStore, selectConnectionPhase, selectStudies } from '@/stores/projectStore'; import { useChecklistAnswers } from '@/primitives/useProject/checklists/useChecklistAnswers'; +import type { TextRef } from '@/primitives/useProject/checklists'; import { db } from '@/primitives/db'; import { getChecklistTypeFromState, scoreChecklistOfType } from '@/checklist-registry/index'; import { ScoreTag } from '@/components/checklist/ScoreTag'; @@ -134,10 +135,10 @@ function LocalChecklistEditor({ checklistId }: { checklistId: string }) { [checklistId], ); - const getQuestionNote = useCallback( - (questionKey: string): Y.Text | null => { + const getTextRef = useCallback( + (ref: TextRef): Y.Text | null => { const ops = connectionPool.getOps(LOCAL_PROJECT_ID); - return ops?.checklist.getQuestionNote(checklistId, checklistId, questionKey) ?? null; + return ops?.checklist.getTextRef(checklistId, checklistId, ref) ?? null; }, [checklistId], ); @@ -250,7 +251,7 @@ function LocalChecklistEditor({ checklistId }: { checklistId: string }) { onPdfChange={handlePdfChange} onPdfClear={handlePdfClear} allowDelete={true} - getQuestionNote={getQuestionNote} + getTextRef={getTextRef} getRobinsText={getRobinsText} getRob2Text={getRob2Text} /> diff --git a/packages/web/src/components/project/completed-tab/PreviousReviewersView.tsx b/packages/web/src/components/project/completed-tab/PreviousReviewersView.tsx index 9becdd555..c42dcca2a 100644 --- a/packages/web/src/components/project/completed-tab/PreviousReviewersView.tsx +++ b/packages/web/src/components/project/completed-tab/PreviousReviewersView.tsx @@ -37,7 +37,7 @@ export function PreviousReviewersView({ const ops = connectionPool.getOps(projectId); if (!ops) throw new Error(`No connection for project ${projectId}`); const getChecklistData = ops.checklist.getChecklistData; - const getQuestionNote = ops.checklist.getQuestionNote; + const getTextRef = ops.checklist.getTextRef; const [checklist1Data, setChecklist1Data] = useState(null); const [checklist2Data, setChecklist2Data] = useState(null); @@ -201,9 +201,9 @@ export function PreviousReviewersView({ checklistType={currentChecklistType ?? undefined} readOnly={true} onUpdate={() => {}} - getQuestionNote={(questionKey: string) => { + getTextRef={ref => { if (!currentChecklistId) return null; - return getQuestionNote(study.id, currentChecklistId, questionKey); + return getTextRef(study.id, currentChecklistId, ref); }} /> From c7dd33082883132786fa1a2ee7f1d79a86accc84 Mon Sep 17 00:00:00 2001 From: Jacob Maynard Date: Sat, 18 Apr 2026 11:37:31 -0500 Subject: [PATCH 5/9] migrate robins to textref and remove redundant defensive checks --- .../AMSTAR2Checklist/AMSTAR2Checklist.tsx | 31 +++++++++---------- .../components/checklist/ChecklistWithPdf.tsx | 5 +-- .../checklist/ChecklistYjsWrapper.tsx | 6 +--- .../components/checklist/GenericChecklist.tsx | 6 ++-- .../checklist/LocalChecklistView.tsx | 17 ---------- .../ROBINSIChecklist/DomainSection.tsx | 8 +++-- .../ROBINSIChecklist/PlanningSection.tsx | 11 ++++--- .../ROBINSIChecklist/ROBINSIChecklist.tsx | 18 ++++++----- .../checklist/ROBINSIChecklist/SectionA.tsx | 12 +++++-- .../checklist/ROBINSIChecklist/SectionB.tsx | 13 ++++++-- .../checklist/ROBINSIChecklist/SectionC.tsx | 12 +++++-- .../checklist/ROBINSIChecklist/SectionD.tsx | 10 +++--- .../ROBINSIChecklist/SignallingQuestion.tsx | 29 +++++++++++------ 13 files changed, 94 insertions(+), 84 deletions(-) diff --git a/packages/web/src/components/checklist/AMSTAR2Checklist/AMSTAR2Checklist.tsx b/packages/web/src/components/checklist/AMSTAR2Checklist/AMSTAR2Checklist.tsx index 01f3130dc..acac0a01a 100644 --- a/packages/web/src/components/checklist/AMSTAR2Checklist/AMSTAR2Checklist.tsx +++ b/packages/web/src/components/checklist/AMSTAR2Checklist/AMSTAR2Checklist.tsx @@ -140,20 +140,19 @@ function StandardQuestion({ question: any; handleChange: (_colIdx: number, _optIdx: number) => void; onUpdate: (_newState: any) => void; - getTextRef?: (_ref: TextRef) => Y.Text | null; + getTextRef: (_ref: TextRef) => Y.Text | null; readOnly?: boolean; width?: string; }) { const questionKey = useMemo(() => { - const text = question?.text || ''; - const match = text.match(/^(\d+[a-z]?)\./); - return match ? `q${match[1]}` : null; + const match = question.text.match(/^(\d+[a-z]?)\./); + return `q${match[1]}`; }, [question]); - const noteYText = useMemo(() => { - if (!questionKey || !getTextRef) return null; - return getTextRef({ type: 'AMSTAR2', questionKey }); - }, [questionKey, getTextRef]); + const noteYText = useMemo( + () => getTextRef({ type: 'AMSTAR2', questionKey }), + [questionKey, getTextRef], + ); return (
@@ -169,7 +168,7 @@ function StandardQuestion({ handleChange={handleChange} width={width} /> - {getTextRef && } +
); } @@ -351,7 +350,7 @@ function Question9({ }: { checklist: any; onUpdate: (_patch: Record) => void; - getTextRef?: (_ref: TextRef) => Y.Text | null; + getTextRef: (_ref: TextRef) => Y.Text | null; readOnly?: boolean; }) { const stateA = checklist.q9a; @@ -419,7 +418,7 @@ function Question9({ ); const noteYText = useMemo( - () => (getTextRef ? getTextRef({ type: 'AMSTAR2', questionKey: 'q9' }) : null), + () => getTextRef({ type: 'AMSTAR2', questionKey: 'q9' }), [getTextRef], ); @@ -444,7 +443,7 @@ function Question9({ columns={question.columns2} handleChange={handleChangeB} /> - {getTextRef && } + ); } @@ -458,7 +457,7 @@ function Question11({ }: { checklist: any; onUpdate: (_patch: Record) => void; - getTextRef?: (_ref: TextRef) => Y.Text | null; + getTextRef: (_ref: TextRef) => Y.Text | null; readOnly?: boolean; }) { const stateA = checklist.q11a; @@ -521,7 +520,7 @@ function Question11({ ); const noteYText = useMemo( - () => (getTextRef ? getTextRef({ type: 'AMSTAR2', questionKey: 'q11' }) : null), + () => getTextRef({ type: 'AMSTAR2', questionKey: 'q11' }), [getTextRef], ); @@ -548,7 +547,7 @@ function Question11({ handleChange={handleChangeB} width='w-48' /> - {getTextRef && } + ); } @@ -559,7 +558,7 @@ interface AMSTAR2ChecklistProps { externalChecklist?: any; onExternalUpdate?: (_patch: Record) => void; readOnly?: boolean; - getTextRef?: (_ref: TextRef) => Y.Text | null; + getTextRef: (_ref: TextRef) => Y.Text | null; } export function AMSTAR2Checklist({ diff --git a/packages/web/src/components/checklist/ChecklistWithPdf.tsx b/packages/web/src/components/checklist/ChecklistWithPdf.tsx index 88bda5169..592334891 100644 --- a/packages/web/src/components/checklist/ChecklistWithPdf.tsx +++ b/packages/web/src/components/checklist/ChecklistWithPdf.tsx @@ -24,8 +24,7 @@ interface ChecklistWithPdfProps { pdfs?: any[]; selectedPdfId?: string | null; onPdfSelect?: (_pdfId: string) => void; - getTextRef?: (_ref: TextRef) => Y.Text | null; - getRobinsText?: (_sectionKey: string, _fieldKey: string, _questionKey?: string) => any; + getTextRef: (_ref: TextRef) => Y.Text | null; getRob2Text?: (_sectionKey: string, _fieldKey: string, _questionKey?: string) => any; pdfUrl?: string | null; onAnnotationAdd?: (_annotation: any) => void; @@ -49,7 +48,6 @@ export function ChecklistWithPdf({ selectedPdfId, onPdfSelect, getTextRef, - getRobinsText, getRob2Text, pdfUrl, onAnnotationAdd, @@ -77,7 +75,6 @@ export function ChecklistWithPdf({ onUpdate={onUpdate} readOnly={readOnly} getTextRef={getTextRef} - getRobinsText={getRobinsText} getRob2Text={getRob2Text} /> diff --git a/packages/web/src/components/checklist/ChecklistYjsWrapper.tsx b/packages/web/src/components/checklist/ChecklistYjsWrapper.tsx index fee9c6805..a6f60217d 100644 --- a/packages/web/src/components/checklist/ChecklistYjsWrapper.tsx +++ b/packages/web/src/components/checklist/ChecklistYjsWrapper.tsx @@ -83,8 +83,7 @@ export function ChecklistYjsWrapper({ projectId, studyId, checklistId }: Checkli const ops = connectionPool.getOps(projectId); if (!ops) throw new Error(`No connection for project ${projectId}`); - const { updateChecklistAnswer, updateChecklist, getTextRef, getRobinsText, getRob2Text } = - ops.checklist; + const { updateChecklistAnswer, updateChecklist, getTextRef, getRob2Text } = ops.checklist; const { addPdfToStudy } = ops.pdf; const { addAnnotation, updateAnnotation, deleteAnnotation } = ops.annotation; @@ -440,9 +439,6 @@ export function ChecklistYjsWrapper({ projectId, studyId, checklistId }: Checkli selectedPdfId={selectedPdfId} onPdfSelect={handlePdfSelect} getTextRef={ref => getTextRef(studyId, checklistId, ref)} - getRobinsText={(sectionKey: string, fieldKey: string, questionKey?: string) => - getRobinsText(studyId, checklistId, sectionKey, fieldKey, questionKey) - } getRob2Text={(sectionKey: string, fieldKey: string, questionKey?: string) => getRob2Text(studyId, checklistId, sectionKey, fieldKey, questionKey) } diff --git a/packages/web/src/components/checklist/GenericChecklist.tsx b/packages/web/src/components/checklist/GenericChecklist.tsx index e675ed5d6..a7aecdd15 100644 --- a/packages/web/src/components/checklist/GenericChecklist.tsx +++ b/packages/web/src/components/checklist/GenericChecklist.tsx @@ -22,8 +22,7 @@ interface GenericChecklistProps { checklist: any; onUpdate: (_patch: Record) => void; readOnly?: boolean; - getTextRef?: (_ref: TextRef) => Y.Text | null; - getRobinsText?: (_sectionKey: string, _fieldKey: string, _questionKey?: string) => any; + getTextRef: (_ref: TextRef) => Y.Text | null; getRob2Text?: (_sectionKey: string, _fieldKey: string, _questionKey?: string) => any; } @@ -33,7 +32,6 @@ export function GenericChecklist({ onUpdate, readOnly, getTextRef, - getRobinsText, getRob2Text, }: GenericChecklistProps) { const checklistType = useMemo(() => { @@ -59,7 +57,7 @@ export function GenericChecklist({ showComments={true} showLegend={true} readOnly={readOnly} - getRobinsText={getRobinsText} + getTextRef={getTextRef} /> )} {checklistType === CHECKLIST_TYPES.ROB2 && ( diff --git a/packages/web/src/components/checklist/LocalChecklistView.tsx b/packages/web/src/components/checklist/LocalChecklistView.tsx index 155785f8a..6b1d7ae0b 100644 --- a/packages/web/src/components/checklist/LocalChecklistView.tsx +++ b/packages/web/src/components/checklist/LocalChecklistView.tsx @@ -143,22 +143,6 @@ function LocalChecklistEditor({ checklistId }: { checklistId: string }) { [checklistId], ); - const getRobinsText = useCallback( - (sectionKey: string, fieldKey: string, questionKey?: string): Y.Text | null => { - const ops = connectionPool.getOps(LOCAL_PROJECT_ID); - return ( - ops?.checklist.getRobinsText( - checklistId, - checklistId, - sectionKey, - fieldKey, - questionKey ?? null, - ) ?? null - ); - }, - [checklistId], - ); - const getRob2Text = useCallback( (sectionKey: string, fieldKey: string, questionKey?: string): Y.Text | null => { const ops = connectionPool.getOps(LOCAL_PROJECT_ID); @@ -252,7 +236,6 @@ function LocalChecklistEditor({ checklistId }: { checklistId: string }) { onPdfClear={handlePdfClear} allowDelete={true} getTextRef={getTextRef} - getRobinsText={getRobinsText} getRob2Text={getRob2Text} /> ); diff --git a/packages/web/src/components/checklist/ROBINSIChecklist/DomainSection.tsx b/packages/web/src/components/checklist/ROBINSIChecklist/DomainSection.tsx index 9723338cd..0d4df86d4 100644 --- a/packages/web/src/components/checklist/ROBINSIChecklist/DomainSection.tsx +++ b/packages/web/src/components/checklist/ROBINSIChecklist/DomainSection.tsx @@ -5,10 +5,12 @@ */ import { useMemo, useCallback } from 'react'; +import type * as Y from 'yjs'; import { ROBINS_I_CHECKLIST, getDomainQuestions } from './checklist-map'; import { SignallingQuestion } from './SignallingQuestion'; import { DomainJudgement, JudgementBadge } from './DomainJudgement'; import { scoreRobinsDomain, getEffectiveDomainJudgement } from './scoring/robins-scoring.js'; +import type { TextRef } from '@/primitives/useProject/checklists'; interface DomainSectionProps { domainKey: string; @@ -18,7 +20,7 @@ interface DomainSectionProps { showComments?: boolean; collapsed?: boolean; onToggleCollapse?: () => void; - getRobinsText?: (_sectionKey: string, _fieldKey: string, _questionKey?: string) => any; + getTextRef: (_ref: TextRef) => Y.Text | null; } export function DomainSection({ @@ -29,7 +31,7 @@ export function DomainSection({ showComments, collapsed, onToggleCollapse, - getRobinsText, + getTextRef, }: DomainSectionProps) { const domain = (ROBINS_I_CHECKLIST as any)[domainKey]; const questions = useMemo(() => getDomainQuestions(domainKey), [domainKey]); @@ -114,7 +116,7 @@ export function DomainSection({ showComment={showComments} domainKey={domainKey} questionKey={qKey} - getRobinsText={getRobinsText} + getTextRef={getTextRef} isSkippable={isQuestionSkippable(qKey)} /> )); diff --git a/packages/web/src/components/checklist/ROBINSIChecklist/PlanningSection.tsx b/packages/web/src/components/checklist/ROBINSIChecklist/PlanningSection.tsx index 827c9d535..7e45fc354 100644 --- a/packages/web/src/components/checklist/ROBINSIChecklist/PlanningSection.tsx +++ b/packages/web/src/components/checklist/ROBINSIChecklist/PlanningSection.tsx @@ -3,20 +3,23 @@ */ import { useMemo } from 'react'; +import type * as Y from 'yjs'; import { PLANNING_SECTION } from './checklist-map'; import { NoteEditor } from '@/components/checklist/common/NoteEditor'; +import type { TextRef } from '@/primitives/useProject/checklists'; interface PlanningSectionProps { disabled?: boolean; - getRobinsText?: (_sectionKey: string, _fieldKey: string) => any; + getTextRef: (_ref: TextRef) => Y.Text | null; } -export function PlanningSection({ disabled, getRobinsText }: PlanningSectionProps) { +export function PlanningSection({ disabled, getTextRef }: PlanningSectionProps) { const p1Field = (PLANNING_SECTION as any).p1; const yText = useMemo( - () => (getRobinsText ? getRobinsText('planning', 'confoundingFactors') : null), - [getRobinsText], + () => + getTextRef({ type: 'ROBINS_I', sectionKey: 'planning', fieldKey: 'confoundingFactors' }), + [getTextRef], ); return ( diff --git a/packages/web/src/components/checklist/ROBINSIChecklist/ROBINSIChecklist.tsx b/packages/web/src/components/checklist/ROBINSIChecklist/ROBINSIChecklist.tsx index ae34d28d6..f890fdb74 100644 --- a/packages/web/src/components/checklist/ROBINSIChecklist/ROBINSIChecklist.tsx +++ b/packages/web/src/components/checklist/ROBINSIChecklist/ROBINSIChecklist.tsx @@ -7,7 +7,9 @@ */ import { useState, useMemo, useCallback, useRef, useEffect } from 'react'; +import type * as Y from 'yjs'; import { getActiveDomainKeys } from './checklist-map'; +import type { TextRef } from '@/primitives/useProject/checklists'; import { shouldStopAssessment } from './checklist.js'; import { PlanningSection } from './PlanningSection'; import { SectionA } from './SectionA'; @@ -25,7 +27,7 @@ interface ROBINSIChecklistProps { showComments?: boolean; showLegend?: boolean; readOnly?: boolean; - getRobinsText?: (_sectionKey: string, _fieldKey: string, _questionKey?: string) => any; + getTextRef: (_ref: TextRef) => Y.Text | null; } export function ROBINSIChecklist({ @@ -34,7 +36,7 @@ export function ROBINSIChecklist({ showComments, showLegend, readOnly, - getRobinsText, + getTextRef, }: ROBINSIChecklistProps) { const isReadOnly = !!readOnly; const [collapsedDomains, setCollapsedDomains] = useState>({}); @@ -104,7 +106,7 @@ export function ROBINSIChecklist({ {showLegend !== false && } {/* Planning Stage */} - + {/* Preliminary Considerations Header */}
@@ -113,27 +115,27 @@ export function ROBINSIChecklist({
- + {/* Domain sections - hidden if assessment stopped */} @@ -157,7 +159,7 @@ export function ROBINSIChecklist({ showComments={showComments} collapsed={collapsedDomains[domainKey]} onToggleCollapse={() => toggleDomainCollapse(domainKey)} - getRobinsText={getRobinsText} + getTextRef={getTextRef} /> ))} diff --git a/packages/web/src/components/checklist/ROBINSIChecklist/SectionA.tsx b/packages/web/src/components/checklist/ROBINSIChecklist/SectionA.tsx index 29f2d7a28..ff2289c06 100644 --- a/packages/web/src/components/checklist/ROBINSIChecklist/SectionA.tsx +++ b/packages/web/src/components/checklist/ROBINSIChecklist/SectionA.tsx @@ -2,15 +2,17 @@ * SectionA - ROBINS-I Part A: Specify the result being assessed */ +import type * as Y from 'yjs'; import { SECTION_A } from './checklist-map'; import { NoteEditor } from '@/components/checklist/common/NoteEditor'; +import type { TextRef } from '@/primitives/useProject/checklists'; interface SectionAProps { disabled?: boolean; - getRobinsText?: (_sectionKey: string, _fieldKey: string) => any; + getTextRef: (_ref: TextRef) => Y.Text | null; } -export function SectionA({ disabled, getRobinsText }: SectionAProps) { +export function SectionA({ disabled, getTextRef }: SectionAProps) { return (
@@ -35,7 +37,11 @@ export function SectionA({ disabled, getRobinsText }: SectionAProps) {
void; disabled?: boolean; - getRobinsText?: (_sectionKey: string, _fieldKey: string, _questionKey?: string) => any; + getTextRef: (_ref: TextRef) => Y.Text | null; } -export function SectionB({ sectionBState, onUpdate, disabled, getRobinsText }: SectionBProps) { +export function SectionB({ sectionBState, onUpdate, disabled, getTextRef }: SectionBProps) { const uniqueId = useId(); const stopAssessment = useMemo(() => shouldStopAssessment(sectionBState), [sectionBState]); @@ -89,7 +91,12 @@ export function SectionB({ sectionBState, onUpdate, disabled, getRobinsText }: S
void; disabled?: boolean; - getRobinsText?: (_sectionKey: string, _fieldKey: string) => any; + getTextRef: (_ref: TextRef) => Y.Text | null; } -export function SectionC({ sectionCState, onUpdate, disabled, getRobinsText }: SectionCProps) { +export function SectionC({ sectionCState, onUpdate, disabled, getTextRef }: SectionCProps) { const uniqueId = useId(); const textFields = useMemo( () => @@ -53,7 +55,11 @@ export function SectionC({ sectionCState, onUpdate, disabled, getRobinsText }: S
void; disabled?: boolean; - getRobinsText?: (_sectionKey: string, _fieldKey: string) => any; + getTextRef: (_ref: TextRef) => Y.Text | null; } -export function SectionD({ sectionDState, onUpdate, disabled, getRobinsText }: SectionDProps) { +export function SectionD({ sectionDState, onUpdate, disabled, getTextRef }: SectionDProps) { const handleSourceToggle = useCallback( (sourceName: string) => { const newSources = { @@ -26,8 +28,8 @@ export function SectionD({ sectionDState, onUpdate, disabled, getRobinsText }: S ); const otherSpecifyYText = useMemo( - () => (getRobinsText ? getRobinsText('sectionD', 'otherSpecify') : null), - [getRobinsText], + () => getTextRef({ type: 'ROBINS_I', sectionKey: 'sectionD', fieldKey: 'otherSpecify' }), + [getTextRef], ); return ( diff --git a/packages/web/src/components/checklist/ROBINSIChecklist/SignallingQuestion.tsx b/packages/web/src/components/checklist/ROBINSIChecklist/SignallingQuestion.tsx index cfcd50081..00b6a0c91 100644 --- a/packages/web/src/components/checklist/ROBINSIChecklist/SignallingQuestion.tsx +++ b/packages/web/src/components/checklist/ROBINSIChecklist/SignallingQuestion.tsx @@ -1,12 +1,13 @@ /** * SignallingQuestion - A single signalling question with response button options - * Used by ROBINS-I DomainSection. Nearly identical to ROB2's version but uses - * getRobinsText instead of getRob2Text and shows question.note inline. + * Used by ROBINS-I DomainSection. Shows question.note inline. */ import { useEffect, useMemo, useCallback } from 'react'; +import type * as Y from 'yjs'; import { RESPONSE_LABELS, getResponseOptions } from './checklist-map'; import { NoteEditor } from '@/components/checklist/common/NoteEditor'; +import type { TextRef } from '@/primitives/useProject/checklists'; interface SignallingQuestionProps { question: any; @@ -14,9 +15,9 @@ interface SignallingQuestionProps { onUpdate: (_newAnswer: any) => void; disabled?: boolean; showComment?: boolean; - domainKey?: string; - questionKey?: string; - getRobinsText?: (_sectionKey: string, _fieldKey: string, _questionKey?: string) => any; + domainKey: string; + questionKey: string; + getTextRef: (_ref: TextRef) => Y.Text | null; isSkippable?: boolean; } @@ -28,7 +29,7 @@ export function SignallingQuestion({ showComment, domainKey, questionKey, - getRobinsText, + getTextRef, isSkippable, }: SignallingQuestionProps) { const options = useMemo(() => getResponseOptions(question.responseType), [question.responseType]); @@ -49,10 +50,18 @@ export function SignallingQuestion({ [answer, onUpdate], ); - const commentYText = useMemo(() => { - if (!showComment || !getRobinsText || !domainKey || !questionKey) return null; - return getRobinsText(domainKey, 'comment', questionKey); - }, [showComment, getRobinsText, domainKey, questionKey]); + const commentYText = useMemo( + () => + showComment ? + getTextRef({ + type: 'ROBINS_I', + sectionKey: domainKey, + fieldKey: 'comment', + questionKey, + }) + : null, + [showComment, getTextRef, domainKey, questionKey], + ); return (
Date: Sat, 18 Apr 2026 11:40:07 -0500 Subject: [PATCH 6/9] migrate rob2 to textref --- .../components/checklist/ChecklistWithPdf.tsx | 5 +---- .../checklist/ChecklistYjsWrapper.tsx | 5 +---- .../components/checklist/GenericChecklist.tsx | 4 +--- .../checklist/LocalChecklistView.tsx | 17 --------------- .../checklist/ROB2Checklist/DomainSection.tsx | 8 ++++--- .../ROB2Checklist/PreliminarySection.tsx | 18 +++++++++------- .../checklist/ROB2Checklist/ROB2Checklist.tsx | 10 +++++---- .../ROB2Checklist/SignallingQuestion.tsx | 21 ++++++++++++------- 8 files changed, 37 insertions(+), 51 deletions(-) diff --git a/packages/web/src/components/checklist/ChecklistWithPdf.tsx b/packages/web/src/components/checklist/ChecklistWithPdf.tsx index 592334891..bd2ea1d44 100644 --- a/packages/web/src/components/checklist/ChecklistWithPdf.tsx +++ b/packages/web/src/components/checklist/ChecklistWithPdf.tsx @@ -25,7 +25,6 @@ interface ChecklistWithPdfProps { selectedPdfId?: string | null; onPdfSelect?: (_pdfId: string) => void; getTextRef: (_ref: TextRef) => Y.Text | null; - getRob2Text?: (_sectionKey: string, _fieldKey: string, _questionKey?: string) => any; pdfUrl?: string | null; onAnnotationAdd?: (_annotation: any) => void; onAnnotationUpdate?: (_annotation: any) => void; @@ -48,7 +47,6 @@ export function ChecklistWithPdf({ selectedPdfId, onPdfSelect, getTextRef, - getRob2Text, pdfUrl, onAnnotationAdd, onAnnotationUpdate, @@ -68,14 +66,13 @@ export function ChecklistWithPdf({ pdfUrl={pdfUrl} pdfData={pdfData} > - {/* First panel: Checklist (type-aware) */} + {/* First panel: Checklist */} {/* Second panel: PDF Viewer */} diff --git a/packages/web/src/components/checklist/ChecklistYjsWrapper.tsx b/packages/web/src/components/checklist/ChecklistYjsWrapper.tsx index a6f60217d..a76de2770 100644 --- a/packages/web/src/components/checklist/ChecklistYjsWrapper.tsx +++ b/packages/web/src/components/checklist/ChecklistYjsWrapper.tsx @@ -83,7 +83,7 @@ export function ChecklistYjsWrapper({ projectId, studyId, checklistId }: Checkli const ops = connectionPool.getOps(projectId); if (!ops) throw new Error(`No connection for project ${projectId}`); - const { updateChecklistAnswer, updateChecklist, getTextRef, getRob2Text } = ops.checklist; + const { updateChecklistAnswer, updateChecklist, getTextRef } = ops.checklist; const { addPdfToStudy } = ops.pdf; const { addAnnotation, updateAnnotation, deleteAnnotation } = ops.annotation; @@ -439,9 +439,6 @@ export function ChecklistYjsWrapper({ projectId, studyId, checklistId }: Checkli selectedPdfId={selectedPdfId} onPdfSelect={handlePdfSelect} getTextRef={ref => getTextRef(studyId, checklistId, ref)} - getRob2Text={(sectionKey: string, fieldKey: string, questionKey?: string) => - getRob2Text(studyId, checklistId, sectionKey, fieldKey, questionKey) - } onAnnotationAdd={handleAnnotationAdd} onAnnotationUpdate={handleAnnotationUpdate} onAnnotationDelete={handleAnnotationDelete} diff --git a/packages/web/src/components/checklist/GenericChecklist.tsx b/packages/web/src/components/checklist/GenericChecklist.tsx index a7aecdd15..517ce81e3 100644 --- a/packages/web/src/components/checklist/GenericChecklist.tsx +++ b/packages/web/src/components/checklist/GenericChecklist.tsx @@ -23,7 +23,6 @@ interface GenericChecklistProps { onUpdate: (_patch: Record) => void; readOnly?: boolean; getTextRef: (_ref: TextRef) => Y.Text | null; - getRob2Text?: (_sectionKey: string, _fieldKey: string, _questionKey?: string) => any; } export function GenericChecklist({ @@ -32,7 +31,6 @@ export function GenericChecklist({ onUpdate, readOnly, getTextRef, - getRob2Text, }: GenericChecklistProps) { const checklistType = useMemo(() => { if (checklistTypeProp) return checklistTypeProp; @@ -67,7 +65,7 @@ export function GenericChecklist({ showComments={true} showLegend={true} readOnly={readOnly} - getRob2Text={getRob2Text} + getTextRef={getTextRef} /> )}
diff --git a/packages/web/src/components/checklist/LocalChecklistView.tsx b/packages/web/src/components/checklist/LocalChecklistView.tsx index 6b1d7ae0b..4222ae166 100644 --- a/packages/web/src/components/checklist/LocalChecklistView.tsx +++ b/packages/web/src/components/checklist/LocalChecklistView.tsx @@ -143,22 +143,6 @@ function LocalChecklistEditor({ checklistId }: { checklistId: string }) { [checklistId], ); - const getRob2Text = useCallback( - (sectionKey: string, fieldKey: string, questionKey?: string): Y.Text | null => { - const ops = connectionPool.getOps(LOCAL_PROJECT_ID); - return ( - ops?.checklist.getRob2Text( - checklistId, - checklistId, - sectionKey, - fieldKey, - questionKey ?? null, - ) ?? null - ); - }, - [checklistId], - ); - const checklistForUI = useMemo(() => { if (!currentChecklist || !answers) return null; return { @@ -236,7 +220,6 @@ function LocalChecklistEditor({ checklistId }: { checklistId: string }) { onPdfClear={handlePdfClear} allowDelete={true} getTextRef={getTextRef} - getRob2Text={getRob2Text} /> ); } diff --git a/packages/web/src/components/checklist/ROB2Checklist/DomainSection.tsx b/packages/web/src/components/checklist/ROB2Checklist/DomainSection.tsx index 943f283ce..a5a100af3 100644 --- a/packages/web/src/components/checklist/ROB2Checklist/DomainSection.tsx +++ b/packages/web/src/components/checklist/ROB2Checklist/DomainSection.tsx @@ -3,10 +3,12 @@ */ import { useMemo, useCallback } from 'react'; +import type * as Y from 'yjs'; import { ROB2_CHECKLIST, getDomainQuestions } from './checklist-map'; import { SignallingQuestion } from './SignallingQuestion'; import { DomainJudgement, JudgementBadge } from './DomainJudgement'; import { scoreRob2Domain, getRequiredQuestions } from './checklist.js'; +import type { TextRef } from '@/primitives/useProject/checklists'; interface DomainSectionProps { domainKey: string; @@ -16,7 +18,7 @@ interface DomainSectionProps { showComments?: boolean; collapsed?: boolean; onToggleCollapse?: () => void; - getRob2Text?: (_sectionKey: string, _fieldKey: string, _questionKey?: string) => any; + getTextRef: (_ref: TextRef) => Y.Text | null; } export function DomainSection({ @@ -27,7 +29,7 @@ export function DomainSection({ showComments, collapsed, onToggleCollapse, - getRob2Text, + getTextRef, }: DomainSectionProps) { const domain = (ROB2_CHECKLIST as any)[domainKey]; const questions = useMemo(() => getDomainQuestions(domainKey), [domainKey]); @@ -132,7 +134,7 @@ export function DomainSection({ showComment={showComments} domainKey={domainKey} questionKey={qKey} - getRob2Text={getRob2Text} + getTextRef={getTextRef} isSkippable={isQuestionSkippable(qKey)} /> ))} diff --git a/packages/web/src/components/checklist/ROB2Checklist/PreliminarySection.tsx b/packages/web/src/components/checklist/ROB2Checklist/PreliminarySection.tsx index b34cc4c20..d669443a0 100644 --- a/packages/web/src/components/checklist/ROB2Checklist/PreliminarySection.tsx +++ b/packages/web/src/components/checklist/ROB2Checklist/PreliminarySection.tsx @@ -4,6 +4,7 @@ */ import { useMemo, useCallback } from 'react'; +import type * as Y from 'yjs'; import { PRELIMINARY_SECTION, STUDY_DESIGNS, @@ -12,19 +13,20 @@ import { INFORMATION_SOURCES, } from './checklist-map'; import { NoteEditor } from '@/components/checklist/common/NoteEditor'; +import type { TextRef } from '@/primitives/useProject/checklists'; interface PreliminarySectionProps { preliminaryState: any; onUpdate: (_newState: any) => void; disabled?: boolean; - getRob2Text?: (_sectionKey: string, _fieldKey: string, _questionKey?: string) => any; + getTextRef: (_ref: TextRef) => Y.Text | null; } export function PreliminarySection({ preliminaryState, onUpdate, disabled, - getRob2Text, + getTextRef, }: PreliminarySectionProps) { // Only send the changed field to onUpdate. The ROB2 handler's updateAnswer // does field-level merging, so we don't need to spread the entire state. @@ -64,16 +66,16 @@ export function PreliminarySection({ ); const experimentalYText = useMemo( - () => getRob2Text?.('preliminary', 'experimental') ?? null, - [getRob2Text], + () => getTextRef({ type: 'ROB2', sectionKey: 'preliminary', fieldKey: 'experimental' }), + [getTextRef], ); const comparatorYText = useMemo( - () => getRob2Text?.('preliminary', 'comparator') ?? null, - [getRob2Text], + () => getTextRef({ type: 'ROB2', sectionKey: 'preliminary', fieldKey: 'comparator' }), + [getTextRef], ); const numericalResultYText = useMemo( - () => getRob2Text?.('preliminary', 'numericalResult') ?? null, - [getRob2Text], + () => getTextRef({ type: 'ROB2', sectionKey: 'preliminary', fieldKey: 'numericalResult' }), + [getTextRef], ); return ( diff --git a/packages/web/src/components/checklist/ROB2Checklist/ROB2Checklist.tsx b/packages/web/src/components/checklist/ROB2Checklist/ROB2Checklist.tsx index 2dab557bf..07a1bb89b 100644 --- a/packages/web/src/components/checklist/ROB2Checklist/ROB2Checklist.tsx +++ b/packages/web/src/components/checklist/ROB2Checklist/ROB2Checklist.tsx @@ -6,12 +6,14 @@ */ import { useState, useMemo, useCallback, useRef, useEffect } from 'react'; +import type * as Y from 'yjs'; import { getActiveDomainKeys } from './checklist-map'; import { PreliminarySection } from './PreliminarySection'; import { DomainSection } from './DomainSection'; import { OverallSection } from './OverallSection'; import { ResponseLegend } from './SignallingQuestion'; import { ScoringSummary } from './ScoringSummary'; +import type { TextRef } from '@/primitives/useProject/checklists'; interface ROB2ChecklistProps { checklistState: any; @@ -19,7 +21,7 @@ interface ROB2ChecklistProps { showComments?: boolean; showLegend?: boolean; readOnly?: boolean; - getRob2Text?: (_sectionKey: string, _fieldKey: string, _questionKey?: string) => any; + getTextRef: (_ref: TextRef) => Y.Text | null; } export function ROB2Checklist({ @@ -28,7 +30,7 @@ export function ROB2Checklist({ showComments, showLegend, readOnly, - getRob2Text, + getTextRef, }: ROB2ChecklistProps) { const isReadOnly = !!readOnly; const [collapsedDomains, setCollapsedDomains] = useState>({}); @@ -104,7 +106,7 @@ export function ROB2Checklist({ preliminaryState={checklistState?.preliminary} onUpdate={handlePreliminaryUpdate} disabled={isReadOnly} - getRob2Text={getRob2Text} + getTextRef={getTextRef} /> {/* Message when aim not selected */} @@ -141,7 +143,7 @@ export function ROB2Checklist({ showComments={showComments} collapsed={collapsedDomains[domainKey]} onToggleCollapse={() => toggleDomainCollapse(domainKey)} - getRob2Text={getRob2Text} + getTextRef={getTextRef} />
))} diff --git a/packages/web/src/components/checklist/ROB2Checklist/SignallingQuestion.tsx b/packages/web/src/components/checklist/ROB2Checklist/SignallingQuestion.tsx index 733d3ccb6..7cb5fa4ba 100644 --- a/packages/web/src/components/checklist/ROB2Checklist/SignallingQuestion.tsx +++ b/packages/web/src/components/checklist/ROB2Checklist/SignallingQuestion.tsx @@ -4,8 +4,10 @@ */ import { useEffect, useMemo, useCallback } from 'react'; +import type * as Y from 'yjs'; import { RESPONSE_LABELS, getResponseOptions } from './checklist-map'; import { NoteEditor } from '@/components/checklist/common/NoteEditor'; +import type { TextRef } from '@/primitives/useProject/checklists'; interface SignallingQuestionProps { question: any; @@ -13,9 +15,9 @@ interface SignallingQuestionProps { onUpdate: (_newAnswer: any) => void; disabled?: boolean; showComment?: boolean; - domainKey?: string; - questionKey?: string; - getRob2Text?: (_sectionKey: string, _fieldKey: string, _questionKey?: string) => any; + domainKey: string; + questionKey: string; + getTextRef: (_ref: TextRef) => Y.Text | null; isSkippable?: boolean; } @@ -27,7 +29,7 @@ export function SignallingQuestion({ showComment, domainKey, questionKey, - getRob2Text, + getTextRef, isSkippable, }: SignallingQuestionProps) { const options = useMemo(() => getResponseOptions(question.responseType), [question.responseType]); @@ -48,10 +50,13 @@ export function SignallingQuestion({ [answer, onUpdate], ); - const commentYText = useMemo(() => { - if (!showComment || !getRob2Text || !domainKey || !questionKey) return null; - return getRob2Text(domainKey, 'comment', questionKey); - }, [showComment, getRob2Text, domainKey, questionKey]); + const commentYText = useMemo( + () => + showComment ? + getTextRef({ type: 'ROB2', sectionKey: domainKey, fieldKey: 'comment', questionKey }) + : null, + [showComment, getTextRef, domainKey, questionKey], + ); return (
Date: Sat, 18 Apr 2026 11:41:46 -0500 Subject: [PATCH 7/9] format --- .../checklist/AMSTAR2Checklist/AMSTAR2Checklist.tsx | 5 +---- .../checklist/ROBINSIChecklist/PlanningSection.tsx | 3 +-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/web/src/components/checklist/AMSTAR2Checklist/AMSTAR2Checklist.tsx b/packages/web/src/components/checklist/AMSTAR2Checklist/AMSTAR2Checklist.tsx index acac0a01a..4c4ba0de7 100644 --- a/packages/web/src/components/checklist/AMSTAR2Checklist/AMSTAR2Checklist.tsx +++ b/packages/web/src/components/checklist/AMSTAR2Checklist/AMSTAR2Checklist.tsx @@ -417,10 +417,7 @@ function Question9({ [stateA, stateB, onUpdate], ); - const noteYText = useMemo( - () => getTextRef({ type: 'AMSTAR2', questionKey: 'q9' }), - [getTextRef], - ); + const noteYText = useMemo(() => getTextRef({ type: 'AMSTAR2', questionKey: 'q9' }), [getTextRef]); return (
diff --git a/packages/web/src/components/checklist/ROBINSIChecklist/PlanningSection.tsx b/packages/web/src/components/checklist/ROBINSIChecklist/PlanningSection.tsx index 7e45fc354..08c7ead17 100644 --- a/packages/web/src/components/checklist/ROBINSIChecklist/PlanningSection.tsx +++ b/packages/web/src/components/checklist/ROBINSIChecklist/PlanningSection.tsx @@ -17,8 +17,7 @@ export function PlanningSection({ disabled, getTextRef }: PlanningSectionProps) const p1Field = (PLANNING_SECTION as any).p1; const yText = useMemo( - () => - getTextRef({ type: 'ROBINS_I', sectionKey: 'planning', fieldKey: 'confoundingFactors' }), + () => getTextRef({ type: 'ROBINS_I', sectionKey: 'planning', fieldKey: 'confoundingFactors' }), [getTextRef], ); From d0eb5da6ba68f32ea6e05ab190648fadc9901257 Mon Sep 17 00:00:00 2001 From: Jacob Maynard Date: Sat, 18 Apr 2026 11:47:20 -0500 Subject: [PATCH 8/9] migrate reconciliation engine and adapters --- .../reconcile-tab/ReconciliationWrapper.tsx | 68 ++----------------- .../amstar2-reconcile/adapter.tsx | 2 +- .../engine/ReconciliationEngine.tsx | 1 - .../project/reconcile-tab/engine/types.ts | 32 +++------ .../engine/useReconciliationEngine.ts | 19 +----- .../reconcile-tab/rob2-reconcile/adapter.tsx | 45 ++++++------ .../rob2-reconcile/pages/PreliminaryPage.tsx | 13 ++-- .../robins-i-reconcile/adapter.tsx | 65 ++++++++++++------ 8 files changed, 100 insertions(+), 145 deletions(-) diff --git a/packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx b/packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx index 6aaa7d9cd..b7e0b05ca 100644 --- a/packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx +++ b/packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx @@ -24,7 +24,6 @@ import { import { downloadPdf, getPdfUrl } from '@/api/pdf-api'; import { getCachedPdf, cachePdf } from '@/primitives/pdfCache.js'; import { showToast } from '@/components/ui/toast'; -import { CHECKLIST_TYPES } from '@/checklist-registry/types'; import { usePdfPreviewStore } from '@/stores/pdfPreviewStore'; import { ReconciliationEngine, registerReconciliationAdapter } from './engine'; import { amstar2Adapter } from './amstar2-reconcile/adapter'; @@ -68,9 +67,8 @@ export function ReconciliationWrapper({ updateChecklistAnswer, updateChecklist, getChecklistData, - getQuestionNote, - getRobinsText, - getRob2Text, + getTextRef: opsGetTextRef, + setTextValue: opsSetTextValue, } = ops.checklist; const { getReconciliationProgress, saveReconciliationProgress } = ops.reconciliation; const getAwareness = ops.getAwareness; @@ -391,9 +389,6 @@ export function ReconciliationWrapper({ }; }, [reconciledChecklistId, getChecklistData, studyId, reconciledChecklistMeta]); - const isRobinsI = checklistType === CHECKLIST_TYPES.ROBINS_I || checklistType === 'ROBINS_I'; - const isRob2 = checklistType === CHECKLIST_TYPES.ROB2 || checklistType === 'ROB2'; - // Build project path const getProjectPath = useCallback(() => `/projects/${projectId}`, [projectId]); @@ -422,64 +417,15 @@ export function ReconciliationWrapper({ navigate({ to: `${getProjectPath()}?tab=reconcile` as string }); }, [navigate, getProjectPath]); - // Unified getTextRef that routes to the correct Yjs text accessor per type const getTextRef = useCallback( - (...args: unknown[]) => { - if (isRobinsI) { - return getRobinsText( - studyId, - reconciledChecklistId as string, - ...(args as [string, string, string?]), - ); - } - if (isRob2) { - return getRob2Text( - studyId, - reconciledChecklistId as string, - ...(args as [string, string, string?]), - ); - } - // AMSTAR2: getQuestionNote takes just the question key - return getQuestionNote(studyId, reconciledChecklistId as string, args[0] as string); - }, - [ - isRobinsI, - isRob2, - studyId, - reconciledChecklistId, - getRobinsText, - getRob2Text, - getQuestionNote, - ], + (ref: TextRef) => opsGetTextRef(studyId, reconciledChecklistId as string, ref), + [opsGetTextRef, studyId, reconciledChecklistId], ); - // Set a Y.Text field by key path without direct Y.Text manipulation. - // Bridges the legacy loose-params shape used by reconciliation adapters into - // the primitive's typed TextRef. Will be replaced when adapters migrate. const setTextValue = useCallback( - (params: { sectionKey?: string; fieldKey?: string; questionKey?: string }, text: string) => { - if (!reconciledChecklistId) return; - const poolOps = connectionPool.getOps(projectId); - if (!poolOps) throw new Error(`No connection for project ${projectId}`); - const ref: TextRef = - isRobinsI ? - { - type: 'ROBINS_I', - sectionKey: params.sectionKey ?? '', - fieldKey: params.fieldKey ?? '', - questionKey: params.questionKey ?? null, - } - : isRob2 ? - { - type: 'ROB2', - sectionKey: params.sectionKey ?? '', - fieldKey: params.fieldKey ?? '', - questionKey: params.questionKey ?? null, - } - : { type: 'AMSTAR2', questionKey: params.questionKey ?? '' }; - poolOps.checklist.setTextValue(studyId, reconciledChecklistId, ref, text); - }, - [studyId, reconciledChecklistId, projectId, isRobinsI, isRob2], + (ref: TextRef, text: string) => + opsSetTextValue(studyId, reconciledChecklistId as string, ref, text), + [opsSetTextValue, studyId, reconciledChecklistId], ); // Shared props for all reconciliation types diff --git a/packages/web/src/components/project/reconcile-tab/amstar2-reconcile/adapter.tsx b/packages/web/src/components/project/reconcile-tab/amstar2-reconcile/adapter.tsx index ddbff8f9b..d8eaec9d3 100644 --- a/packages/web/src/components/project/reconcile-tab/amstar2-reconcile/adapter.tsx +++ b/packages/web/src/components/project/reconcile-tab/amstar2-reconcile/adapter.tsx @@ -215,7 +215,7 @@ function renderPage(context: EngineContext) { isMultiPart={!!currentItem.meta?.isMultiPart} reviewer1Note={getReviewerNote(checklist1, key)} reviewer2Note={getReviewerNote(checklist2, key)} - finalNoteYText={getTextRef?.(key)} + finalNoteYText={getTextRef({ type: 'AMSTAR2', questionKey: key })} /> ); } diff --git a/packages/web/src/components/project/reconcile-tab/engine/ReconciliationEngine.tsx b/packages/web/src/components/project/reconcile-tab/engine/ReconciliationEngine.tsx index 079f71dbf..ac467d5cc 100644 --- a/packages/web/src/components/project/reconcile-tab/engine/ReconciliationEngine.tsx +++ b/packages/web/src/components/project/reconcile-tab/engine/ReconciliationEngine.tsx @@ -83,7 +83,6 @@ export function ReconciliationEngine({ checklist2, reconciledChecklist, updateChecklistAnswer, - getTextRef, setTextValue, onSaveReconciled, checklist1Id: (checklist1 as any)?.id ?? null, diff --git a/packages/web/src/components/project/reconcile-tab/engine/types.ts b/packages/web/src/components/project/reconcile-tab/engine/types.ts index a427a50b2..3e8afc175 100644 --- a/packages/web/src/components/project/reconcile-tab/engine/types.ts +++ b/packages/web/src/components/project/reconcile-tab/engine/types.ts @@ -1,5 +1,7 @@ import type { ReactNode } from 'react'; +import type * as Y from 'yjs'; import type { getUserColor } from '@/lib/userColors.js'; +import type { TextRef } from '@/primitives/useProject/checklists'; // --------------------------------------------------------------------------- // Presence types (mirrored from useReconciliationPresence to avoid @@ -71,15 +73,10 @@ export interface EngineContext { isAgreement: boolean; /** Raw Yjs write callback - adapter formats args for its data model */ updateChecklistAnswer: (sectionKey: string, data: unknown) => void; - /** Raw Y.Text accessor - adapter calls with type-specific arg pattern */ - getTextRef: ((...args: unknown[]) => unknown) | null; - /** Set a Y.Text field value by key path (equality-checked, transacted) */ - setTextValue: - | (( - params: { sectionKey?: string; fieldKey?: string; questionKey?: string }, - text: string, - ) => void) - | null; + /** Y.Text accessor for collaborative comment/note fields */ + getTextRef: (ref: TextRef) => Y.Text | null; + /** Set a Y.Text field value (equality-checked, transacted) */ + setTextValue: (ref: TextRef, text: string) => void; } /** @@ -193,13 +190,7 @@ export interface ReconciliationAdapter { item: ReconciliationNavItem, checklist1: unknown, updateChecklistAnswer: (sectionKey: string, data: unknown) => void, - getTextRef: ((...args: unknown[]) => unknown) | null, - setTextValue?: - | (( - params: { sectionKey?: string; fieldKey?: string; questionKey?: string }, - text: string, - ) => void) - | null, + setTextValue: (ref: TextRef, text: string) => void, ) => void; /** Reset all answers to empty/default state */ @@ -263,13 +254,8 @@ export interface ReconciliationEngineProps { onSaveReconciled: (name?: string) => void; onCancel: () => void; updateChecklistAnswer: (sectionKey: string, data: unknown) => void; - getTextRef: ((...args: unknown[]) => unknown) | null; - setTextValue: - | (( - params: { sectionKey?: string; fieldKey?: string; questionKey?: string }, - text: string, - ) => void) - | null; + getTextRef: (ref: TextRef) => Y.Text | null; + setTextValue: (ref: TextRef, text: string) => void; // PDF pdfData: ArrayBuffer | null; diff --git a/packages/web/src/components/project/reconcile-tab/engine/useReconciliationEngine.ts b/packages/web/src/components/project/reconcile-tab/engine/useReconciliationEngine.ts index 1df3e2d3d..2e58a4e2d 100644 --- a/packages/web/src/components/project/reconcile-tab/engine/useReconciliationEngine.ts +++ b/packages/web/src/components/project/reconcile-tab/engine/useReconciliationEngine.ts @@ -14,6 +14,7 @@ import type { ReconciliationNavItem, ReconciliationSummaryStats, } from './types'; +import type { TextRef } from '@/primitives/useProject/checklists'; interface UseReconciliationEngineOptions { adapter: ReconciliationAdapter; @@ -21,13 +22,7 @@ interface UseReconciliationEngineOptions { checklist2: unknown; reconciledChecklist: unknown; updateChecklistAnswer: (sectionKey: string, data: unknown) => void; - getTextRef: ((...args: unknown[]) => unknown) | null; - setTextValue: - | (( - params: { sectionKey?: string; fieldKey?: string; questionKey?: string }, - text: string, - ) => void) - | null; + setTextValue: (ref: TextRef, text: string) => void; onSaveReconciled: (name?: string) => void; checklist1Id: string | null; checklist2Id: string | null; @@ -73,7 +68,6 @@ export function useReconciliationEngine({ checklist2, reconciledChecklist, updateChecklistAnswer, - getTextRef, setTextValue, onSaveReconciled, checklist1Id, @@ -258,13 +252,7 @@ export function useReconciliationEngine({ const hasAns = adapter.hasAnswer(item, finalAnswers); const isAgree = adapter.isAgreement(item, comparison); if (!hasAns && isAgree) { - adapter.autoFillFromReviewer1( - item, - checklist1, - updateChecklistAnswer, - getTextRef, - setTextValue, - ); + adapter.autoFillFromReviewer1(item, checklist1, updateChecklistAnswer, setTextValue); } if (currentPage < totalPages - 1) { @@ -291,7 +279,6 @@ export function useReconciliationEngine({ comparison, checklist1, updateChecklistAnswer, - getTextRef, setTextValue, expandedDomain, ]); diff --git a/packages/web/src/components/project/reconcile-tab/rob2-reconcile/adapter.tsx b/packages/web/src/components/project/reconcile-tab/rob2-reconcile/adapter.tsx index e6a9f2bdc..2efc8135c 100644 --- a/packages/web/src/components/project/reconcile-tab/rob2-reconcile/adapter.tsx +++ b/packages/web/src/components/project/reconcile-tab/rob2-reconcile/adapter.tsx @@ -14,6 +14,7 @@ import type { NavbarContext, SummaryContext, } from '../engine/types'; +import type { TextRef } from '@/primitives/useProject/checklists'; import { compareChecklists, hasAimMismatch, @@ -193,13 +194,7 @@ function autoFillFromReviewer1( item: ReconciliationNavItem, checklist1: unknown, updateChecklistAnswer: (sectionKey: string, data: unknown) => void, - _getTextRef: ((...args: unknown[]) => unknown) | null, - setTextValue?: - | (( - params: { sectionKey?: string; fieldKey?: string; questionKey?: string }, - text: string, - ) => void) - | null, + setTextValue: (ref: TextRef, text: string) => void, ): void { const c1 = checklist1 as any; @@ -209,8 +204,8 @@ function autoFillFromReviewer1( // Always update finalAnswers so hasNavItemAnswer works even if page is unmounted updatePreliminaryField(updateChecklistAnswer, item.key, value); if (PRELIMINARY_TEXT_FIELDS.includes(item.key)) { - setTextValue?.( - { sectionKey: 'preliminary', fieldKey: item.key }, + setTextValue( + { type: 'ROB2', sectionKey: 'preliminary', fieldKey: item.key }, typeof value === 'string' ? value : '', ); } @@ -219,8 +214,13 @@ function autoFillFromReviewer1( const answer = c1?.[item.domainKey]?.answers?.[item.key]; if (answer) { updateDomainQuestionAnswer(updateChecklistAnswer, item.domainKey, item.key, answer.answer); - setTextValue?.( - { sectionKey: item.domainKey, fieldKey: 'comment', questionKey: item.key }, + setTextValue( + { + type: 'ROB2', + sectionKey: item.domainKey, + fieldKey: 'comment', + questionKey: item.key, + }, answer.comment || '', ); } @@ -322,7 +322,7 @@ function renderPage(context: EngineContext) { onFinalChange={(value: any) => updatePreliminaryField(context.updateChecklistAnswer, currentItem.key, value) } - getRob2Text={getTextRef as any} + getTextRef={getTextRef} onUseReviewer1={() => { const value = c1?.preliminary?.[currentItem.key]; if (value !== undefined) { @@ -333,8 +333,8 @@ function renderPage(context: EngineContext) { // Also write to Y.Text for the NoteEditor. The equality check in // setYTextField prevents a feedback loop (setTextValue -> updateChecklistAnswer // -> setYTextField sees same value -> skips). - context.setTextValue?.( - { sectionKey: 'preliminary', fieldKey: currentItem.key }, + context.setTextValue( + { type: 'ROB2', sectionKey: 'preliminary', fieldKey: currentItem.key }, typeof value === 'string' ? value : '', ); } @@ -345,8 +345,8 @@ function renderPage(context: EngineContext) { if (value !== undefined) { updatePreliminaryField(context.updateChecklistAnswer, currentItem.key, value); if (PRELIMINARY_TEXT_FIELDS.includes(currentItem.key)) { - context.setTextValue?.( - { sectionKey: 'preliminary', fieldKey: currentItem.key }, + context.setTextValue( + { type: 'ROB2', sectionKey: 'preliminary', fieldKey: currentItem.key }, typeof value === 'string' ? value : '', ); } @@ -364,7 +364,12 @@ function renderPage(context: EngineContext) { reviewer1Data={c1?.[currentItem.domainKey!]?.answers?.[currentItem.key]} reviewer2Data={c2?.[currentItem.domainKey!]?.answers?.[currentItem.key]} finalData={fa[currentItem.domainKey!]?.answers?.[currentItem.key]} - finalCommentYText={getTextRef?.(currentItem.domainKey!, 'comment', currentItem.key)} + finalCommentYText={getTextRef({ + type: 'ROB2', + sectionKey: currentItem.domainKey!, + fieldKey: 'comment', + questionKey: currentItem.key, + })} reviewer1Name={context.reviewer1Name || 'Reviewer 1'} reviewer2Name={context.reviewer2Name || 'Reviewer 2'} isAgreement={context.isAgreement} @@ -386,8 +391,9 @@ function renderPage(context: EngineContext) { currentItem.key, data.answer, ); - context.setTextValue?.( + context.setTextValue( { + type: 'ROB2', sectionKey: currentItem.domainKey!, fieldKey: 'comment', questionKey: currentItem.key, @@ -405,8 +411,9 @@ function renderPage(context: EngineContext) { currentItem.key, data.answer, ); - context.setTextValue?.( + context.setTextValue( { + type: 'ROB2', sectionKey: currentItem.domainKey!, fieldKey: 'comment', questionKey: currentItem.key, diff --git a/packages/web/src/components/project/reconcile-tab/rob2-reconcile/pages/PreliminaryPage.tsx b/packages/web/src/components/project/reconcile-tab/rob2-reconcile/pages/PreliminaryPage.tsx index 70563ba77..087c67de6 100644 --- a/packages/web/src/components/project/reconcile-tab/rob2-reconcile/pages/PreliminaryPage.tsx +++ b/packages/web/src/components/project/reconcile-tab/rob2-reconcile/pages/PreliminaryPage.tsx @@ -1,4 +1,5 @@ import { useMemo, useEffect, useEffectEvent, useId, useCallback } from 'react'; +import type * as Y from 'yjs'; import { useYText } from '@/hooks/useYText'; import { CheckIcon, XIcon, AlertTriangleIcon } from 'lucide-react'; import { @@ -9,6 +10,7 @@ import { INFORMATION_SOURCES, } from '@corates/shared/checklists/rob2'; import { NoteEditor } from '@/components/checklist/common/NoteEditor'; +import type { TextRef } from '@/primitives/useProject/checklists'; const PRELIMINARY_TEXT_FIELDS = ['experimental', 'comparator', 'numericalResult']; @@ -255,7 +257,7 @@ interface PreliminaryPageProps { isAgreement: boolean; isAimMismatch: boolean; onFinalChange: (_value: any) => void; - getRob2Text: ((_sectionKey: string, _fieldKey: string) => any) | null; + getTextRef: (_ref: TextRef) => Y.Text | null; onUseReviewer1: () => void; onUseReviewer2: () => void; } @@ -294,7 +296,7 @@ export function PreliminaryPage({ isAgreement, isAimMismatch, onFinalChange, - getRob2Text, + getTextRef, onUseReviewer1, onUseReviewer2, }: PreliminaryPageProps) { @@ -302,7 +304,8 @@ export function PreliminaryPage({ const fieldLabel = fieldDef?.label || fieldKey; const isTextField = PRELIMINARY_TEXT_FIELDS.includes(fieldKey); - const preliminaryYText = isTextField && getRob2Text ? getRob2Text('preliminary', fieldKey) : null; + const preliminaryYText = + isTextField ? getTextRef({ type: 'ROB2', sectionKey: 'preliminary', fieldKey }) : null; const preliminaryText = useYText(preliminaryYText); // Sync Y.Text changes back to finalAnswers so hasNavItemAnswer detects @@ -366,10 +369,10 @@ export function PreliminaryPage({ ); default: // Text fields use NoteEditor with Y.Text - if (isTextField && getRob2Text) { + if (isTextField) { return ( void, - _getTextRef: ((...args: unknown[]) => unknown) | null, - setTextValue?: - | (( - params: { sectionKey?: string; fieldKey?: string; questionKey?: string }, - text: string, - ) => void) - | null, + setTextValue: (ref: TextRef, text: string) => void, ): void { const c1 = checklist1 as any; @@ -167,8 +162,13 @@ function autoFillFromReviewer1( const answer = c1?.sectionB?.[item.key]; if (answer) { updateSectionBAnswer(updateChecklistAnswer, item.key, answer.answer); - setTextValue?.( - { sectionKey: 'sectionB', fieldKey: 'comment', questionKey: item.key }, + setTextValue( + { + type: 'ROBINS_I', + sectionKey: 'sectionB', + fieldKey: 'comment', + questionKey: item.key, + }, answer.comment || '', ); } @@ -176,8 +176,13 @@ function autoFillFromReviewer1( const answer = c1?.[item.domainKey]?.answers?.[item.key]; if (answer) { updateDomainQuestionAnswer(updateChecklistAnswer, item.domainKey, item.key, answer.answer); - setTextValue?.( - { sectionKey: item.domainKey, fieldKey: 'comment', questionKey: item.key }, + setTextValue( + { + type: 'ROBINS_I', + sectionKey: item.domainKey, + fieldKey: 'comment', + questionKey: item.key, + }, answer.comment || '', ); } @@ -242,7 +247,12 @@ function renderPage(context: EngineContext) { reviewer1Data={c1?.sectionB?.[currentItem.key]} reviewer2Data={c2?.sectionB?.[currentItem.key]} finalData={fa.sectionB?.[currentItem.key]} - finalCommentYText={getTextRef?.('sectionB', 'comment', currentItem.key)} + finalCommentYText={getTextRef({ + type: 'ROBINS_I', + sectionKey: 'sectionB', + fieldKey: 'comment', + questionKey: currentItem.key, + })} reviewer1Name={context.reviewer1Name || 'Reviewer 1'} reviewer2Name={context.reviewer2Name || 'Reviewer 2'} isAgreement={context.isAgreement} @@ -253,8 +263,13 @@ function renderPage(context: EngineContext) { const data = c1?.sectionB?.[currentItem.key]; if (data) { updateSectionBAnswer(context.updateChecklistAnswer, currentItem.key, data.answer); - context.setTextValue?.( - { sectionKey: 'sectionB', fieldKey: 'comment', questionKey: currentItem.key }, + context.setTextValue( + { + type: 'ROBINS_I', + sectionKey: 'sectionB', + fieldKey: 'comment', + questionKey: currentItem.key, + }, data.comment || '', ); } @@ -263,8 +278,13 @@ function renderPage(context: EngineContext) { const data = c2?.sectionB?.[currentItem.key]; if (data) { updateSectionBAnswer(context.updateChecklistAnswer, currentItem.key, data.answer); - context.setTextValue?.( - { sectionKey: 'sectionB', fieldKey: 'comment', questionKey: currentItem.key }, + context.setTextValue( + { + type: 'ROBINS_I', + sectionKey: 'sectionB', + fieldKey: 'comment', + questionKey: currentItem.key, + }, data.comment || '', ); } @@ -281,7 +301,12 @@ function renderPage(context: EngineContext) { reviewer1Data={c1?.[currentItem.domainKey]?.answers?.[currentItem.key]} reviewer2Data={c2?.[currentItem.domainKey]?.answers?.[currentItem.key]} finalData={fa[currentItem.domainKey]?.answers?.[currentItem.key]} - finalCommentYText={getTextRef?.(currentItem.domainKey, 'comment', currentItem.key)} + finalCommentYText={getTextRef({ + type: 'ROBINS_I', + sectionKey: currentItem.domainKey, + fieldKey: 'comment', + questionKey: currentItem.key, + })} reviewer1Name={context.reviewer1Name || 'Reviewer 1'} reviewer2Name={context.reviewer2Name || 'Reviewer 2'} isAgreement={context.isAgreement} @@ -302,8 +327,9 @@ function renderPage(context: EngineContext) { currentItem.key, data.answer, ); - context.setTextValue?.( + context.setTextValue( { + type: 'ROBINS_I', sectionKey: currentItem.domainKey!, fieldKey: 'comment', questionKey: currentItem.key, @@ -321,8 +347,9 @@ function renderPage(context: EngineContext) { currentItem.key, data.answer, ); - context.setTextValue?.( + context.setTextValue( { + type: 'ROBINS_I', sectionKey: currentItem.domainKey!, fieldKey: 'comment', questionKey: currentItem.key, From 9c33822117db5bd1026006e07b56448f42224cc8 Mon Sep 17 00:00:00 2001 From: Jacob Maynard Date: Sat, 18 Apr 2026 11:48:49 -0500 Subject: [PATCH 9/9] remove legacy text functions --- .../primitives/useProject/checklists/index.ts | 73 ++++--------------- 1 file changed, 15 insertions(+), 58 deletions(-) diff --git a/packages/web/src/primitives/useProject/checklists/index.ts b/packages/web/src/primitives/useProject/checklists/index.ts index 5982809f4..7f0c8d5ed 100644 --- a/packages/web/src/primitives/useProject/checklists/index.ts +++ b/packages/web/src/primitives/useProject/checklists/index.ts @@ -38,21 +38,6 @@ export interface ChecklistOperations { key: string, data: Record, ) => void; - getQuestionNote: (studyId: string, checklistId: string, questionKey: string) => Y.Text | null; - getRobinsText: ( - studyId: string, - checklistId: string, - sectionKey: string, - fieldKey: string, - questionKey?: string | null, - ) => Y.Text | null; - getRob2Text: ( - studyId: string, - checklistId: string, - sectionKey: string, - fieldKey: string, - questionKey?: string | null, - ) => Y.Text | null; getTextRef: (studyId: string, checklistId: string, ref: TextRef) => Y.Text | null; setTextValue: ( studyId: string, @@ -299,60 +284,35 @@ export function createChecklistOperations( checklistYMap.set('updatedAt', Date.now()); } - function getQuestionNote( - studyId: string, - checklistId: string, - questionKey: string, - ): Y.Text | null { - const textGetter = amstar2Handler.getTextGetter(getYDoc); - if (!textGetter) return null; - return textGetter(studyId, checklistId, questionKey, '', null); - } - - function getRobinsText( - studyId: string, - checklistId: string, - sectionKey: string, - fieldKey: string, - questionKey: string | null = null, - ): Y.Text | null { - const textGetter = robinsIHandler.getTextGetter(getYDoc); - if (!textGetter) return null; - return textGetter(studyId, checklistId, sectionKey, fieldKey, questionKey); - } - - function getRob2Text( - studyId: string, - checklistId: string, - sectionKey: string, - fieldKey: string, - questionKey: string | null = null, - ): Y.Text | null { - const textGetter = rob2Handler.getTextGetter(getYDoc); - if (!textGetter) return null; - return textGetter(studyId, checklistId, sectionKey, fieldKey, questionKey); - } - function getTextRef(studyId: string, checklistId: string, ref: TextRef): Y.Text | null { switch (ref.type) { - case 'AMSTAR2': - return getQuestionNote(studyId, checklistId, ref.questionKey); - case 'ROBINS_I': - return getRobinsText( + case 'AMSTAR2': { + const textGetter = amstar2Handler.getTextGetter(getYDoc); + if (!textGetter) return null; + return textGetter(studyId, checklistId, ref.questionKey, '', null); + } + case 'ROBINS_I': { + const textGetter = robinsIHandler.getTextGetter(getYDoc); + if (!textGetter) return null; + return textGetter( studyId, checklistId, ref.sectionKey, ref.fieldKey, ref.questionKey ?? null, ); - case 'ROB2': - return getRob2Text( + } + case 'ROB2': { + const textGetter = rob2Handler.getTextGetter(getYDoc); + if (!textGetter) return null; + return textGetter( studyId, checklistId, ref.sectionKey, ref.fieldKey, ref.questionKey ?? null, ); + } } } @@ -377,9 +337,6 @@ export function createChecklistOperations( getChecklistAnswersMap: commonOps.getChecklistAnswersMap, getChecklistData, updateChecklistAnswer, - getQuestionNote, - getRobinsText, - getRob2Text, getTextRef, setTextValue, };