Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/docs/guides/development-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,6 @@ pnpm install

## Resources

- [Contributing Guide](/.github/Contributing.md) - Detailed contribution guidelines
- [Architecture Diagrams](/architecture/) - System architecture
- [Error Handling Guide](/guides/error-handling) - Error handling patterns
- [Style Guide](/guides/style-guide) - UI/UX guidelines
Expand Down
21 changes: 21 additions & 0 deletions packages/landing/app.config.timestamp_1766853344755.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// app.config.js
import { defineConfig } from "@solidjs/start/config";
import tailwindcss from "@tailwindcss/vite";
var app_config_default = defineConfig({
vite: {
plugins: [tailwindcss()],
build: {
target: ["es2020", "safari14"]
}
},
server: {
preset: "static",
prerender: {
routes: ["/", "/about", "/contact", "/privacy", "/resources", "/security", "/terms"],
crawlLinks: true
}
}
});
export {
app_config_default as default
};
7 changes: 3 additions & 4 deletions packages/web/jsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@auth-ui/*": ["src/components/auth-ui/*"],
"@checklist-ui/*": ["src/components/checklist-ui/*"],
"@project-ui/*": ["src/components/project-ui/*"],
"@auth/*": ["src/components/auth/*"],
"@checklist/*": ["src/components/checklist/*"],
"@project/*": ["src/components/project/*"],
"@routes/*": ["src/routes/*"],
"@primitives/*": ["src/primitives/*"],
"@auth/*": ["src/components/auth-ui/*"],
"@offline/*": ["src/offline/*"],
"@api/*": ["src/api/*"],
"@config/*": ["src/config/*"],
Expand Down
47 changes: 47 additions & 0 deletions packages/web/src/AMSTAR2/checklist.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,53 @@ function getSelectedAnswer(answers, question) {
return null;
}

/**
* Check if an AMSTAR2 checklist is complete (all questions have final answers).
* A question has a final answer if the last column has at least one option selected.
*
* @param {Object} checklist - The checklist object to validate
* @returns {boolean} True if all questions have final answers, false otherwise
*/
export function isAMSTAR2Complete(checklist) {
if (!checklist || typeof checklist !== 'object') return false;

// All required AMSTAR2 questions
const requiredQuestions = [
'q1',
'q2',
'q3',
'q4',
'q5',
'q6',
'q7',
'q8',
'q9a',
'q9b',
'q10',
'q11a',
'q11b',
'q12',
'q13',
'q14',
'q15',
'q16',
];

// Check each required question has a final answer
for (const questionKey of requiredQuestions) {
const question = checklist[questionKey];
if (!question || !Array.isArray(question.answers)) return false;

// Check if the last column has at least one option selected
const lastCol = question.answers[question.answers.length - 1];
if (!Array.isArray(lastCol)) return false;
const hasAnswer = lastCol.some(v => v === true);
if (!hasAnswer) return false;
}

return true;
}

export function getAnswers(checklist) {
if (!checklist || typeof checklist !== 'object') return null;
const result = {};
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/Layout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createSignal, onMount } from 'solid-js';
import Navbar from './components/Navbar.jsx';
import Sidebar from './components/sidebar/Sidebar.jsx';
import { Toaster } from '@corates/ui';
import { ImpersonationBanner } from '@components/admin-ui/index.js';
import { ImpersonationBanner } from '@/components/admin/index.js';
import { isImpersonating } from '@/stores/adminStore.js';

const SIDEBAR_STORAGE_KEY = 'corates-sidebar-open';
Expand Down
30 changes: 15 additions & 15 deletions packages/web/src/Routes.jsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { Router, Route } from '@solidjs/router';
import Dashboard from './components/Dashboard.jsx';
import SignIn from '@auth-ui/SignIn.jsx';
import SignUp from '@auth-ui/SignUp.jsx';
import CheckEmail from '@auth-ui/CheckEmail.jsx';
import CompleteProfile from '@auth-ui/CompleteProfile.jsx';
import ResetPassword from '@auth-ui/ResetPassword.jsx';
import AuthLayout from '@auth-ui/AuthLayout.jsx';
import SignIn from '@/components/auth/SignIn.jsx';
import SignUp from '@/components/auth/SignUp.jsx';
import CheckEmail from '@/components/auth/CheckEmail.jsx';
import CompleteProfile from '@/components/auth/CompleteProfile.jsx';
import ResetPassword from '@/components/auth/ResetPassword.jsx';
import AuthLayout from '@/components/auth/AuthLayout.jsx';
import Layout from '@/Layout.jsx';
import ChecklistYjsWrapper from '@checklist-ui/ChecklistYjsWrapper.jsx';
import ReconciliationWrapper from '@/components/checklist-ui/compare/ReconciliationWrapper.jsx';
import ProjectView from '@project-ui/ProjectView.jsx';
import LocalChecklistView from '@checklist-ui/LocalChecklistView.jsx';
import ProfilePage from '@components/profile-ui/ProfilePage.jsx';
import SettingsPage from '@components/profile-ui/SettingsPage.jsx';
import ChecklistYjsWrapper from '@/components/checklist/ChecklistYjsWrapper.jsx';
import ReconciliationWrapper from '@/components/checklist/compare/ReconciliationWrapper.jsx';
import ProjectView from '@/components/project/ProjectView.jsx';
import LocalChecklistView from '@/components/checklist/LocalChecklistView.jsx';
import ProfilePage from '@/components/profile/ProfilePage.jsx';
import SettingsPage from '@/components/profile/SettingsPage.jsx';
import BillingPage from '@components/billing/BillingPage.jsx';
import NotFoundPage from '@components/NotFoundPage.jsx';
import { AdminDashboard } from '@components/admin-ui/index.js';
import StorageManagement from '@components/admin-ui/StorageManagement.jsx';
import { AdminDashboard } from '@/components/admin/index.js';
import StorageManagement from '@/components/admin/StorageManagement.jsx';
import { BASEPATH } from '@config/api.js';
import ProtectedGuard from '@/components/auth-ui/ProtectedGuard.jsx';
import ProtectedGuard from '@/components/auth/ProtectedGuard.jsx';

