From d8de55fbb16df0f91daa051d748014fd16a0f694 Mon Sep 17 00:00:00 2001 From: Jacob Maynard Date: Tue, 24 Feb 2026 19:50:52 -0600 Subject: [PATCH] improve ux visibility for multiple outcomes --- .../completed-tab/CompletedOutcomeRow.jsx | 4 +- .../completed-tab/CompletedStudyRow.jsx | 148 ++++------- .../reconcile-tab/ReconcileStudyRow.jsx | 234 ++++++++++-------- .../project/todo-tab/ChecklistForm.jsx | 37 +-- .../project/todo-tab/TodoStudyRow.jsx | 176 +++++++------ 5 files changed, 286 insertions(+), 313 deletions(-) diff --git a/packages/web/src/components/project/completed-tab/CompletedOutcomeRow.jsx b/packages/web/src/components/project/completed-tab/CompletedOutcomeRow.jsx index 95d5a394a..8094d56e2 100644 --- a/packages/web/src/components/project/completed-tab/CompletedOutcomeRow.jsx +++ b/packages/web/src/components/project/completed-tab/CompletedOutcomeRow.jsx @@ -54,8 +54,8 @@ export default function CompletedOutcomeRow(props) { - {/* Checklist type */} - + {/* Checklist type badge */} + {getChecklistMetadata(outcomeGroup().type)?.name || outcomeGroup().type} diff --git a/packages/web/src/components/project/completed-tab/CompletedStudyRow.jsx b/packages/web/src/components/project/completed-tab/CompletedStudyRow.jsx index 6477e454b..385a8bb01 100644 --- a/packages/web/src/components/project/completed-tab/CompletedStudyRow.jsx +++ b/packages/web/src/components/project/completed-tab/CompletedStudyRow.jsx @@ -1,9 +1,9 @@ /** - * CompletedStudyRow - Compact study row for the completed tab + * CompletedStudyRow - Study row for the completed tab * - * Displays study info, completed checklist(s), collapsible PDF section, and - * button to view previous reviewer checklists (for dual-reviewer studies). - * Supports multiple outcomes - shows expandable sections when multiple outcomes exist. + * Single-outcome studies show inline badges + buttons in the header. + * Multi-outcome studies show stacked sub-rows (always visible). + * PDFs are expandable via chevron. */ import { For, Show, createMemo, createSignal } from 'solid-js'; @@ -17,13 +17,8 @@ import PreviousReviewersView from './PreviousReviewersView.jsx'; import CompletedOutcomeRow from './CompletedOutcomeRow.jsx'; export default function CompletedStudyRow(props) { - // props.study: Study object with pdfs and checklists arrays - // props.onOpenChecklist: (checklistId) => void - // props.onViewPdf: (pdf) => void - // props.onDownloadPdf: (pdf) => void - // props.getReconciliationProgress: (outcomeId, type) => Object | null - // props.getAssigneeName: (userId) => string - // props.getOutcomeName: (outcomeId) => string | null + // props.study, props.onOpenChecklist, props.onViewPdf, props.onDownloadPdf, + // props.getReconciliationProgress, props.getAssigneeName, props.getOutcomeName const [expanded, setExpanded] = createSignal(false); const [showPreviousReviewers, setShowPreviousReviewers] = createSignal(false); @@ -59,10 +54,7 @@ export default function CompletedStudyRow(props) { return getCompletedChecklistsByOutcome(study()); }); - // Check if there are multiple outcomes const hasMultipleOutcomes = () => completedOutcomeGroups().length > 1; - - // Get the first group for compact display const firstGroup = () => completedOutcomeGroups()[0]; // Check if we have previous reviewers to show (for first group in single-outcome mode) @@ -73,12 +65,9 @@ export default function CompletedStudyRow(props) { return !!(progress?.checklist1Id && progress?.checklist2Id); }; - // Determine if row should be expandable (has PDFs or multiple outcomes) - const isExpandable = () => hasPdfs() || hasMultipleOutcomes(); - - // Handle row click - toggle unless clicking on interactive elements or selectable text + // Handle row click - only for PDF expansion const handleRowClick = e => { - if (!isExpandable()) return; + if (!hasPdfs()) return; const target = e.target; const interactive = target.closest('button, [role="button"], [data-selectable]'); if (interactive) return; @@ -89,12 +78,13 @@ export default function CompletedStudyRow(props) { <>
+ {/* Study header */}
- {/* Chevron indicator (only if expandable) */} - + {/* Chevron for PDFs */} +
{study().name}
- {/* Citation line - selectable */}

· {pdfCount()} PDFs - - - {' '} - · {completedOutcomeGroups().length} outcomes - -

- -

- {pdfCount()} PDFs - · - - {completedOutcomeGroups().length} outcomes - -

+ +

{pdfCount()} PDFs

- {/* First outcome badge (if has outcomeId) */} - - - {props.getOutcomeName?.(firstGroup().outcomeId) || 'Unknown Outcome'} - - - - {/* Multiple outcomes indicator */} - - - +{completedOutcomeGroups().length - 1} more - - + {/* Single outcome: inline badges + buttons */} + + + + {props.getOutcomeName?.(firstGroup().outcomeId) || 'Unknown Outcome'} + + - {/* Checklist type badge - selectable */} - {getChecklistMetadata(firstGroup()?.type)?.name || 'Checklist'} - - {/* Checklist status badge (single outcome mode) */} - {getStatusLabel(firstGroup()?.checklists[0]?.status)} - - {/* View Previous Reviewers button (single outcome mode, for dual-reviewer studies) */} - - - + + + - {/* Open checklist button (single outcome mode) */} -
- - {/* Multiple outcomes section */} - -
- - {outcomeGroup => ( - - )} - -
-
+ {/* Multi-outcome: stacked sub-rows (always visible) */} + +
+ + {outcomeGroup => ( + + )} + +
+
- {/* Expanded PDF Section */} + {/* Expandable PDF Section */} +
diff --git a/packages/web/src/components/project/reconcile-tab/ReconcileStudyRow.jsx b/packages/web/src/components/project/reconcile-tab/ReconcileStudyRow.jsx index 3a729d9d3..3490ee449 100644 --- a/packages/web/src/components/project/reconcile-tab/ReconcileStudyRow.jsx +++ b/packages/web/src/components/project/reconcile-tab/ReconcileStudyRow.jsx @@ -1,8 +1,10 @@ /** - * ReconcileStudyRow - Compact study row for the reconcile tab + * ReconcileStudyRow - Study row for the reconcile tab * - * Displays study info, reconciliation status, and collapsible PDF section. - * Supports multiple outcomes per study - shows each ready pair as a sub-row. + * Displays study info with stacked outcome sub-rows (always visible). + * Multi-outcome studies show READY/WAITING section headers. + * Single-outcome studies show inline reconcile controls. + * PDFs are expandable via chevron. */ import { For, Show, createMemo, createSignal } from 'solid-js'; @@ -10,6 +12,7 @@ import { BiRegularChevronRight } from 'solid-icons/bi'; import { BsFileDiff } from 'solid-icons/bs'; import { Collapsible, CollapsibleContent } from '@/components/ui/collapsible'; import { CHECKLIST_STATUS } from '@/constants/checklist-status.js'; +import { getChecklistMetadata } from '@/checklist-registry'; import { isReconciledChecklist, getReconciliationChecklistsByOutcome, @@ -17,19 +20,10 @@ import { import { PdfListItem } from '@pdf'; import ReconcileStatusTag from './ReconcileStatusTag.jsx'; -/** - * ReconcileStudyRow - Compact study row for the reconcile tab - * - * @param {Object} props - * @param {Object} props.study - Study object with pdfs and checklists arrays - * @param {Function} props.onReconcile - (checklist1Id, checklist2Id) => void - * @param {Function} props.onViewPdf - (pdf) => void - * @param {Function} props.onDownloadPdf - (pdf) => void - * @param {Function} props.getAssigneeName - (userId) => string - * @param {Function} props.getOutcomeName - (outcomeId) => string | null - * @returns {JSX.Element} - */ export default function ReconcileStudyRow(props) { + // props.study, props.onReconcile, props.onViewPdf, props.onDownloadPdf, + // props.getAssigneeName, props.getOutcomeName + const [expanded, setExpanded] = createSignal(false); const study = () => props.study; @@ -71,7 +65,6 @@ export default function ReconcileStudyRow(props) { return groups.filter(group => { if (group.checklists.length !== 2) return false; - // Check if there's already a finalized reconciled checklist for this outcome const hasFinalized = checklists.some( c => isReconciledChecklist(c) && @@ -89,41 +82,32 @@ export default function ReconcileStudyRow(props) { return reconciliationGroups().filter(g => g.checklists.length === 1); }); - // Check if any pair is ready const hasReadyPair = () => readyGroups().length > 0; - - // Get the first ready group for simple display const firstReadyGroup = () => readyGroups()[0] || null; - // Check if there are multiple outcomes const hasMultipleOutcomes = () => { - const ready = readyGroups(); - const waiting = waitingGroups(); - return ready.length + waiting.length > 1; + return readyGroups().length + waitingGroups().length > 1; }; - // Get reviewer name for a checklist const getReviewerName = checklist => { if (!checklist.assignedTo) return 'Unknown'; return props.getAssigneeName?.(checklist.assignedTo) || 'Unknown'; }; - // Get outcome name for a group const getOutcomeName = group => { if (!group.outcomeId) return null; return props.getOutcomeName?.(group.outcomeId) || 'Unknown Outcome'; }; - // Start reconciliation for a group const startReconciliationForGroup = group => { if (group.checklists.length === 2) { props.onReconcile?.(group.checklists[0].id, group.checklists[1].id); } }; - // Handle row click - toggle unless clicking on interactive elements or selectable text + // Handle row click -- only for PDF expansion const handleRowClick = e => { - if (!hasPdfs() && !hasMultipleOutcomes()) return; + if (!hasPdfs()) return; const target = e.target; const interactive = target.closest('button, [role="button"], [data-selectable]'); if (interactive) return; @@ -133,12 +117,13 @@ export default function ReconcileStudyRow(props) { return (
+ {/* Study header */}
- {/* Chevron indicator */} - + {/* Chevron for PDFs */} +
{study().name}
- {/* Citation line - selectable */}

- {/* Reconciliation status tag */} - - - {/* Outcome badge (for first ready group) */} - - - {getOutcomeName(firstReadyGroup())} - - + {/* Single outcome: inline controls */} + + - {/* Multiple outcomes indicator */} - - - +{readyGroups().length + waitingGroups().length - 1} more - - + + + {getOutcomeName(firstReadyGroup())} + + - {/* Reviewer info when ready (single outcome mode) */} - -
- - {checklist => {getReviewerName(checklist())}} - - vs - - {checklist => {getReviewerName(checklist())}} - -
-
+ +
+ + {checklist => {getReviewerName(checklist())}} + + vs + + {checklist => {getReviewerName(checklist())}} + +
+
- {/* Reconcile button (single outcome mode) */} - -
- - {/* Multiple outcome rows */} + + {/* Multi-outcome: summary count badges in header */} -
- - {group => ( -
-
- +
+ 0}> + + {readyGroups().length} ready + + + 0}> + + {waitingGroups().length} waiting + + +
+
+
+ + {/* Multi-outcome: stacked sub-rows with section headers (always visible) */} + +
+ {/* READY section */} + 0}> +
+ READY + ({readyGroups().length}) +
+
+
+ + {group => ( +
+
- {getOutcomeName(group)} + {getChecklistMetadata(group.type)?.name || group.type} + + + + {getOutcomeName(group)} + + + + {getReviewerName(group.checklists[0])}{' '} + vs{' '} + {getReviewerName(group.checklists[1])} - -
- {getReviewerName(group.checklists[0])} - vs - {getReviewerName(group.checklists[1])}
+
- -
- )} -
- {/* Waiting groups */} - - {group => ( -
-
- + )} + +
+ + + {/* WAITING section */} + 0}> +
0 ? 'mt-3' : ''}`}> + WAITING + ({waitingGroups().length}) +
+
+
+ + {group => ( +
+
- {getOutcomeName(group)} + {getChecklistMetadata(group.type)?.name || group.type} + + + + {getOutcomeName(group)} + + + + {getReviewerName(group.checklists[0])} -- waiting for second reviewer - -
- {getReviewerName(group.checklists[0])} - waiting for second reviewer
+ + Waiting +
- - Waiting - -
- )} -
-
- + )} + +
+
+
+ - {/* Expanded PDF Section */} + {/* Expandable PDF Section */} +
diff --git a/packages/web/src/components/project/todo-tab/ChecklistForm.jsx b/packages/web/src/components/project/todo-tab/ChecklistForm.jsx index d434a1bbd..012362b66 100644 --- a/packages/web/src/components/project/todo-tab/ChecklistForm.jsx +++ b/packages/web/src/components/project/todo-tab/ChecklistForm.jsx @@ -6,6 +6,7 @@ import { createSignal, createMemo, Show } from 'solid-js'; import { getChecklistTypeOptions, + getChecklistMetadata, DEFAULT_CHECKLIST_TYPE, CHECKLIST_TYPES, } from '@/checklist-registry'; @@ -124,25 +125,25 @@ export default function ChecklistForm(props) {
- {/* Warning messages for outcome issues */} - + {/* Warning: no outcomes defined (blocking) */} +
- -

All outcomes already used

-

- You already have a {type()} checklist for each available outcome. -

- - } - > -

No outcomes defined

-

- {type()} requires an outcome. Add outcomes in the All Studies tab first. -

-
+

No outcomes defined

+

+ {getChecklistMetadata(type())?.name || type()} requires an outcome. Add outcomes in the + All Studies tab first. +

+
+
+ + {/* Info: all outcomes covered (non-blocking) */} + 0}> +
+

All outcomes covered

+

+ You already have a {getChecklistMetadata(type())?.name || type()} checklist for each + available outcome. +

diff --git a/packages/web/src/components/project/todo-tab/TodoStudyRow.jsx b/packages/web/src/components/project/todo-tab/TodoStudyRow.jsx index 648947597..857dd5137 100644 --- a/packages/web/src/components/project/todo-tab/TodoStudyRow.jsx +++ b/packages/web/src/components/project/todo-tab/TodoStudyRow.jsx @@ -1,8 +1,9 @@ /** - * TodoStudyRow - Compact study card for the todo tab + * TodoStudyRow - Study card for the todo tab * * Displays study info, checklist status, and collapsible PDF section. - * Supports multiple checklists per study (one per outcome for ROB-2/ROBINS-I). + * Multiple checklists are shown as stacked sub-rows (always visible). + * Single checklists are shown inline in the header row. */ import { For, Show, createMemo, createSignal } from 'solid-js'; @@ -33,7 +34,7 @@ export default function TodoStudyRow(props) { // props.study: Study object with pdfs and checklists arrays // props.members: Array of project members // props.currentUserId: Current user's ID - // props.expanded: boolean - controlled expanded state + // props.expanded: boolean - controlled expanded state (for PDFs) // props.onToggleExpanded: () => void // props.onOpenChecklist: (checklistId) => void // props.onDeleteChecklist: (checklistId) => void @@ -129,27 +130,51 @@ export default function TodoStudyRow(props) { return `${author || 'Unknown'}${year ? ` (${year})` : ''}`; }; - // Determine if row should be expandable (has PDFs or multiple checklists) - const isExpandable = () => hasPdfs() || checklists().length > 1; + // PDFs are expandable via chevron + const isPdfExpandable = () => hasPdfs(); - // Handle row click - toggle unless clicking on interactive elements or selectable text + // Handle row click for PDF expansion only const handleRowClick = e => { - if (!isExpandable()) return; + if (!isPdfExpandable()) return; const target = e.target; const interactive = target.closest('button, [role="button"], [data-selectable]'); if (interactive) return; props.onToggleExpanded?.(); }; + // Add/Cancel button shared between single and multi modes + const AddCancelButton = () => ( + + + + ); + return (
+ {/* Study header row */}
- {/* Chevron indicator */} - + {/* Chevron for PDFs */} +
{study().name}
- {/* Citation line - selectable */}

· {pdfCount()} PDFs - 1}> - · {checklists().length} checklists -

- 1)}> -

- {pdfCount()} PDFs - 1}> · - 1}>{checklists().length} checklists -

+ +

{pdfCount()} PDFs

@@ -192,25 +209,22 @@ export default function TodoStudyRow(props) { const checklist = checklists()[0]; return ( <> - {/* Checklist type badge */} {getChecklistMetadata(checklist.type)?.name || 'Checklist'} - {/* Outcome badge if applicable */} {getOutcomeName(checklist.outcomeId)} - {/* Status badge */} - {/* Open button */} - {/* Delete button */} + {/* Add/Cancel button for single checklist mode */} + + -
- - {/* Multiple checklists list */} + {/* Multi-checklist: show Add button in header */} 1}> -
- - {checklist => ( -
-
+ + +
+ + {/* Stacked sub-rows for multiple checklists (always visible, no dropdown) */} + 1}> +
+ + {checklist => ( +
+
+ + {getChecklistMetadata(checklist.type)?.name || 'Checklist'} + + - {getChecklistMetadata(checklist.type)?.name || 'Checklist'} + {getOutcomeName(checklist.outcomeId)} - - - {getOutcomeName(checklist.outcomeId)} - - - - {getStatusLabel(checklist.status)} - -
-
- - -
+ + + {getStatusLabel(checklist.status)} +
- )} -
-
-
+ + +
+ )} +
+
+
- {/* Expanded PDF Section */} + {/* Expandable PDF Section */} +