From ef3850edf96bf2d71ca423679c5a9547a3a21f88 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Thu, 5 Dec 2024 12:11:30 -0500 Subject: [PATCH 1/8] chore: fix issue with storybook import of runtime version --- apps/playground/package.json | 2 +- apps/web/package.json | 2 +- pnpm-lock.yaml | 28 ++++++++++++++-------------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/playground/package.json b/apps/playground/package.json index 149b51b21..a36448318 100644 --- a/apps/playground/package.json +++ b/apps/playground/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "scripts": { "build": "tsc && env-cmd -f ../../.env pnpm exec vite build", - "deploy": "rsync -r dist/* douglasneuroinformatics.ca:/home/unrjos/www/playground.opendatacapture.org", + "deploy": "rsync -r dist/* unrjos@cloud1.douglasneuroinformatics.ca:/home/unrjos/www/playground.opendatacapture.org", "dev": "NODE_ENV=development env-cmd -f ../../.env pnpm exec vite", "format": "prettier --write src", "lint": "tsc && eslint --fix src", diff --git a/apps/web/package.json b/apps/web/package.json index c59afa34b..f4f8d2604 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,7 +12,7 @@ "format:translations": "find src/translations -name '*.json' -exec pnpm exec sort-json {} \\;", "inject": "pnpm exec import-meta-env -x .env.public -p dist/index.html", "lint": "tsc && eslint --fix src", - "storybook": "env-cmd -f ../../.env storybook dev -p 6006", + "storybook": "NODE_OPTIONS='--import=tsx' env-cmd -f ../../.env storybook dev -p 6006", "test": "env-cmd -f ../../.env vitest" }, "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dcbde3f79..d4c2bcacc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,7 +35,7 @@ catalogs: version: 0.2.0 '@douglasneuroinformatics/libui': specifier: latest - version: 3.7.0 + version: 3.7.3 '@douglasneuroinformatics/libui-form-types': specifier: latest version: 0.11.0 @@ -373,7 +373,7 @@ importers: version: 0.0.2 '@douglasneuroinformatics/libui': specifier: 'catalog:' - version: 3.7.0(@types/react@18.3.12)(immer@10.1.1)(react-dom@vendor+react-dom@18.x)(react@vendor+react@18.x)(tailwindcss@3.4.14)(typescript@5.5.4)(zod@vendor+zod@3.23.x) + version: 3.7.3(@types/react@18.3.12)(immer@10.1.1)(react-dom@vendor+react-dom@18.x)(react@vendor+react@18.x)(tailwindcss@3.4.14)(typescript@5.5.4)(zod@vendor+zod@3.23.x) '@opendatacapture/instrument-renderer': specifier: workspace:* version: link:../../packages/instrument-renderer @@ -473,7 +473,7 @@ importers: dependencies: '@douglasneuroinformatics/libui': specifier: 'catalog:' - version: 3.7.0(@types/react@18.3.12)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.14)(typescript@5.5.4)(zod@3.23.8) + version: 3.7.3(@types/react@18.3.12)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.14)(typescript@5.5.4)(zod@3.23.8) '@opendatacapture/schemas': specifier: workspace:* version: link:../../packages/schemas @@ -546,7 +546,7 @@ importers: version: 1.0.2(typescript@5.5.4) '@douglasneuroinformatics/libui': specifier: 'catalog:' - version: 3.7.0(@types/react@18.3.12)(immer@10.1.1)(react-dom@vendor+react-dom@18.x)(react@vendor+react@18.x)(tailwindcss@3.4.14)(typescript@5.5.4)(zod@vendor+zod@3.23.x) + version: 3.7.3(@types/react@18.3.12)(immer@10.1.1)(react-dom@vendor+react-dom@18.x)(react@vendor+react@18.x)(tailwindcss@3.4.14)(typescript@5.5.4)(zod@vendor+zod@3.23.x) '@monaco-editor/react': specifier: ^4.6.0 version: 4.6.0(monaco-editor@0.51.0)(react-dom@vendor+react-dom@18.x)(react@vendor+react@18.x) @@ -661,7 +661,7 @@ importers: version: 0.0.3(typescript@5.5.4) '@douglasneuroinformatics/libui': specifier: 'catalog:' - version: 3.7.0(@types/react@18.3.12)(immer@10.1.1)(react-dom@vendor+react-dom@18.x)(react@vendor+react@18.x)(tailwindcss@3.4.14)(typescript@5.5.4)(zod@vendor+zod@3.23.x) + version: 3.7.3(@types/react@18.3.12)(immer@10.1.1)(react-dom@vendor+react-dom@18.x)(react@vendor+react@18.x)(tailwindcss@3.4.14)(typescript@5.5.4)(zod@vendor+zod@3.23.x) '@heroicons/react': specifier: ^2.1.5 version: 2.1.5(react@vendor+react@18.x) @@ -913,7 +913,7 @@ importers: version: 1.0.2(typescript@5.5.4) '@douglasneuroinformatics/libui': specifier: 'catalog:' - version: 3.7.0(@types/react@18.3.12)(immer@10.1.1)(react-dom@vendor+react-dom@18.x)(react@vendor+react@18.x)(tailwindcss@3.4.14)(typescript@5.5.4)(zod@vendor+zod@3.23.x) + version: 3.7.3(@types/react@18.3.12)(immer@10.1.1)(react-dom@vendor+react-dom@18.x)(react@vendor+react@18.x)(tailwindcss@3.4.14)(typescript@5.5.4)(zod@vendor+zod@3.23.x) '@opendatacapture/evaluate-instrument': specifier: workspace:* version: link:../evaluate-instrument @@ -1024,7 +1024,7 @@ importers: version: 1.0.2(typescript@5.5.4) '@douglasneuroinformatics/libui': specifier: 'catalog:' - version: 3.7.0(@types/react@18.3.12)(immer@10.1.1)(react-dom@vendor+react-dom@18.x)(react@vendor+react@18.x)(tailwindcss@3.4.14)(typescript@5.5.4)(zod@vendor+zod@3.23.x) + version: 3.7.3(@types/react@18.3.12)(immer@10.1.1)(react-dom@vendor+react-dom@18.x)(react@vendor+react@18.x)(tailwindcss@3.4.14)(typescript@5.5.4)(zod@vendor+zod@3.23.x) '@opendatacapture/runtime-core': specifier: workspace:* version: link:../runtime-core @@ -2071,9 +2071,9 @@ packages: resolution: { integrity: sha512-erds8oNXFrWSJfCglR8S7I3Yfkgx2Vz6RIQTa5OFtVAVx8DTSFf5FbnHpp49l6BcQ4FCU5w/PLO5NWdx08cNUg== } - '@douglasneuroinformatics/libui@3.7.0': + '@douglasneuroinformatics/libui@3.7.3': resolution: - { integrity: sha512-Ai66Qnn1Dt2RbICAIfIQVOg8SE3GMddqbH37OJHx3vvplm0Qwm82hpPd6IweK1NHpC9Fl9xc61l/wcVBXqiWbQ== } + { integrity: sha512-ThcNOM5zE2e3iNYlEOtC+gDFHJovFdhCAp2K+uzIUYcSP1FbY/GogtCnO/3qJFNLdcaNKF42lldjDdbfdp4idw== } peerDependencies: react: ^18.2.0 react-dom: ^18.2.0 @@ -12801,7 +12801,7 @@ snapshots: '@douglasneuroinformatics/libjs@0.8.0(typescript@5.5.4)': dependencies: - type-fest: 4.26.1 + type-fest: 4.28.0 typescript: 5.5.4 '@douglasneuroinformatics/libjs@1.0.2(typescript@5.5.4)': @@ -12876,7 +12876,7 @@ snapshots: dependencies: type-fest: 4.26.1 - '@douglasneuroinformatics/libui@3.7.0(@types/react@18.3.12)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.14)(typescript@5.5.4)(zod@3.23.8)': + '@douglasneuroinformatics/libui@3.7.3(@types/react@18.3.12)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.14)(typescript@5.5.4)(zod@3.23.8)': dependencies: '@douglasneuroinformatics/libjs': 0.8.0(typescript@5.5.4) '@douglasneuroinformatics/libui-form-types': 0.11.0 @@ -12919,7 +12919,7 @@ snapshots: tailwindcss: 3.4.14 tailwindcss-animate: 1.0.7(tailwindcss@3.4.14) ts-pattern: 5.5.0 - type-fest: 4.26.1 + type-fest: 4.28.0 vaul: 0.9.9(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) zod: 3.23.8 zustand: 4.5.5(@types/react@18.3.12)(immer@10.1.1)(react@18.3.1) @@ -12930,7 +12930,7 @@ snapshots: - immer - typescript - '@douglasneuroinformatics/libui@3.7.0(@types/react@18.3.12)(immer@10.1.1)(react-dom@vendor+react-dom@18.x)(react@vendor+react@18.x)(tailwindcss@3.4.14)(typescript@5.5.4)(zod@vendor+zod@3.23.x)': + '@douglasneuroinformatics/libui@3.7.3(@types/react@18.3.12)(immer@10.1.1)(react-dom@vendor+react-dom@18.x)(react@vendor+react@18.x)(tailwindcss@3.4.14)(typescript@5.5.4)(zod@vendor+zod@3.23.x)': dependencies: '@douglasneuroinformatics/libjs': 0.8.0(typescript@5.5.4) '@douglasneuroinformatics/libui-form-types': 0.11.0 @@ -12973,7 +12973,7 @@ snapshots: tailwindcss: 3.4.14 tailwindcss-animate: 1.0.7(tailwindcss@3.4.14) ts-pattern: 5.5.0 - type-fest: 4.26.1 + type-fest: 4.28.0 vaul: 0.9.9(@types/react@18.3.12)(react-dom@vendor+react-dom@18.x)(react@vendor+react@18.x) zod: link:vendor/zod@3.23.x zustand: 4.5.5(@types/react@18.3.12)(immer@10.1.1)(react@vendor+react@18.x) From a1e080d7c2cfe99cc689cc0971548f3397802cb7 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Thu, 5 Dec 2024 12:14:54 -0500 Subject: [PATCH 2/8] feat: implement client details --- .../src/instruments/instruments.service.ts | 3 +- .../examples/form/Form-Reference/index.ts | 6 +- .../form/Form-With-Computed-Measures/index.ts | 6 +- .../examples/form/Form-With-Groups/index.ts | 12 +- .../index.ts | 12 +- .../interactive/Interactive-With-CSS/index.ts | 6 +- .../Interactive-With-JSPsych/index.ts | 6 +- .../Interactive-With-Legacy-Script/index.ts | 6 +- .../Interactive-With-React/index.tsx | 6 +- .../Interactive-With-Vanilla/index.ts | 6 +- .../Multilingual-Interactive/index.ts | 12 +- .../templates/form/Multilingual-Form/index.ts | 12 +- .../templates/form/Unilingual-Form/index.ts | 6 +- .../Interactive-Instrument/index.ts | 7 +- .../InstrumentCard/InstrumentCard.stories.tsx | 2 +- .../InstrumentCard/InstrumentCard.tsx | 231 ++++++++++-------- .../src/__tests__/repositories/form/index.ts | 6 +- .../repositories/interactive/index.tsx | 6 +- .../brief-psychiatric-rating-scale/index.ts | 12 +- .../index.ts | 13 +- .../src/forms/general-consent-form/index.ts | 4 +- .../mini-mental-state-examination/index.ts | 13 +- .../montreal-cognitive-assessment/index.ts | 12 +- .../patient-health-questionnaire-9/index.ts | 13 +- .../src/interactive/breakout-task/index.ts | 6 +- .../src/interactive/stroop-task/index.tsx | 6 +- .../index.ts | 10 + packages/instrument-stubs/src/forms.js | 8 +- .../src/__tests__/define.test-d.ts | 32 ++- packages/runtime-core/src/define.d.ts | 25 +- .../src/types/instrument.base.d.ts | 63 +++-- .../schemas/src/instrument/instrument.base.ts | 25 +- 32 files changed, 370 insertions(+), 223 deletions(-) diff --git a/apps/api/src/instruments/instruments.service.ts b/apps/api/src/instruments/instruments.service.ts index caff938f5..142411d6e 100644 --- a/apps/api/src/instruments/instruments.service.ts +++ b/apps/api/src/instruments/instruments.service.ts @@ -164,8 +164,9 @@ export class InstrumentsService { options: EntityOperationOptions = {} ): Promise { const instances = await this.find(query, options); - return instances.map(({ __runtimeVersion, details, id, kind, language, tags }) => ({ + return instances.map(({ __runtimeVersion, clientDetails, details, id, kind, language, tags }) => ({ __runtimeVersion, + clientDetails, details, id, kind, diff --git a/apps/playground/src/instruments/examples/form/Form-Reference/index.ts b/apps/playground/src/instruments/examples/form/Form-Reference/index.ts index 6557ff278..b017e46e3 100644 --- a/apps/playground/src/instruments/examples/form/Form-Reference/index.ts +++ b/apps/playground/src/instruments/examples/form/Form-Reference/index.ts @@ -188,11 +188,13 @@ export default defineInstrument({ } } ], + clientDetails: { + estimatedDuration: 5, + instructions: ['Please complete all questions'] + }, details: { description: 'This example includes all possible static field variants', title: 'Reference Instrument', - estimatedDuration: 5, - instructions: ['Please complete all questions'], license: 'Apache-2.0' }, measures: {}, diff --git a/apps/playground/src/instruments/examples/form/Form-With-Computed-Measures/index.ts b/apps/playground/src/instruments/examples/form/Form-With-Computed-Measures/index.ts index 76faeeb1d..e167c83bb 100644 --- a/apps/playground/src/instruments/examples/form/Form-With-Computed-Measures/index.ts +++ b/apps/playground/src/instruments/examples/form/Form-With-Computed-Measures/index.ts @@ -33,10 +33,12 @@ export default defineInstrument({ variant: 'slider' } }, + clientDetails: { + estimatedDuration: 1, + instructions: ['Please respond to all questions'] + }, details: { description: 'This is an example of a form with computed measures', - estimatedDuration: 1, - instructions: ['Please respond to all questions'], license: 'Apache-2.0', title: 'Happiness Questionnaire' }, diff --git a/apps/playground/src/instruments/examples/form/Form-With-Groups/index.ts b/apps/playground/src/instruments/examples/form/Form-With-Groups/index.ts index 1afdd7702..06dcd5309 100644 --- a/apps/playground/src/instruments/examples/form/Form-With-Groups/index.ts +++ b/apps/playground/src/instruments/examples/form/Form-With-Groups/index.ts @@ -91,15 +91,17 @@ export default defineInstrument({ } } ], - details: { - description: { - en: 'This is an example of a multilingual grouped form', - fr: 'Voici un exemple de formulaire groupé multilingue' - }, + clientDetails: { estimatedDuration: 1, instructions: { en: ['Please respond to all questions'], fr: ['Veuillez répondre à toutes les questions'] + } + }, + details: { + description: { + en: 'This is an example of a multilingual grouped form', + fr: 'Voici un exemple de formulaire groupé multilingue' }, license: 'Apache-2.0', title: { diff --git a/apps/playground/src/instruments/examples/form/Multilingual-Form-With-Dynamic-Field/index.ts b/apps/playground/src/instruments/examples/form/Multilingual-Form-With-Dynamic-Field/index.ts index 5f6578021..cc17b316b 100644 --- a/apps/playground/src/instruments/examples/form/Multilingual-Form-With-Dynamic-Field/index.ts +++ b/apps/playground/src/instruments/examples/form/Multilingual-Form-With-Dynamic-Field/index.ts @@ -53,15 +53,17 @@ export default defineInstrument({ } } }, - details: { - description: { - en: 'This is an example of a simple form with conditional rendering and validation logic', - fr: 'Voici un exemple de formulaire simple avec un rendu conditionnel et une logique de validation' - }, + clientDetails: { estimatedDuration: 1, instructions: { en: ['Please respond to all questions'], fr: ['Veuillez répondre à toutes les questions'] + } + }, + details: { + description: { + en: 'This is an example of a simple form with conditional rendering and validation logic', + fr: 'Voici un exemple de formulaire simple avec un rendu conditionnel et une logique de validation' }, license: 'Apache-2.0', title: { diff --git a/apps/playground/src/instruments/examples/interactive/Interactive-With-CSS/index.ts b/apps/playground/src/instruments/examples/interactive/Interactive-With-CSS/index.ts index 94d3eec91..37f308932 100644 --- a/apps/playground/src/instruments/examples/interactive/Interactive-With-CSS/index.ts +++ b/apps/playground/src/instruments/examples/interactive/Interactive-With-CSS/index.ts @@ -31,10 +31,12 @@ export default defineInstrument({ }); } }, + clientDetails: { + estimatedDuration: 1, + instructions: [''] + }, details: { description: '', - estimatedDuration: 1, - instructions: [''], license: 'UNLICENSED', title: '' }, diff --git a/apps/playground/src/instruments/examples/interactive/Interactive-With-JSPsych/index.ts b/apps/playground/src/instruments/examples/interactive/Interactive-With-JSPsych/index.ts index a8557c044..54567ff15 100644 --- a/apps/playground/src/instruments/examples/interactive/Interactive-With-JSPsych/index.ts +++ b/apps/playground/src/instruments/examples/interactive/Interactive-With-JSPsych/index.ts @@ -12,6 +12,10 @@ import orange from './orange.png'; import '/runtime/v1/jspsych@8.x/css/jspsych.css'; export default defineInstrument({ + clientDetails: { + estimatedDuration: 1, + instructions: ['The user will be displayed instructions on the screen'] + }, content: { async render(done) { const jsPsych = initJsPsych({ @@ -116,8 +120,6 @@ export default defineInstrument({ }, details: { description: 'This reaction time task is a non-trivial proof of concept with jspsych.', - estimatedDuration: 1, - instructions: ['The user will be displayed instructions on the screen'], license: 'MIT', title: 'Reaction Time Task' }, diff --git a/apps/playground/src/instruments/examples/interactive/Interactive-With-Legacy-Script/index.ts b/apps/playground/src/instruments/examples/interactive/Interactive-With-Legacy-Script/index.ts index ed3442858..e6df80772 100644 --- a/apps/playground/src/instruments/examples/interactive/Interactive-With-Legacy-Script/index.ts +++ b/apps/playground/src/instruments/examples/interactive/Interactive-With-Legacy-Script/index.ts @@ -23,10 +23,12 @@ export default defineInstrument({ }); } }, + clientDetails: { + estimatedDuration: 1, + instructions: ['Please complete the task.'] + }, details: { description: 'This is an example of how ancient scripts, that fail in strict mode, can be used in an instrument.', - estimatedDuration: 1, - instructions: ['Please complete the task.'], license: 'Apache-2.0', title: 'Interactive Instrument With Legacy Script' }, diff --git a/apps/playground/src/instruments/examples/interactive/Interactive-With-React/index.tsx b/apps/playground/src/instruments/examples/interactive/Interactive-With-React/index.tsx index b2d6ed4e5..d6a67ec54 100644 --- a/apps/playground/src/instruments/examples/interactive/Interactive-With-React/index.tsx +++ b/apps/playground/src/instruments/examples/interactive/Interactive-With-React/index.tsx @@ -7,6 +7,10 @@ import { App } from './App.tsx'; import './index.css'; export default defineInstrument({ + clientDetails: { + estimatedDuration: 1, + instructions: ['Please submit whatever count you want!'] + }, content: { render(done) { const rootElement = document.createElement('div'); @@ -17,8 +21,6 @@ export default defineInstrument({ }, details: { description: 'This task is completely useless. It is a proof of concept for an interactive instrument.', - estimatedDuration: 1, - instructions: ['Please submit whatever count you want!'], license: 'Apache-2.0', title: 'React Click Task' }, diff --git a/apps/playground/src/instruments/examples/interactive/Interactive-With-Vanilla/index.ts b/apps/playground/src/instruments/examples/interactive/Interactive-With-Vanilla/index.ts index 338d2032c..a727bdfb9 100644 --- a/apps/playground/src/instruments/examples/interactive/Interactive-With-Vanilla/index.ts +++ b/apps/playground/src/instruments/examples/interactive/Interactive-With-Vanilla/index.ts @@ -4,6 +4,10 @@ import { z } from '/runtime/v1/zod@3.23.x'; import './styles.css'; export default defineInstrument({ + clientDetails: { + estimatedDuration: 1, + instructions: ['Please attempt to win the game as quickly as possible.'] + }, content: { render(done) { const startTime = Date.now(); @@ -185,8 +189,6 @@ export default defineInstrument({ authors: ['Andrzej Mazur', 'Mozilla Contributors', 'Joshua Unrau'], description: 'This is a very simple interactive instrument, adapted from a 2D breakout game in the Mozilla documentation.', - estimatedDuration: 1, - instructions: ['Please attempt to win the game as quickly as possible.'], license: 'CC0-1.0', referenceUrl: 'https://developer.mozilla.org/en-US/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript', sourceUrl: 'https://github.com/end3r/Gamedev-Canvas-workshop/tree/gh-pages', diff --git a/apps/playground/src/instruments/examples/interactive/Multilingual-Interactive/index.ts b/apps/playground/src/instruments/examples/interactive/Multilingual-Interactive/index.ts index b5f902c18..0eab5b659 100644 --- a/apps/playground/src/instruments/examples/interactive/Multilingual-Interactive/index.ts +++ b/apps/playground/src/instruments/examples/interactive/Multilingual-Interactive/index.ts @@ -42,15 +42,17 @@ export default defineInstrument({ }); } }, - details: { - description: { - en: '', - fr: '' - }, + clientDetails: { estimatedDuration: 1, instructions: { en: [''], fr: [''] + } + }, + details: { + description: { + en: '', + fr: '' }, license: 'UNLICENSED', title: { diff --git a/apps/playground/src/instruments/templates/form/Multilingual-Form/index.ts b/apps/playground/src/instruments/templates/form/Multilingual-Form/index.ts index 13734e587..1c907b49e 100644 --- a/apps/playground/src/instruments/templates/form/Multilingual-Form/index.ts +++ b/apps/playground/src/instruments/templates/form/Multilingual-Form/index.ts @@ -15,17 +15,19 @@ export default defineInstrument({ edition: 1, name: '' }, + clientDetails: { + estimatedDuration: 1, + instructions: { + en: [''], + fr: [''] + } + }, content: {}, details: { description: { en: '', fr: '' }, - estimatedDuration: 1, - instructions: { - en: [''], - fr: [''] - }, license: 'UNLICENSED', title: { en: '', diff --git a/apps/playground/src/instruments/templates/form/Unilingual-Form/index.ts b/apps/playground/src/instruments/templates/form/Unilingual-Form/index.ts index 13594add3..b6bb962c5 100644 --- a/apps/playground/src/instruments/templates/form/Unilingual-Form/index.ts +++ b/apps/playground/src/instruments/templates/form/Unilingual-Form/index.ts @@ -11,11 +11,13 @@ export default defineInstrument({ edition: 1, name: '' }, + clientDetails: { + estimatedDuration: 1, + instructions: [''] + }, content: {}, details: { description: '', - estimatedDuration: 1, - instructions: [''], license: 'UNLICENSED', title: '' }, diff --git a/apps/playground/src/instruments/templates/interactive/Interactive-Instrument/index.ts b/apps/playground/src/instruments/templates/interactive/Interactive-Instrument/index.ts index a3baf2813..9d8079dd1 100644 --- a/apps/playground/src/instruments/templates/interactive/Interactive-Instrument/index.ts +++ b/apps/playground/src/instruments/templates/interactive/Interactive-Instrument/index.ts @@ -6,7 +6,6 @@ import { z } from '/runtime/v1/zod@3.23.x'; export default defineInstrument({ kind: 'INTERACTIVE', language: 'en', - tags: [''], internal: { edition: 1, @@ -22,10 +21,12 @@ export default defineInstrument({ }); } }, + clientDetails: { + estimatedDuration: 1, + instructions: [''] + }, details: { description: '', - estimatedDuration: 1, - instructions: [''], license: 'UNLICENSED', title: '' }, diff --git a/apps/web/src/features/instruments/components/InstrumentCard/InstrumentCard.stories.tsx b/apps/web/src/features/instruments/components/InstrumentCard/InstrumentCard.stories.tsx index 1ca35eedb..1c59b4215 100644 --- a/apps/web/src/features/instruments/components/InstrumentCard/InstrumentCard.stories.tsx +++ b/apps/web/src/features/instruments/components/InstrumentCard/InstrumentCard.stories.tsx @@ -10,7 +10,7 @@ export default { component: InstrumentCard } as Meta; export const Default: Story = { args: { - instrument: { ...unilingualFormInstrument.instance, supportedLanguages: [] }, + instrument: { ...unilingualFormInstrument.instance, supportedLanguages: ['en', 'fr'] }, onClick: () => alert('Click!') } }; diff --git a/apps/web/src/features/instruments/components/InstrumentCard/InstrumentCard.tsx b/apps/web/src/features/instruments/components/InstrumentCard/InstrumentCard.tsx index a25eae252..ae9c7bdd6 100644 --- a/apps/web/src/features/instruments/components/InstrumentCard/InstrumentCard.tsx +++ b/apps/web/src/features/instruments/components/InstrumentCard/InstrumentCard.tsx @@ -6,6 +6,14 @@ import { InstrumentIcon } from '@opendatacapture/react-core'; import type { UnilingualInstrumentInfo } from '@opendatacapture/schemas/instrument'; import { BadgeAlertIcon, BadgeCheckIcon } from 'lucide-react'; +type BaseCardItem = { label: string; tooltip?: React.ReactNode }; + +type LinkCardItem = BaseCardItem & { href?: null | string; kind: 'link' }; + +type TextCardItem = BaseCardItem & { kind: 'text'; text?: string }; + +type CardItem = LinkCardItem | TextCardItem; + export type InstrumentCardProps = { instrument: UnilingualInstrumentInfo & { supportedLanguages: Language[]; @@ -18,119 +26,140 @@ export const InstrumentCard = ({ instrument, onClick }: InstrumentCardProps) => const license = licenses.get(instrument.details.license); + const content: CardItem[] = [ + { + kind: 'text', + label: t({ + en: 'Authors', + fr: 'Auteurs' + }), + text: instrument.details.authors?.join(', ') + }, + { + kind: 'text', + label: t({ + en: 'Description', + fr: 'Description' + }), + text: instrument.details.description + }, + { + kind: 'text', + label: t({ + en: 'Languages', + fr: 'Langues' + }), + text: instrument.supportedLanguages + .map((language) => { + switch (language) { + case 'en': + return 'English'; + case 'fr': + return 'Français'; + } + }) + .join(', ') + }, + { + kind: 'text', + label: t({ + en: 'License', + fr: 'Licence' + }), + text: license?.name ?? 'NA', + tooltip: ( + + + {license?.isOpenSource ? ( + + ) : ( + + )} + + +