export default function AppRoutes() {
return (
Expand Down
4 changes: 2 additions & 2 deletions packages/web/src/components/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Show } from 'solid-js';
import ProjectDashboard from '@project-ui/ProjectDashboard.jsx';
import ChecklistsDashboard from '@checklist-ui/ChecklistsDashboard.jsx';
import ProjectDashboard from '@/components/project/ProjectDashboard.jsx';
import ChecklistsDashboard from '@/components/checklist/ChecklistsDashboard.jsx';
import { useBetterAuth } from '@api/better-auth-store.js';
import { API_BASE } from '@config/api.js';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { AMSTAR_CHECKLIST } from '@/AMSTAR2/checklist-map.js';
import { createChecklist as createAMSTAR2Checklist } from '@/AMSTAR2/checklist.js';
import { FaSolidCircleInfo } from 'solid-icons/fa';
import { Tooltip } from '@corates/ui';
import NoteEditor from '@checklist-ui/common/NoteEditor.jsx';
import NoteEditor from '@/components/checklist/common/NoteEditor.jsx';

export function Question1(props) {
const state = () => props.checklistState().q1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
* Supports multiple checklist types via the GenericChecklist component.
*/

import GenericChecklist from '@checklist-ui/GenericChecklist.jsx';
import PdfViewer from '@/components/checklist-ui/pdf/PdfViewer.jsx';
import SplitScreenLayout from '@checklist-ui/SplitScreenLayout.jsx';
import GenericChecklist from '@/components/checklist/GenericChecklist.jsx';
import PdfViewer from '@/components/checklist/pdf/PdfViewer.jsx';
import SplitScreenLayout from '@/components/checklist/SplitScreenLayout.jsx';

export default function ChecklistWithPdf(props) {
// props.checklistType - the type of checklist ('AMSTAR2', 'ROBINS_I', etc.)
Expand All @@ -24,6 +24,7 @@ export default function ChecklistWithPdf(props) {
// props.selectedPdfId - currently selected PDF ID
// props.onPdfSelect - handler for PDF selection change
// props.getQuestionNote - function to get Y.Text for a question note
// props.pdfUrl - optional PDF URL (for server-hosted PDFs)

return (
<div class='flex h-full flex-col bg-blue-50'>
Expand All @@ -33,6 +34,8 @@ export default function ChecklistWithPdf(props) {
defaultRatio={50}
showSecondPanel={false}
headerContent={props.headerContent}
pdfUrl={props.pdfUrl}
pdfData={props.pdfData}
>
{/* First panel: Checklist (type-aware) */}
<GenericChecklist
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import { createSignal, createEffect, createMemo, Show } from 'solid-js';
import { useParams, useNavigate } from '@solidjs/router';
import ChecklistWithPdf from '@checklist-ui/ChecklistWithPdf.jsx';
import { useParams, useNavigate, useLocation } from '@solidjs/router';
import ChecklistWithPdf from '@/components/checklist/ChecklistWithPdf.jsx';
import useProject from '@/primitives/useProject/index.js';
import projectStore from '@/stores/projectStore.js';
import { ACCESS_DENIED_ERRORS } from '@/constants/errors.js';
import { CHECKLIST_STATUS, isEditable } from '@/constants/checklist-status.js';
import { getNextStatusForCompletion } from '@/lib/checklist-domain.js';
import { downloadPdf, uploadPdf, deletePdf } from '@api/pdf-api.js';
import { downloadPdf, uploadPdf, deletePdf, getPdfUrl } from '@api/pdf-api.js';
import { getCachedPdf, cachePdf } from '@primitives/pdfCache.js';
import { showToast, useConfirmDialog } from '@corates/ui';
import { useBetterAuth } from '@api/better-auth-store.js';
import { getChecklistTypeFromState, scoreChecklistOfType } from '@/checklist-registry';
import { IoChevronBack } from 'solid-icons/io';
import ScoreTag from '@/components/checklist-ui/ScoreTag.jsx';
import ScoreTag from '@/components/checklist/ScoreTag.jsx';
import { isAMSTAR2Complete } from '@/AMSTAR2/checklist.js';

export default function ChecklistYjsWrapper() {
const params = useParams();
const navigate = useNavigate();
const location = useLocation();
const { user } = useBetterAuth();
const confirmDialog = useConfirmDialog();

Expand Down Expand Up @@ -262,6 +264,15 @@ export default function ChecklistYjsWrapper() {
return;
}

// Safety check: validate checklist before allowing completion
if (!isChecklistValid()) {
showToast.error(
'Incomplete Checklist',
'All questions must have a final answer before marking the checklist as complete.',
);
return;
}

// Show confirmation dialog before marking complete
const confirmed = await confirmDialog.open({
title: 'Mark Checklist as Complete?',
Expand Down Expand Up @@ -303,23 +314,31 @@ export default function ChecklistYjsWrapper() {
return scoreChecklistOfType(type, checklist);
});

// Determine back button navigation based on checklist status
const getBackTab = () => {
const checklist = currentChecklist();
const study = currentStudy();
if (!checklist) return 'todo';
// Validate checklist completion - only for AMSTAR2 checklists
const isChecklistValid = createMemo(() => {
const type = checklistType();
const checklist = checklistForUI();

if (checklist.status === CHECKLIST_STATUS.COMPLETED) {
// Completed checklist: navigate to appropriate tab
const isSingleReviewer = study?.reviewer1 && !study?.reviewer2;
return isSingleReviewer ? 'completed' : 'reconcile';
}
// For non-AMSTAR2 checklists, allow completion (no validation)
if (type !== 'AMSTAR2') return true;

if (checklist.status === CHECKLIST_STATUS.AWAITING_RECONCILE) {
return 'reconcile';
}
// For AMSTAR2, check if all questions have final answers
if (!checklist) return false;
return isAMSTAR2Complete(checklist);
});

// Generate PDF URL for opening in new tab
const pdfUrl = createMemo(() => {
const fileName = pdfFileName();
if (!fileName) return null;
return getPdfUrl(params.projectId, params.studyId, fileName);
});

return 'todo';
// Determine back button navigation from tab query param
const getBackTab = () => {
// console.log('location', location.search);
const tabFromUrl = new URLSearchParams(location.search).get('tab');
return tabFromUrl || 'overview';
};

// Header content for the split screen toolbar (left side)
Expand Down Expand Up @@ -357,9 +376,16 @@ export default function ChecklistYjsWrapper() {
>
<button
onClick={handleToggleComplete}
disabled={!isChecklistValid()}
title={
!isChecklistValid() ?
'All questions must have a final answer before marking complete'
: undefined
}
class={`rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${
currentChecklist()?.status === CHECKLIST_STATUS.COMPLETED ?
'bg-green-100 text-green-700 hover:bg-green-200'
: !isChecklistValid() ? 'cursor-not-allowed bg-gray-300 text-gray-500 opacity-60'
: 'bg-blue-600 text-white hover:bg-blue-700'
}`}
>
Expand Down Expand Up @@ -396,6 +422,7 @@ export default function ChecklistYjsWrapper() {
headerContent={headerContent}
pdfData={pdfData()}
pdfFileName={pdfFileName()}
pdfUrl={pdfUrl()}
onPdfChange={handlePdfChange}
readOnly={isReadOnly()}
allowDelete={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
DEFAULT_CHECKLIST_TYPE,
CHECKLIST_TYPES,
} from '@/checklist-registry';
import AMSTAR2Checklist from '@checklist-ui/AMSTAR2Checklist.jsx';
import { ROBINSIChecklist } from '@checklist-ui/ROBINSIChecklist/index.js';
import AMSTAR2Checklist from '@/components/checklist/AMSTAR2Checklist.jsx';
import { ROBINSIChecklist } from '@/components/checklist/ROBINSIChecklist/index.js';

/**
* GenericChecklist Component
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@

import { createSignal, createEffect, Show, onCleanup, createMemo } from 'solid-js';
import { useParams, useNavigate } from '@solidjs/router';
import ChecklistWithPdf from '@checklist-ui/ChecklistWithPdf.jsx';
import CreateLocalChecklist from '@checklist-ui/CreateLocalChecklist.jsx';
import ChecklistWithPdf from '@/components/checklist/ChecklistWithPdf.jsx';
import CreateLocalChecklist from '@/components/checklist/CreateLocalChecklist.jsx';
import useLocalChecklists from '@primitives/useLocalChecklists.js';
import { getChecklistTypeFromState, scoreChecklistOfType } from '@/checklist-registry';
import { IoChevronBack } from 'solid-icons/io';
import ScoreTag from '@/components/checklist-ui/ScoreTag.jsx';
import ScoreTag from '@/components/checklist/ScoreTag.jsx';

export default function LocalChecklistView() {
const params = useParams();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,32 +47,42 @@ function getTooltipContent(checklistType) {
* @param {Object} props
* @param {string} props.currentScore - The current score value
* @param {string} [props.checklistType] - The checklist type (defaults to AMSTAR2)
* @param {boolean} [props.showRatingOnly] - Whether to only show the rating text and info icon
*/
export default function ScoreTag(props) {
const showRatingOnly = () => props.showRatingOnly ?? false;
const checklistType = () => props.checklistType || DEFAULT_CHECKLIST_TYPE;

const styleClass = createMemo(() => getScoreStyle(props.currentScore, checklistType()));
const infoUrl = createMemo(() => getInfoUrl(checklistType()));
const tooltipContent = createMemo(() => getTooltipContent(checklistType()));

return (
<Show when={props.currentScore}>
<span
class={`inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-medium ${styleClass()}`}
>
<span>Score: {props.currentScore}</span>
<Tooltip content={tooltipContent()} placement='bottom' openDelay={200}>
<a
href={infoUrl()}
target='_blank'
rel='noreferrer'
class='mt-0.5 inline-flex items-center justify-center rounded-full p-0.5 opacity-70 hover:opacity-100 focus:opacity-100 focus:ring-2 focus:ring-blue-500 focus:outline-none'
aria-label={`Open ${getChecklistMetadata(checklistType()).name} guidance in a new tab`}
>
<FaSolidCircleInfo size={12} />
</a>
</Tooltip>
<Show when={!showRatingOnly()} fallback={<span>{props.currentScore}</span>}>
<span>Rating: {props.currentScore}</span>
<ScoreTooltip checklistType={checklistType()} />
</Show>
</span>
</Show>
);
}

export function ScoreTooltip(props) {
const infoUrl = createMemo(() => getInfoUrl(props.checklistType));
const tooltipContent = createMemo(() => getTooltipContent(props.checklistType));

return (
<Tooltip content={tooltipContent()} placement='bottom' openDelay={200}>
<a
href={infoUrl()}
target='_blank'
rel='noreferrer'
class='mt-0.5 inline-flex items-center justify-center rounded-full p-0.5 opacity-70 hover:opacity-100 focus:opacity-100 focus:ring-2 focus:ring-blue-500 focus:outline-none'
aria-label={`Open ${getChecklistMetadata(props.checklistType).name} guidance in a new tab`}
>
<FaSolidCircleInfo size={12} />
</a>
</Tooltip>
);
}
Comment on lines +71 to +88
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix reactivity issue in aria-label.

Line 82 directly accesses props.checklistType inside an expression, which breaks SolidJS reactivity. Prop access in JSX must be wrapped in a function or computed via a memo.

Proposed fix

Create a memo for the metadata and use it in the aria-label:

 export function ScoreTooltip(props) {
   const infoUrl = createMemo(() => getInfoUrl(props.checklistType));
   const tooltipContent = createMemo(() => getTooltipContent(props.checklistType));
+  const metadata = createMemo(() => getChecklistMetadata(props.checklistType));
 
   return (
     <Tooltip content={tooltipContent()} placement='bottom' openDelay={200}>
       <a
         href={infoUrl()}
         target='_blank'
         rel='noreferrer'
         class='mt-0.5 inline-flex items-center justify-center rounded-full p-0.5 opacity-70 hover:opacity-100 focus:opacity-100 focus:ring-2 focus:ring-blue-500 focus:outline-none'
-        aria-label={`Open ${getChecklistMetadata(props.checklistType).name} guidance in a new tab`}
+        aria-label={`Open ${metadata().name} guidance in a new tab`}
       >
         <FaSolidCircleInfo size={12} />
       </a>
     </Tooltip>
   );
 }
🤖 Prompt for AI Agents
In packages/web/src/components/checklist/ScoreTag.jsx around lines 71 to 88, the
aria-label accesses props.checklistType directly which breaks SolidJS
reactivity; wrap the derived metadata in a memo and use that memo in the
aria-label. Create a createMemo (e.g., const metadata = createMemo(() =>
getChecklistMetadata(props.checklistType))) alongside the existing memos and
replace getChecklistMetadata(props.checklistType).name usage with
metadata().name so the aria-label reads the reactive memo value rather than
directly accessing the prop.

Loading