+ {license?.isOpenSource + ? t({ + en: 'This is a free and open-source license', + fr: "Il s'agit d'une licence libre" + }) + : t({ + en: 'This is not a free and open source license', + fr: "Il ne s'agit pas d'une licence libre" + })} +

+
+
+ ) + }, + { + href: instrument.details.referenceUrl, + kind: 'link', + label: t({ + en: 'Reference Link', + fr: 'Lien vers la référence' + }) + }, + { + href: instrument.details.sourceUrl, + kind: 'link', + label: t({ + en: 'Source Link', + fr: 'Lien vers le code source' + }) + }, + { + kind: 'text', + label: 'Tags', + text: instrument.tags.join(', ') + } + ]; + return ( -
- +
+
+ +
-
- +
+ {instrument.details.title} -
- {instrument.details.authors && ( - {`${t({ en: 'Authors:', fr: 'Auteurs :' })} ${instrument.details.authors.join(', ')}`} - )} - - {`${t({ en: 'Tags:', fr: 'Tags :' })} ${instrument.tags.join(', ')}`} - - - {`${t({ - en: 'Supported Languages: ', - fr: 'Langues disponibles :' - })} ${instrument.supportedLanguages - .map((language) => { - switch (language) { - case 'en': - return 'English'; - case 'fr': - return 'Français'; - } - }) - .join(', ')}`} - -
- - {t({ - en: 'License: ', - fr: 'Licence : ' - }) + (license?.name ?? 'NA')} - -   - - - {license?.isOpenSource ? ( - - ) : ( - - )} - - -

- {license?.isOpenSource - ? t({ - en: 'This is a free and open-source license', - fr: "Il s'agit d'une licence libre" - }) - : t({ - en: 'This is not a free and open source license', - fr: "Il ne s'agit pas d'une licence libre" - })} +

+ {content.map((item) => { + if (item.kind === 'link' && !item.href) { + return null; + } else if (item.kind === 'text' && !item.text) { + return null; + } + return ( +
+

+ {item.label + t({ en: ': ', fr: ' : ' })} + {item.kind === 'text' && {item.text}} + {item.kind === 'link' && ( + + {item.href} + + )}

- - -
- {instrument.details.referenceUrl && ( -
- - {t({ - en: 'Reference Link: ', - fr: 'Lien vers la référence : ' - })} - -   - - {instrument.details.referenceUrl} - -
- )} - {instrument.details.sourceUrl && ( -
- - {t({ - en: 'Source Link', - fr: 'Lien vers le code source' - })} - -   - - {instrument.details.sourceUrl} - -
- )} + {item.tooltip} +
+ ); + })}
-
-

{instrument.details.description}

); diff --git a/packages/instrument-bundler/src/__tests__/repositories/form/index.ts b/packages/instrument-bundler/src/__tests__/repositories/form/index.ts index 1e21f31ac..4b6202817 100644 --- a/packages/instrument-bundler/src/__tests__/repositories/form/index.ts +++ b/packages/instrument-bundler/src/__tests__/repositories/form/index.ts @@ -2,6 +2,10 @@ import { defineInstrument } from '/runtime/v1/@opendatacapture/runtime-core'; import { z } from '/runtime/v1/zod@3.23.x'; export default defineInstrument({ + clientDetails: { + estimatedDuration: 1, + instructions: ['Please complete the form'] + }, content: { q1: { kind: 'boolean', @@ -25,8 +29,6 @@ export default defineInstrument({ }, details: { description: 'This is a form instrument', - estimatedDuration: 1, - instructions: ['Please complete the form'], license: 'Apache-2.0', title: 'Form Instrument Stub' }, diff --git a/packages/instrument-bundler/src/__tests__/repositories/interactive/index.tsx b/packages/instrument-bundler/src/__tests__/repositories/interactive/index.tsx index 9f0826515..e484765a7 100644 --- a/packages/instrument-bundler/src/__tests__/repositories/interactive/index.tsx +++ b/packages/instrument-bundler/src/__tests__/repositories/interactive/index.tsx @@ -6,6 +6,10 @@ import './styles.css'; import '/runtime/v1/normalize.css@8.x/normalize.css'; export default defineInstrument({ + clientDetails: { + estimatedDuration: 1, + instructions: ['Please complete the task'] + }, content: { render(done) { const rootElement = document.createElement('div'); @@ -21,8 +25,6 @@ export default defineInstrument({ }, details: { description: 'This is an interactive instrument', - estimatedDuration: 1, - instructions: ['Please complete the task'], license: 'Apache-2.0', title: 'Interactive Instrument Stub' }, diff --git a/packages/instrument-library/src/forms/brief-psychiatric-rating-scale/index.ts b/packages/instrument-library/src/forms/brief-psychiatric-rating-scale/index.ts index 70841f10a..69bc0d116 100644 --- a/packages/instrument-library/src/forms/brief-psychiatric-rating-scale/index.ts +++ b/packages/instrument-library/src/forms/brief-psychiatric-rating-scale/index.ts @@ -172,17 +172,19 @@ export default defineInstrument({ variant: 'slider' } }, + clientDetails: { + estimatedDuration: 30, + instructions: [ + "Please enter the score for the term which best describes the patient's condition.", + '0 = not assessed, 1 = not present, 2 = very mild, 3 = mild, 4 = moderate, 5 = moderately severe, 6 = severe, 7 = extremely severe.' + ] + }, details: { description: ` The Brief Psychiatric Rating Scale is a rating scale which a clinician or researcher may use to measure psychiatric symptoms such as depression, anxiety, hallucinations and unusual behavior. The scale is one of the oldest, most widely used scales to measure psychotic symptoms and was first published in 1962.`, - estimatedDuration: 30, - instructions: [ - "Please enter the score for the term which best describes the patient's condition.", - '0 = not assessed, 1 = not present, 2 = very mild, 3 = mild, 4 = moderate, 5 = moderately severe, 6 = severe, 7 = extremely severe.' - ], license: 'UNLICENSED', title: 'Brief Psychiatric Rating Scale' }, diff --git a/packages/instrument-library/src/forms/enhanced-demographics-questionnaire/index.ts b/packages/instrument-library/src/forms/enhanced-demographics-questionnaire/index.ts index fa76ebfeb..873e42899 100644 --- a/packages/instrument-library/src/forms/enhanced-demographics-questionnaire/index.ts +++ b/packages/instrument-library/src/forms/enhanced-demographics-questionnaire/index.ts @@ -789,11 +789,7 @@ export default defineInstrument({ } } ], - details: { - description: { - en: 'This instrument is designed to capture more specific demographic data, beyond that which is required for initial subject registration. All questions are optional.', - fr: "Cet instrument est conçu pour recueillir des données démographiques plus spécifiques que celles requises pour l'enregistrement initial des sujets. celles qui sont requises pour l'enregistrement initial des sujets. Toutes les questions sont optionnelles." - }, + clientDetails: { estimatedDuration: 5, instructions: { en: [ @@ -802,7 +798,14 @@ export default defineInstrument({ fr: [ "Veuillez fournir la réponse la plus précise aux questions suivantes. S'il y a plusieurs réponses correctes, choisissez celle qui s'applique le mieux." ] + } + }, + details: { + description: { + en: 'This instrument is designed to capture more specific demographic data, beyond that which is required for initial subject registration. All questions are optional.', + fr: "Cet instrument est conçu pour recueillir des données démographiques plus spécifiques que celles requises pour l'enregistrement initial des sujets. celles qui sont requises pour l'enregistrement initial des sujets. Toutes les questions sont optionnelles." }, + license: 'Apache-2.0', title: { en: 'Enhanced Demographics Questionnaire', diff --git a/packages/instrument-library/src/forms/general-consent-form/index.ts b/packages/instrument-library/src/forms/general-consent-form/index.ts index 3003b666d..fc4034222 100644 --- a/packages/instrument-library/src/forms/general-consent-form/index.ts +++ b/packages/instrument-library/src/forms/general-consent-form/index.ts @@ -36,12 +36,14 @@ export default defineInstrument({ } } ], + clientDetails: { + estimatedDuration: 1 + }, details: { description: { en: 'The general consent form asks participants if they consent to their data being used for any purpose. This is intended for demo purposes and is not recommended for real-world research projects.', fr: "Le formulaire de consentement général demande aux participants s'ils acceptent que leurs données soient utilisées à quelque fin que ce soit. Ce formulaire est destiné à des fins de démonstration et n'est pas recommandé pour des projets de recherche réels." }, - estimatedDuration: 1, license: 'Apache-2.0', title: { en: 'General Consent Form', diff --git a/packages/instrument-library/src/forms/mini-mental-state-examination/index.ts b/packages/instrument-library/src/forms/mini-mental-state-examination/index.ts index 9e1eaf61c..38a974927 100644 --- a/packages/instrument-library/src/forms/mini-mental-state-examination/index.ts +++ b/packages/instrument-library/src/forms/mini-mental-state-examination/index.ts @@ -397,11 +397,7 @@ export default defineInstrument({ } } ], - details: { - description: { - en: 'The Mini Mental State Examination (MMSE) is a tool that can be used to systematically and thoroughly assess mental status. It is an 11-question measure that tests five areas of cognitive function: orientation, registration, attention and calculation, recall, and language. The maximum score is 30. A score of 23 or lower is indicative of cognitive impairment. The MMSE takes only 5-10 minutes to administer and is therefore practical to use repeatedly and routinely.', - fr: "Le mini-examen de l'état mental (MMSE) est un outil qui peut être utilisé pour évaluer systématiquement et complètement l'état mental. Il s'agit d'un questionnaire de 11 questions qui teste cinq domaines de la fonction cognitive : l'orientation, l'enregistrement, l'attention et le calcul, la mémorisation et le langage. Le score maximum est de 30. Un score de 23 ou moins indique une déficience cognitive. L'administration du MMSE ne prend que 5 à 10 minutes et il est donc pratique de l'utiliser de manière répétée et routinière." - }, + clientDetails: { estimatedDuration: 10, instructions: { en: [ @@ -422,7 +418,14 @@ export default defineInstrument({ "Si la personne pose une question, n'expliquez pas et n'engagez pas la conversation. Répétez simplement les mêmes instructions trois fois au maximum.", "Si la personne vous interrompt (par exemple pour demander l'objet d'une question), vous devez répondre : \"Je vous expliquerai dans quelques minutes, lorsque nous aurons terminé. Maintenant, si nous pouvions continuer, s'il vous plaît. Nous avons presque terminé\"." ] + } + }, + details: { + description: { + en: 'The Mini Mental State Examination (MMSE) is a tool that can be used to systematically and thoroughly assess mental status. It is an 11-question measure that tests five areas of cognitive function: orientation, registration, attention and calculation, recall, and language. The maximum score is 30. A score of 23 or lower is indicative of cognitive impairment. The MMSE takes only 5-10 minutes to administer and is therefore practical to use repeatedly and routinely.', + fr: "Le mini-examen de l'état mental (MMSE) est un outil qui peut être utilisé pour évaluer systématiquement et complètement l'état mental. Il s'agit d'un questionnaire de 11 questions qui teste cinq domaines de la fonction cognitive : l'orientation, l'enregistrement, l'attention et le calcul, la mémorisation et le langage. Le score maximum est de 30. Un score de 23 ou moins indique une déficience cognitive. L'administration du MMSE ne prend que 5 à 10 minutes et il est donc pratique de l'utiliser de manière répétée et routinière." }, + license: 'UNLICENSED', title: { en: 'Mini Mental State Examination', diff --git a/packages/instrument-library/src/forms/montreal-cognitive-assessment/index.ts b/packages/instrument-library/src/forms/montreal-cognitive-assessment/index.ts index 6813fc078..b50147cc2 100644 --- a/packages/instrument-library/src/forms/montreal-cognitive-assessment/index.ts +++ b/packages/instrument-library/src/forms/montreal-cognitive-assessment/index.ts @@ -105,15 +105,17 @@ export default defineInstrument({ variant: 'input' } }, - details: { - description: { - en: 'The Montreal Cognitive Assessment (MoCA) was designed as a rapid screening instrument for mild cognitive dysfunction. It assesses different cognitive domains: attention and concentration, executive functions, memory, language, visuoconstructional skills, conceptual thinking, calculations, and orientation. The MoCA may be administered by anyone who understands and follows the instructions, however, only a health professional with expertise in the cognitive field may interpret the results. Time to administer the MoCA is approximately 10 minutes. The total possible score is 30 points; a score of 26 or above is considered normal.', - fr: "Le Montreal Cognitive Assessment (MoCA) a été conçue comme un instrument de dépistage rapide des troubles cognitifs légers. Il évalue différents domaines cognitifs : l'attention et la concentration, les fonctions exécutives, la mémoire, le langage, les capacités visuoconstructives, la pensée conceptuelle, les calculs et l'orientation. Le MoCA peut être administré par toute personne qui comprend et suit les instructions, mais seul un professionnel de la santé spécialisé dans le domaine cognitif peut interpréter les résultats. L'administration du MoCA dure environ 10 minutes. Le score total possible est de 30 points ; un score de 26 ou plus est considéré comme normal." - }, + clientDetails: { estimatedDuration: 10, instructions: { en: ['All instructions may be repeated once.'], fr: ['Toutes les instructions peuvent être répétées une fois.'] + } + }, + details: { + description: { + en: 'The Montreal Cognitive Assessment (MoCA) was designed as a rapid screening instrument for mild cognitive dysfunction. It assesses different cognitive domains: attention and concentration, executive functions, memory, language, visuoconstructional skills, conceptual thinking, calculations, and orientation. The MoCA may be administered by anyone who understands and follows the instructions, however, only a health professional with expertise in the cognitive field may interpret the results. Time to administer the MoCA is approximately 10 minutes. The total possible score is 30 points; a score of 26 or above is considered normal.', + fr: "Le Montreal Cognitive Assessment (MoCA) a été conçue comme un instrument de dépistage rapide des troubles cognitifs légers. Il évalue différents domaines cognitifs : l'attention et la concentration, les fonctions exécutives, la mémoire, le langage, les capacités visuoconstructives, la pensée conceptuelle, les calculs et l'orientation. Le MoCA peut être administré par toute personne qui comprend et suit les instructions, mais seul un professionnel de la santé spécialisé dans le domaine cognitif peut interpréter les résultats. L'administration du MoCA dure environ 10 minutes. Le score total possible est de 30 points ; un score de 26 ou plus est considéré comme normal." }, license: 'UNLICENSED', title: { diff --git a/packages/instrument-library/src/forms/patient-health-questionnaire-9/index.ts b/packages/instrument-library/src/forms/patient-health-questionnaire-9/index.ts index bcfcd5c51..a36dfb669 100644 --- a/packages/instrument-library/src/forms/patient-health-questionnaire-9/index.ts +++ b/packages/instrument-library/src/forms/patient-health-questionnaire-9/index.ts @@ -141,11 +141,7 @@ export default defineInstrument({ } } ], - details: { - description: { - en: 'The Patient Health Questionnaire (PHQ) is a diagnostic tool for mental health disorders used by health care professionals that is quick and easy for patients to complete. In the mid-1990s, Robert L. Spitzer, MD, Janet B.W. Williams, DSW, and Kurt Kroenke, MD, and colleagues at Columbia University developed the Primary Care Evaluation of Mental Disorders (PRIME-MD), a diagnostic tool containing modules on 12 different mental health disorders. They worked in collaboration with researchers at the Regenstrief Institute at Indiana University and with the support of an educational grant from Pfizer Inc. During the development of PRIME-MD, Drs. Spitzer, Williams and Kroenke, created the PHQ and GAD-7 screeners. The PHQ-9, a tool specific to depression, simply scores each of the 9 DSM-IV criteria based on the mood module from the original PRIME-MD.', - fr: "Le questionnaire sur la santé du patient (PHQ) est un outil de diagnostic des troubles mentaux utilisé par les professionnels de la santé, rapide et facile à remplir par les patients. Au milieu des années 1990, Robert L. Spitzer, MD, Janet B.W. Williams, DSW, et Kurt Kroenke, MD, et leurs collègues de Columbia University ont mis au point le Primary Care Evaluation of Mental Disorders (PRIME-MD), un outil de diagnostic contenant des modules sur 12 troubles mentaux différents. Ils ont travaillé en collaboration avec des chercheurs de l'Institut Regenstrief d'Indiana University et avec le soutien d'une bourse éducative de Pfizer Inc. Au cours du développement de PRIME-MD, les docteurs Spitzer, Williams et Kroenke ont créé les screeners PHQ et GAD-7. Le PHQ-9, un outil spécifique à la dépression, évalue simplement chacun des 9 critères du DSM-IV en se basant sur le module de l'humeur de PRIME-MD." - }, + clientDetails: { estimatedDuration: 1, instructions: { en: [ @@ -154,7 +150,14 @@ export default defineInstrument({ fr: [ "Avant de commencer ce test, assurez-vous d'être dans un endroit calme où vous pourrez vous concentrer sans distraction. Vous allez répondre à 9 questions sur ce que vous avez ressenti au cours des deux dernières semaines. Répondez à chaque question le plus honnêtement possible, en vous basant sur vos sentiments et vos expériences." ] + } + }, + details: { + description: { + en: 'The Patient Health Questionnaire (PHQ) is a diagnostic tool for mental health disorders used by health care professionals that is quick and easy for patients to complete. In the mid-1990s, Robert L. Spitzer, MD, Janet B.W. Williams, DSW, and Kurt Kroenke, MD, and colleagues at Columbia University developed the Primary Care Evaluation of Mental Disorders (PRIME-MD), a diagnostic tool containing modules on 12 different mental health disorders. They worked in collaboration with researchers at the Regenstrief Institute at Indiana University and with the support of an educational grant from Pfizer Inc. During the development of PRIME-MD, Drs. Spitzer, Williams and Kroenke, created the PHQ and GAD-7 screeners. The PHQ-9, a tool specific to depression, simply scores each of the 9 DSM-IV criteria based on the mood module from the original PRIME-MD.', + fr: "Le questionnaire sur la santé du patient (PHQ) est un outil de diagnostic des troubles mentaux utilisé par les professionnels de la santé, rapide et facile à remplir par les patients. Au milieu des années 1990, Robert L. Spitzer, MD, Janet B.W. Williams, DSW, et Kurt Kroenke, MD, et leurs collègues de Columbia University ont mis au point le Primary Care Evaluation of Mental Disorders (PRIME-MD), un outil de diagnostic contenant des modules sur 12 troubles mentaux différents. Ils ont travaillé en collaboration avec des chercheurs de l'Institut Regenstrief d'Indiana University et avec le soutien d'une bourse éducative de Pfizer Inc. Au cours du développement de PRIME-MD, les docteurs Spitzer, Williams et Kroenke ont créé les screeners PHQ et GAD-7. Le PHQ-9, un outil spécifique à la dépression, évalue simplement chacun des 9 critères du DSM-IV en se basant sur le module de l'humeur de PRIME-MD." }, + license: 'PUBLIC-DOMAIN', title: { en: 'Patient Health Questionnaire (PHQ-9)', diff --git a/packages/instrument-library/src/interactive/breakout-task/index.ts b/packages/instrument-library/src/interactive/breakout-task/index.ts index 338d2032c..a727bdfb9 100644 --- a/packages/instrument-library/src/interactive/breakout-task/index.ts +++ b/packages/instrument-library/src/interactive/breakout-task/index.ts @@ -4,6 +4,10 @@ import { z } from '/runtime/v1/zod@3.23.x'; import './styles.css'; export default defineInstrument({ + clientDetails: { + estimatedDuration: 1, + instructions: ['Please attempt to win the game as quickly as possible.'] + }, content: { render(done) { const startTime = Date.now(); @@ -185,8 +189,6 @@ export default defineInstrument({ authors: ['Andrzej Mazur', 'Mozilla Contributors', 'Joshua Unrau'], description: 'This is a very simple interactive instrument, adapted from a 2D breakout game in the Mozilla documentation.', - estimatedDuration: 1, - instructions: ['Please attempt to win the game as quickly as possible.'], license: 'CC0-1.0', referenceUrl: 'https://developer.mozilla.org/en-US/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript', sourceUrl: 'https://github.com/end3r/Gamedev-Canvas-workshop/tree/gh-pages', diff --git a/packages/instrument-library/src/interactive/stroop-task/index.tsx b/packages/instrument-library/src/interactive/stroop-task/index.tsx index 38c72e90c..1585d1772 100644 --- a/packages/instrument-library/src/interactive/stroop-task/index.tsx +++ b/packages/instrument-library/src/interactive/stroop-task/index.tsx @@ -7,6 +7,10 @@ import { StroopTask } from './StroopTask.tsx'; import './styles.css'; export default defineInstrument({ + clientDetails: { + estimatedDuration: 1, + instructions: ['Please follow the instructions on the screen.'] + }, content: { render(done) { const rootElement = document.createElement('div'); @@ -17,8 +21,6 @@ export default defineInstrument({ }, details: { description: 'The Stroop Task is a psychological test designed to measure cognitive flexibility and attention.', - estimatedDuration: 1, - instructions: ['Please follow the instructions on the screen.'], license: 'Apache-2.0', title: 'Stroop Task' }, diff --git a/packages/instrument-library/src/series/happiness-questionnaire-with-consent/index.ts b/packages/instrument-library/src/series/happiness-questionnaire-with-consent/index.ts index 6a92cac99..8333a50e3 100644 --- a/packages/instrument-library/src/series/happiness-questionnaire-with-consent/index.ts +++ b/packages/instrument-library/src/series/happiness-questionnaire-with-consent/index.ts @@ -10,6 +10,16 @@ const instrument: SeriesInstrument = { en: ['Well-Being'], fr: ['Bien-être'] }, + clientDetails: { + instructions: { + en: [ + 'This instrument consists of two parts: a general consent form and a questionnaire to assess your happiness. Please complete both in a timely manner.' + ], + fr: [ + 'Cet instrument se compose de deux parties : un formulaire de consentement général et un questionnaire pour évaluer votre bonheur. Veuillez remplir les deux en temps opportun.' + ] + } + }, details: { description: { en: 'The Happiness Questionnaire is a questionnaire about happiness.', diff --git a/packages/instrument-stubs/src/forms.js b/packages/instrument-stubs/src/forms.js index 366a3089a..7ca331ea5 100644 --- a/packages/instrument-stubs/src/forms.js +++ b/packages/instrument-stubs/src/forms.js @@ -33,11 +33,17 @@ export const unilingualFormInstrument = await createInstrumentStub(async () => { } } }, + clientDetails: { + title: 'Unilingual Form (Client Title)' + }, details: { - description: 'This is a unilingual form instrument', + authors: ['Jane Doe', 'John Smith'], + description: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', estimatedDuration: 1, instructions: ['Please complete all questions'], license: 'Apache-2.0', + sourceUrl: 'https://github.com', title: 'Unilingual Form' }, measures: { diff --git a/packages/runtime-core/src/__tests__/define.test-d.ts b/packages/runtime-core/src/__tests__/define.test-d.ts index 88bb661b7..f9ff3755c 100644 --- a/packages/runtime-core/src/__tests__/define.test-d.ts +++ b/packages/runtime-core/src/__tests__/define.test-d.ts @@ -22,6 +22,10 @@ expectTypeOf>().toBeNeve expectTypeOf( defineInstrument({ + clientDetails: { + estimatedDuration: 1, + instructions: ['Please answer the questions based on your current feelings.'] + }, content: { overallHappiness: { kind: 'number', @@ -31,8 +35,6 @@ expectTypeOf( }, details: { description: 'The Happiness Questionnaire is a questionnaire about happiness.', - estimatedDuration: 1, - instructions: ['Please answer the questions based on your current feelings.'], license: 'Apache-2.0', title: 'Happiness Questionnaire' }, @@ -52,6 +54,10 @@ expectTypeOf( expectTypeOf( defineInstrument({ + clientDetails: { + estimatedDuration: 1, + instructions: ['Please answer the questions based on your current feelings.'] + }, content: { overallHappiness: { kind: 'number', @@ -61,8 +67,7 @@ expectTypeOf( }, details: { description: 'The Happiness Questionnaire is a questionnaire about happiness.', - estimatedDuration: 1, - instructions: ['Please answer the questions based on your current feelings.'], + license: 'Apache-2.0', title: 'Happiness Questionnaire' }, @@ -82,6 +87,13 @@ expectTypeOf( expectTypeOf( defineInstrument({ + clientDetails: { + estimatedDuration: 1, + instructions: { + en: ['Please answer the questions based on your current feelings.'], + fr: ['Veuillez répondre àux questions en fonction de vos sentiments actuels.'] + } + }, content: { overallHappiness: { kind: 'number', @@ -97,11 +109,7 @@ expectTypeOf( en: 'The Happiness Questionnaire is a questionnaire about happiness.', fr: 'Le questionnaire sur le bonheur est un questionnaire sur le bonheur.' }, - estimatedDuration: 1, - instructions: { - en: ['Please answer the questions based on your current feelings.'], - fr: ['Veuillez répondre àux questions en fonction de vos sentiments actuels.'] - }, + license: 'Apache-2.0', title: { en: 'Happiness Questionnaire', @@ -127,6 +135,10 @@ expectTypeOf( expectTypeOf( defineInstrument({ + clientDetails: { + estimatedDuration: 1, + instructions: [''] + }, content: { render(done) { const button = document.createElement('button'); @@ -139,8 +151,6 @@ expectTypeOf( }, details: { description: '', - estimatedDuration: 1, - instructions: [''], license: 'UNLICENSED', title: '' }, diff --git a/packages/runtime-core/src/define.d.ts b/packages/runtime-core/src/define.d.ts index 1302f8572..189b7810a 100644 --- a/packages/runtime-core/src/define.d.ts +++ b/packages/runtime-core/src/define.d.ts @@ -20,19 +20,28 @@ type DiscriminatedInstrument< : never : never; +/** @public */ +type LegacyInstrumentProperties = { + details: { + estimatedDuration?: never; + instructions?: never; + }; +}; + /** @public */ type InstrumentDef< TKind extends InstrumentKind, TLanguage extends InstrumentLanguage, TSchema extends z.ZodTypeAny -> = Omit< - DiscriminatedInstrument>, - '__runtimeVersion' | 'kind' | 'language' | 'validationSchema' -> & { - kind: TKind; - language: TLanguage; - validationSchema: TSchema; -}; +> = LegacyInstrumentProperties & + Omit< + DiscriminatedInstrument>, + '__runtimeVersion' | 'kind' | 'language' | 'validationSchema' + > & { + kind: TKind; + language: TLanguage; + validationSchema: TSchema; + }; /** @public */ export declare function defineInstrument< diff --git a/packages/runtime-core/src/types/instrument.base.d.ts b/packages/runtime-core/src/types/instrument.base.d.ts index 5babf4250..73975607c 100644 --- a/packages/runtime-core/src/types/instrument.base.d.ts +++ b/packages/runtime-core/src/types/instrument.base.d.ts @@ -1,5 +1,5 @@ import type { LicenseIdentifier } from '@opendatacapture/licenses'; -import type { ConditionalKeys, Merge } from 'type-fest'; +import type { ConditionalKeys, Merge, SetRequired } from 'type-fest'; import type { z } from 'zod'; import type { Language } from './core.d.ts'; @@ -105,26 +105,45 @@ type MultilingualClientInstrumentDetails = ClientInstrumentDetails; /** @public */ type InstrumentMeasureValue = boolean | Date | number | string | undefined; +/** @public */ +type InstrumentMeasureVisibility = 'hidden' | 'visible'; + +/** @public */ +type BaseInstrumentMeasure = { + /** @deprecated use `visibility` */ + hidden?: boolean; + label?: InstrumentUIOption; + visibility?: InstrumentMeasureVisibility; +}; + +/** @public */ +type ComputedInstrumentMeasure = SetRequired< + BaseInstrumentMeasure, + 'label' +> & { + kind: 'computed'; + value: (data: TData) => InstrumentMeasureValue; +}; + +/** @public */ +type ConstantInstrumentMeasure< + TData = any, + TLanguage extends InstrumentLanguage = InstrumentLanguage +> = BaseInstrumentMeasure & { + kind: 'const'; + ref: TData extends { [key: string]: any } + ? ConditionalKeys extends infer K + ? [K] extends [never] + ? string + : K + : never + : never; +}; + /** @public */ type InstrumentMeasure = - | { - hidden?: boolean; - kind: 'computed'; - label: InstrumentUIOption; - value: (data: TData) => InstrumentMeasureValue; - } - | { - hidden?: boolean; - kind: 'const'; - label?: InstrumentUIOption; - ref: TData extends { [key: string]: any } - ? ConditionalKeys extends infer K - ? [K] extends [never] - ? string - : K - : never - : never; - }; + | ComputedInstrumentMeasure + | ConstantInstrumentMeasure; /** @public */ type InstrumentMeasures = { @@ -176,6 +195,8 @@ type ScalarInstrumentInternal = { type ScalarInstrument = Merge< BaseInstrument, { + defaultMeasureVisibility?: InstrumentMeasureVisibility; + internal: ScalarInstrumentInternal; /** Arbitrary measures derived from the data */ @@ -188,13 +209,17 @@ type ScalarInstrument; +const $InstrumentMeasureVisibility: z.ZodType = z.enum(['hidden', 'visible']); + const $InstrumentMeasureValue: z.ZodType = z.union([ z.string(), z.boolean(), @@ -95,28 +100,31 @@ const $ComputedInstrumentMeasure = z.object({ kind: z.literal('computed'), label: $$InstrumentUIOption(z.string()), value: $ComputeMeasureFunction -}) satisfies z.ZodType, { kind: 'computed' }>>; +}) satisfies z.ZodType; const $UnilingualComputedInstrumentMeasure = z.object({ hidden: z.boolean().optional(), kind: z.literal('computed'), label: z.string(), - value: $ComputeMeasureFunction -}) satisfies z.ZodType>, { kind?: 'computed' }>>; + value: $ComputeMeasureFunction, + visibility: $InstrumentMeasureVisibility.optional() +}) satisfies z.ZodType>; const $ConstantInstrumentMeasure = z.object({ hidden: z.boolean().optional(), kind: z.literal('const'), label: $$InstrumentUIOption(z.string()).optional(), - ref: z.string() -}) satisfies z.ZodType, { kind?: 'const' }>>; + ref: z.string(), + visibility: $InstrumentMeasureVisibility.optional() +}) satisfies z.ZodType; const $UnilingualConstantInstrumentMeasure = z.object({ hidden: z.boolean().optional(), kind: z.literal('const'), label: z.string().optional(), - ref: z.string() -}) satisfies z.ZodType>, { kind?: 'const' }>>; + ref: z.string(), + visibility: $InstrumentMeasureVisibility.optional() +}) satisfies z.ZodType>; const $InstrumentMeasures = z.record( z.union([$ComputedInstrumentMeasure, $ConstantInstrumentMeasure]) @@ -143,6 +151,7 @@ const $ScalarInstrumentInternal = z.object({ }); const $ScalarInstrument = $BaseInstrument.extend({ + defaultMeasureVisibility: $InstrumentMeasureVisibility.optional(), internal: $ScalarInstrumentInternal, measures: $InstrumentMeasures.nullable(), validationSchema: $ZodTypeAny From 8160db4f31c85c3c1e06c41e9b758c1363211279 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Thu, 5 Dec 2024 12:15:24 -0500 Subject: [PATCH 3/8] feat: add instructions dialog to form --- .../components/FormContent/FormContent.tsx | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/instrument-renderer/src/components/FormContent/FormContent.tsx b/packages/instrument-renderer/src/components/FormContent/FormContent.tsx index 05842927d..2463e9179 100644 --- a/packages/instrument-renderer/src/components/FormContent/FormContent.tsx +++ b/packages/instrument-renderer/src/components/FormContent/FormContent.tsx @@ -1,5 +1,7 @@ -import { Form, Heading } from '@douglasneuroinformatics/libui/components'; +import { Button, Dialog, Form, Heading } from '@douglasneuroinformatics/libui/components'; +import { useTranslation } from '@douglasneuroinformatics/libui/hooks'; import type { AnyUnilingualFormInstrument, FormInstrument } from '@opendatacapture/runtime-core'; +import { InfoIcon } from 'lucide-react'; import type { Promisable } from 'type-fest'; export type FormContentProps = { @@ -8,9 +10,33 @@ export type FormContentProps = { }; export const FormContent = ({ instrument, onSubmit }: FormContentProps) => { + const { t } = useTranslation(); + const instructions = instrument.clientDetails?.instructions ?? instrument.details.instructions; return (
- {instrument.details.title} +
+ {instrument.clientDetails?.title ?? instrument.details.title} + + + + + + + + {t({ + en: 'Instructions', + fr: 'Instructions' + })} + + + +

{instructions?.join(', ')}

+
+
+
+
Date: Thu, 5 Dec 2024 12:15:44 -0500 Subject: [PATCH 4/8] fix: adjust editor params --- apps/playground/src/components/Editor/EditorPane.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/playground/src/components/Editor/EditorPane.tsx b/apps/playground/src/components/Editor/EditorPane.tsx index ba0a5c02e..ddfe85ecd 100644 --- a/apps/playground/src/components/Editor/EditorPane.tsx +++ b/apps/playground/src/components/Editor/EditorPane.tsx @@ -142,6 +142,7 @@ export const EditorPane = React.forwardRef(funct options={{ automaticLayout: true, codeLens: false, + colorDecorators: true, contextmenu: false, fixedOverflowWidgets: true, minimap: { @@ -150,9 +151,13 @@ export const EditorPane = React.forwardRef(funct quickSuggestions: true, quickSuggestionsDelay: 10, scrollBeyondLastLine: false, + showDeprecated: true, stickyScroll: { enabled: false }, + suggest: { + showDeprecated: false + }, suggestOnTriggerCharacters: true, tabCompletion: 'on', tabSize: 2 From 13a39751f96775f903e2b738a4a5254c016ff5a9 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Thu, 5 Dec 2024 12:16:00 -0500 Subject: [PATCH 5/8] fix: add instrument title to render page --- .../features/instruments/pages/InstrumentRenderPage.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/web/src/features/instruments/pages/InstrumentRenderPage.tsx b/apps/web/src/features/instruments/pages/InstrumentRenderPage.tsx index 6393e8469..49bbf9687 100644 --- a/apps/web/src/features/instruments/pages/InstrumentRenderPage.tsx +++ b/apps/web/src/features/instruments/pages/InstrumentRenderPage.tsx @@ -7,7 +7,7 @@ import { InstrumentRenderer, type InstrumentSubmitHandler } from '@opendatacaptu import type { UnilingualInstrumentInfo } from '@opendatacapture/schemas/instrument'; import type { CreateInstrumentRecordData } from '@opendatacapture/schemas/instrument-records'; import axios from 'axios'; -import { useLocation, useNavigate, useParams } from 'react-router-dom'; +import { type Location, useLocation, useNavigate, useParams } from 'react-router-dom'; import { PageHeader } from '@/components/PageHeader'; import { useInstrumentBundle } from '@/hooks/useInstrumentBundle'; @@ -20,13 +20,12 @@ export const InstrumentRenderPage = () => { const params = useParams(); const navigate = useNavigate(); const notifications = useNotificationsStore(); - const location = useLocation(); + const location = useLocation() as Location<{ info?: UnilingualInstrumentInfo }>; const { t } = useTranslation(); const instrumentBundleQuery = useInstrumentBundle(params.id!); - const locationState = location.state as { instrument?: undefined | UnilingualInstrumentInfo } | undefined; - const title = locationState?.instrument?.details.title; + const title = location.state?.info?.clientDetails?.title ?? location.state.info?.details.title; useEffect(() => { if (!currentSession?.id) { From 5bb309599baa8a2a061ae79ef0540bc37143b11a Mon Sep 17 00:00:00 2001 From: joshunrau Date: Thu, 5 Dec 2024 12:16:51 -0500 Subject: [PATCH 6/8] fix: styling changes --- .../InstrumentRenderer/SeriesInstrumentContent.tsx | 4 ++-- .../InstrumentRenderer/SeriesInstrumentRenderer.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/instrument-renderer/src/components/InstrumentRenderer/SeriesInstrumentContent.tsx b/packages/instrument-renderer/src/components/InstrumentRenderer/SeriesInstrumentContent.tsx index 45ec7e406..be33f5436 100644 --- a/packages/instrument-renderer/src/components/InstrumentRenderer/SeriesInstrumentContent.tsx +++ b/packages/instrument-renderer/src/components/InstrumentRenderer/SeriesInstrumentContent.tsx @@ -31,9 +31,9 @@ export const SeriesInstrumentContent = ({ status }: SeriesInstrumentContentProps return (
- + {t({ - en: 'Series Instrument in Process', + en: 'Series Instrument in Progress', fr: "Série d'instruments en cours" })} diff --git a/packages/instrument-renderer/src/components/InstrumentRenderer/SeriesInstrumentRenderer.tsx b/packages/instrument-renderer/src/components/InstrumentRenderer/SeriesInstrumentRenderer.tsx index 0373c66eb..01781dab5 100644 --- a/packages/instrument-renderer/src/components/InstrumentRenderer/SeriesInstrumentRenderer.tsx +++ b/packages/instrument-renderer/src/components/InstrumentRenderer/SeriesInstrumentRenderer.tsx @@ -90,9 +90,9 @@ export const SeriesInstrumentRenderer = ({ .with({ index: 0 }, () => setIndex(1)} />) .with({ index: 1, isInstrumentInProgress: false }, () => (
- + {t({ - en: 'Series Instrument in Process', + en: 'Series Instrument in Progress', fr: "Série d'instruments en cours" })} @@ -143,10 +143,10 @@ export const SeriesInstrumentRenderer = ({ .with({ index: 2 }, () => (
- + {t({ en: 'Thank You!', fr: 'Merci' From e1923d2101dee4bf0703f6645650583206f18b67 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Thu, 5 Dec 2024 12:17:07 -0500 Subject: [PATCH 7/8] feat: implement new measures visibility --- .../InstrumentSummary/InstrumentSummary.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/instrument-renderer/src/components/InstrumentSummary/InstrumentSummary.tsx b/packages/instrument-renderer/src/components/InstrumentSummary/InstrumentSummary.tsx index 955b649ec..4507fd349 100644 --- a/packages/instrument-renderer/src/components/InstrumentSummary/InstrumentSummary.tsx +++ b/packages/instrument-renderer/src/components/InstrumentSummary/InstrumentSummary.tsx @@ -28,7 +28,13 @@ export const InstrumentSummary = ({ data, instrument, subject, timeCollected }: } const computedMeasures = filter(computeInstrumentMeasures(instrument, data), (_, key) => { - return !instrument.measures?.[key]!.hidden; + const measure = instrument.measures?.[key]; + if (measure?.visibility === 'hidden' || measure?.hidden === true) { + return false; + } else if (measure?.visibility === 'visible' || measure?.visibility === false) { + return true; + } + return instrument.defaultMeasureVisibility === 'visible'; }); const handleDownload = () => { @@ -62,15 +68,17 @@ export const InstrumentSummary = ({ data, instrument, subject, timeCollected }: timeStyle: 'long' }); + const title = (instrument.clientDetails?.title ?? instrument.details.title).trim(); + return (
- {instrument.details.title.trim() + {title ? t({ - en: `Summary of Results for the ${instrument.details.title}`, - fr: `${instrument.details.title} : résumé des résultats` + en: `Summary of Results for the ${title}`, + fr: `${title} : résumé des résultats` }) : t({ en: 'Summary of Results', @@ -163,7 +171,7 @@ export const InstrumentSummary = ({ data, instrument, subject, timeCollected }: en: 'Title', fr: 'Titre' }), - value: instrument.details.title + value: title }, { label: t({ From a84fe84473eb1f36c80f60fb6a69f1315723b7cf Mon Sep 17 00:00:00 2001 From: joshunrau Date: Thu, 5 Dec 2024 12:19:27 -0500 Subject: [PATCH 8/8] chore: publish v1.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 68775624a..3302348ba 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "opendatacapture", "type": "module", - "version": "1.7.0", + "version": "1.8.0", "private": true, "packageManager": "pnpm@9.14.2", "license": "Apache-2.0",