From fe4820cc2f46035b7c97bff52972f26b1e244332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cle=CC=81ment=20Turbelin?= Date: Sun, 20 Nov 2022 15:51:11 +0100 Subject: [PATCH 01/17] expression evaluator --- src/components/SurveyExpressionEvaluator.tsx | 235 +++++++++++++++++++ src/components/SurveySimulator.tsx | 39 ++- 2 files changed, 269 insertions(+), 5 deletions(-) create mode 100644 src/components/SurveyExpressionEvaluator.tsx diff --git a/src/components/SurveyExpressionEvaluator.tsx b/src/components/SurveyExpressionEvaluator.tsx new file mode 100644 index 0000000..9ef9a2a --- /dev/null +++ b/src/components/SurveyExpressionEvaluator.tsx @@ -0,0 +1,235 @@ +import React, { ReactNode, useState } from 'react'; +import { Button, ListGroup } from 'react-bootstrap'; +import { Expression, ExpressionArg, isExpression, isItemGroupComponent, isSurveyGroupItem, ItemComponent, SurveyGroupItem, SurveyItem } from 'survey-engine/data_types'; +import Editor from '@monaco-editor/react'; +import clsx from 'clsx'; + +import { SurveyEngineCore } from 'survey-engine/engine'; + +interface ExpressionRef { + exp: Expression; + field: string; + key: string; + value?: any; + changed?: boolean; +} + +class ExpressionRegistry { + + exps : Map; + + constructor() { + this.exps = new Map(); + } + + build(survey: SurveyGroupItem) { + this.handleItem(survey); + } + + add(itemKey:string, key: string, field: string, exp?: Expression) { + if(!exp) { + return; + } + + const ref = { + key: key, + field: field, + exp: exp + }; + + if(!this.exps.has(itemKey)) { + const rr : ExpressionRef[] = [ ref ]; + this.exps.set(itemKey, rr); + } else { + const rr = this.exps.get(itemKey); + rr?.push(ref); + } + } + + handleComponent(itemKey:string, comp: ItemComponent, index: number) { + const key = (comp.key ?? comp.role + '#'+ index); + if(comp.disabled && isExpression(comp.disabled)) { + this.add(itemKey, key, 'disabled', comp.disabled); + } + if(comp.displayCondition && isExpression(comp.displayCondition)) { + this.add(itemKey, key, 'displayCondition', comp.displayCondition); + } + if(comp.properties) { + const handleProp = (prop: ExpressionArg|number|string|undefined, name: string) => { + if(prop && typeof(prop) == "object" && "dtype" in prop && prop.dtype === "exp" ) { + this.add(itemKey, key, 'properties.'+name, (prop as ExpressionArg).exp); + } + } + handleProp(comp.properties.min, 'min'); + handleProp(comp.properties.max, 'max'); + handleProp(comp.properties.dateInputMode, 'dateInputMode'); + handleProp(comp.properties.stepSize, 'stepSize'); + } + if(isItemGroupComponent(comp)) { + comp.items.forEach( (c, i) => this.handleComponent(itemKey, c, i)); + } + } + + handleItem(item: SurveyItem) { + if(!item) { + return; + } + if(item.condition) { + this.add(item.key, '', 'condition', item.condition); + } + if(isSurveyGroupItem(item)) { + item.items.forEach(i => this.handleItem(i)); + } else { + if(item.components) { + this.handleComponent(item.key, item.components, 0); + } + } + } +} + +export class EngineState { + engine?: SurveyEngineCore; + surveyDefinition?: SurveyGroupItem + registry: ExpressionRegistry + + constructor(surveyDefinition?: SurveyGroupItem) { + this.engine = undefined; + this.registry = new ExpressionRegistry(); + this.surveyDefinition = surveyDefinition; + if(surveyDefinition) { + this.registry.build(surveyDefinition); + } + } + + setEngine(engine: SurveyEngineCore) { + this.engine = engine; + } + + update() { + this.registry.exps.forEach((refs)=> { + refs.forEach(ref => { + const oldValue = ref.value; + ref.value = this.engine?.resolveExpression(ref.exp); + ref.changed = oldValue !== ref.value; + }); + }); + } +} + + +interface SurveyEvaluatorProps { + engineState: EngineState + update: number; +} + +interface ExpressionListProps { + engineState: EngineState + update: number; + onSelect: (exp:Expression)=>void; +} + +export const ExpressionList: React.FC = (props) => { + + const list = props.engineState.registry.exps; + + const toFunc = (e: Expression):string => { + var p : string[]; + + if(e.data) { + p = e.data.map(arg => { + if(arg.dtype) { + if(arg.dtype === "exp" && arg.exp) { + return toFunc(arg.exp); + } + if(arg.dtype === "num") { + return '' + arg.num; + } + } + return '"' + arg.str + '"'; + }); + } else { + p = []; + } + return e.name + '(' + p.join(',') + ')'; + } + + const ExpItem = (itemKey: string, ref: ExpressionRef, index: number) => { + return + {itemKey}:{ref.key} {ref.field} +

{ toFunc(ref.exp) }

+

{ JSON.stringify(ref.value) }

+
+ }; + + const buildList = () => { + const r : ReactNode[] = []; + list.forEach( (refs, key) => { + r.push( ...refs.map( (ref, index) => ExpItem(key, ref, index)) ); + }); + return r; + } + + return + { buildList() } + + +} + +export const SurveyExpressionEvaluator: React.FC = (props) => { + + const [hasEditorErrors, setHasEditorErrors] = useState(false); + const [expression, setExpression] = useState(undefined); + const [inputExp, setInputExpression] = useState({name:"getContext"}); + const [result, setResult] = useState(undefined); + + const evaluateExpression = () => { + if(props.engineState.engine) { + setResult(props.engineState.engine.resolveExpression(expression)); + } + }; + + const handleExpSelected = (exp: Expression) => { + setInputExpression(exp); + } + + return + + { + if (markers.length > 0) { + setHasEditorErrors(true) + } else { + setHasEditorErrors(false) + } + }} + onChange={(value) => { + if (!value) { return } + let config: Expression; + try { + config = JSON.parse(value); + } catch (e: any) { + console.error(e); + return + } + if (!config) { return } + setExpression(config); + }} + /> + +
+ Result + +
+
; +} diff --git a/src/components/SurveySimulator.tsx b/src/components/SurveySimulator.tsx index f33f96d..b861efd 100644 --- a/src/components/SurveySimulator.tsx +++ b/src/components/SurveySimulator.tsx @@ -1,9 +1,12 @@ import { AlertBox, SurveyView, Dialog, DialogBtn } from 'case-web-ui'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Dropdown, DropdownButton } from 'react-bootstrap'; import { Survey, SurveyContext, SurveySingleItemResponse } from 'survey-engine/data_types'; import { nl, nlBE, fr, de, it } from 'date-fns/locale'; +import { SurveyEngineCore } from 'survey-engine/engine'; +import { EngineState, SurveyExpressionEvaluator } from './SurveyExpressionEvaluator'; +import { getSurveyDefinition } from '../utils/survey'; const dateLocales = [ { code: 'nl', locale: nl, format: 'dd-MM-yyyy' }, @@ -38,10 +41,32 @@ interface SurveySimulatorProps { onExit: () => void; } + const SurveySimulator: React.FC = (props) => { + + const surveyDefinition = props.surveyAndContext ? getSurveyDefinition(props.surveyAndContext.survey) : undefined; + const [openSurveyEndDialog, setOpenSurveyEndDialog] = useState(false); const [surveyResponseData, setSurveyResponseData] = useState([]); + const [engineState, setEngineState ] = useState(new EngineState(surveyDefinition)); + const [engineReady, setEngineReady ] = useState(false); // Engine has been set + const [evaluatorCounter, setEvaluatorCounter ] = useState(0); // Ok to show the evaluator (after engineReady is true) + + useEffect(()=> { + if(engineReady) { + setEvaluatorCounter(evaluatorCounter + 1); + engineState.update(); + } + }, [engineReady, engineState]); + + + const onResponseChanged=(responses: SurveySingleItemResponse[], version: string, engine: SurveyEngineCore) => { + console.log(responses, engineState.engine, engine); + engineState.setEngine(engine); + setEngineReady(true); + } + const surveySubmitDialog = { @@ -71,7 +96,7 @@ const SurveySimulator: React.FC = (props) => { var a = document.createElement("a"); var file = new Blob([JSON.stringify(exportData, undefined, 2)], { type: 'json' }); a.href = URL.createObjectURL(file); - a.download = `${props.surveyAndContext?.survey.current.surveyDefinition.key}_responses_${(new Date()).toLocaleDateString()}.json`; + a.download = `${surveyDefinition?.key}_responses_${(new Date()).toLocaleDateString()}.json`; a.click(); setSurveyResponseData([]); @@ -113,9 +138,7 @@ const SurveySimulator: React.FC = (props) => {
-
+
{props.surveyAndContext ? = (props) => { setSurveyResponseData(responses.slice()) setOpenSurveyEndDialog(true); }} + onResponsesChanged={onResponseChanged} nextBtnText={props.config.texts.nextBtn} backBtnText={props.config.texts.backBtn} submitBtnText={props.config.texts.submitBtn} @@ -140,6 +164,10 @@ const SurveySimulator: React.FC = (props) => { /> }
+
+ Evaluation + { evaluatorCounter ? : '' } +
{surveySubmitDialog} @@ -147,4 +175,5 @@ const SurveySimulator: React.FC = (props) => { ); }; + export default SurveySimulator; From 63dfc28e5616823727aa5867bc2c1ead8dc41412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cle=CC=81ment=20Turbelin?= Date: Sun, 20 Nov 2022 18:47:08 +0100 Subject: [PATCH 02/17] add title to language selector --- src/components/NavbarComp.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/NavbarComp.tsx b/src/components/NavbarComp.tsx index baf70d7..3db204b 100644 --- a/src/components/NavbarComp.tsx +++ b/src/components/NavbarComp.tsx @@ -12,6 +12,7 @@ const NavbarComp: React.FC = (props) => { const languageSelector = (codes: string[]) => { return () } From edb03d3017040aaa22119c5eb806300d59f72fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cle=CC=81ment=20Turbelin?= Date: Thu, 24 Nov 2022 09:03:47 +0100 Subject: [PATCH 03/17] Show surveys by study --- src/components/SurveyServiceLoader.tsx | 31 +++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/components/SurveyServiceLoader.tsx b/src/components/SurveyServiceLoader.tsx index cf9b7de..e1608a0 100644 --- a/src/components/SurveyServiceLoader.tsx +++ b/src/components/SurveyServiceLoader.tsx @@ -1,9 +1,9 @@ import { Survey } from 'survey-engine/data_types'; -import { Badge, ListGroup } from 'react-bootstrap'; +import { Badge, ListGroup, Accordion } from 'react-bootstrap'; import useLoadJSON from '../hooks/useLoadJSON'; import { useEffect, useState } from 'react'; import Card from './Card'; -import { format, parseISO } from 'date-fns'; +import { parseISO } from 'date-fns'; interface localisedString { [key:string]: string; @@ -80,11 +80,22 @@ const SurveyList: React.FC = (props) => { const [lang, setLang] = useState(new Set(languages)); + const surveyByStudy : Map = new Map(); + props.surveys.forEach(s => { const codes = Object.keys(s.description); codes.forEach(k => languages.add(k)); + + const study = s.study; + if (!surveyByStudy.has(study)) { + surveyByStudy.set(study, []); + } + const list = surveyByStudy.get(study); + list?.push(s); }); + console.log(surveyByStudy); + const changeLanguage = (newLang: Set)=> { setLang(newLang); } @@ -113,12 +124,22 @@ const SurveyList: React.FC = (props) => { ) } + const surveyList = (study: string, surveys: SurveyDescription[])=> { + return + {study} + + + { surveys.map( (survey)=> { return surveySelector(survey)}) } + + + ; + } return (
- - { props.surveys.map( (survey)=> { return surveySelector(survey)}) } - + + {Array.from(surveyByStudy.entries()).map(entry => surveyList(entry[0], entry[1]))} +
); }; From 98668d33c12716ed873e8da1e4d4707240bd7b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cle=CC=81ment=20Turbelin?= Date: Thu, 24 Nov 2022 09:04:33 +0100 Subject: [PATCH 04/17] reorganize simulator's menu --- src/components/SurveySimulator.tsx | 42 +++++++++--------------------- 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/src/components/SurveySimulator.tsx b/src/components/SurveySimulator.tsx index 39e20b8..c3d92f8 100644 --- a/src/components/SurveySimulator.tsx +++ b/src/components/SurveySimulator.tsx @@ -1,4 +1,4 @@ -import { AlertBox, SurveyView, Dialog, DialogBtn } from 'case-web-ui'; +import { AlertBox, SurveyView, Dialog, DialogBtn, Checkbox } from 'case-web-ui'; import React, { useEffect, useState } from 'react'; import { Button, Dropdown, DropdownButton } from 'react-bootstrap'; import { Survey, SurveyContext, SurveySingleItemResponse } from 'survey-engine/data_types'; @@ -58,6 +58,7 @@ const SurveySimulator: React.FC = (props) => { const [evaluatorCounter, setEvaluatorCounter ] = useState(0); // Ok to show the evaluator (after engineReady is true) const [showEvaluator, setShowEvaluator] = useState(false); + const [showKeys, setShowKeys ] = useState(props.config.showKeys); const onResponseChanged=(responses: SurveySingleItemResponse[], version: string, engine: SurveyEngineCore) => { console.log(responses, engineState.engine, engine); @@ -114,42 +115,23 @@ const SurveySimulator: React.FC = (props) => { return (
-
-
-
- { - switch (eventKey) { - case 'save': - break; - case 'exit': - if (window.confirm('Do you want to exit the simulator (will lose state)?')) { +
+
+ + +
-
+
+
{props.surveyAndContext ? = (props) => { /> }
-
+
Evaluation { evaluatorCounter ? : '' }
From f5fed37a2fa9b1765e852229b9015483bd50def7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cle=CC=81ment=20Turbelin?= Date: Wed, 7 Dec 2022 19:49:03 +0100 Subject: [PATCH 05/17] add response inspector, refactor to SurveyInspector --- .../ExpressionEvaluator.tsx} | 131 +++++++----------- .../SurveyInspector/ResponseList.tsx | 54 ++++++++ .../SurveyInspector/SurveyInspector.tsx | 80 +++++++++++ src/components/SurveyInspector/index.ts | 2 + src/components/SurveySimulator.tsx | 17 ++- src/index.scss | 11 ++ 6 files changed, 204 insertions(+), 91 deletions(-) rename src/components/{SurveyExpressionEvaluator.tsx => SurveyInspector/ExpressionEvaluator.tsx} (63%) create mode 100644 src/components/SurveyInspector/ResponseList.tsx create mode 100644 src/components/SurveyInspector/SurveyInspector.tsx create mode 100644 src/components/SurveyInspector/index.ts diff --git a/src/components/SurveyExpressionEvaluator.tsx b/src/components/SurveyInspector/ExpressionEvaluator.tsx similarity index 63% rename from src/components/SurveyExpressionEvaluator.tsx rename to src/components/SurveyInspector/ExpressionEvaluator.tsx index 603d274..c26772d 100644 --- a/src/components/SurveyExpressionEvaluator.tsx +++ b/src/components/SurveyInspector/ExpressionEvaluator.tsx @@ -1,18 +1,19 @@ import React, { ReactNode, useState } from 'react'; -import { Button, ListGroup } from 'react-bootstrap'; -import { Expression, ExpressionArg, isExpression, isItemGroupComponent, isSurveyGroupItem, ItemComponent, SurveyGroupItem, SurveyItem } from 'survey-engine/data_types'; -import Editor from '@monaco-editor/react'; +import { ListGroup, Tabs, Tab, } from 'react-bootstrap'; +import { Expression, ExpressionArg, isExpression, isItemGroupComponent, isSurveyGroupItem, ItemComponent, SurveyGroupItem, SurveyItem, SurveySingleItemResponse } from 'survey-engine/data_types'; import clsx from 'clsx'; import { SurveyEngineCore } from 'survey-engine/engine'; -import { Icon, IconButton } from '@material-ui/core'; +/** + * Reference to an expression in a survey + */ interface ExpressionRef { - exp: Expression; - field: string; - key: string; - value?: any; - changed?: boolean; + exp: Expression; // The expression to inspect + field: string; // Field in the survey item + key: string; // itemKey where the expression lives + value?: any; // Resolved value with the last state + changed?: boolean; // Does this value change after the last survey state transition ? show: boolean; } @@ -21,10 +22,13 @@ interface ExpressionRef { */ class ExpressionRegistry { - exps : Map; + exps : Map; // All known expressions + + fields: Set; constructor() { this.exps = new Map(); + this.fields = new Set(); } build(survey: SurveyGroupItem) { @@ -50,6 +54,9 @@ class ExpressionRegistry { const rr = this.exps.get(itemKey); rr?.push(ref); } + if(field) { + this.fields.add(field); + } } handleComponent(itemKey:string, comp: ItemComponent, index: number) { @@ -71,6 +78,7 @@ class ExpressionRegistry { handleProp(comp.properties.dateInputMode, 'dateInputMode'); handleProp(comp.properties.stepSize, 'stepSize'); } + if(comp) if(isItemGroupComponent(comp)) { comp.items.forEach( (c, i) => this.handleComponent(itemKey, c, i)); } @@ -89,19 +97,28 @@ class ExpressionRegistry { if(item.components) { this.handleComponent(item.key, item.components, 0); } + if(item.validations) { + item.validations.forEach((v) => { + if(isExpression(v.rule)) { + this.add(item.key, v.key, 'validations' , v.rule ); + } + }); + } } - } + } } export class EngineState { engine?: SurveyEngineCore; surveyDefinition?: SurveyGroupItem registry: ExpressionRegistry + responses?: SurveySingleItemResponse[] constructor(surveyDefinition?: SurveyGroupItem) { this.engine = undefined; this.registry = new ExpressionRegistry(); - this.surveyDefinition = surveyDefinition; + this.responses = undefined; + this.surveyDefinition = surveyDefinition; if(surveyDefinition) { this.registry.build(surveyDefinition); } @@ -111,6 +128,10 @@ export class EngineState { this.engine = engine; } + setResponses(responses : SurveySingleItemResponse[]) { + this.responses = responses; + } + update() { this.registry.exps.forEach((refs)=> { refs.forEach(ref => { @@ -122,12 +143,6 @@ export class EngineState { } } - -interface SurveyEvaluatorProps { - engineState: EngineState - update: number; -} - interface ExpressionListProps { engineState: EngineState update: number; @@ -138,6 +153,8 @@ export const ExpressionList: React.FC = (props) => { const list = props.engineState.registry.exps; + const [search, setSearch] = useState(undefined); + const [refresh, setRefresh] = useState(false); const toFunc = (e: Expression):string => { @@ -164,7 +181,7 @@ export const ExpressionList: React.FC = (props) => { const ExpItem = (itemKey: string, ref: ExpressionRef, index: number) => { return - {itemKey}:{ref.key} + {itemKey}{ref.key ? ' [' + ref.key + ']' : ''} {ref.field}

{ toFunc(ref.exp) }

@@ -175,72 +192,22 @@ export const ExpressionList: React.FC = (props) => { const buildList = (refresh: boolean) => { const r : ReactNode[] = []; list.forEach( (refs, key) => { + if(search) { + if(!key.includes(search) ){ + return; + } + } r.push( ...refs.map( (ref, index) => ExpItem(key, ref, index)) ); }); return r; } - return - { buildList(refresh) } - - -} - -export const SurveyExpressionEvaluator: React.FC = (props) => { - - const [hasEditorErrors, setHasEditorErrors] = useState(false); - const [expression, setExpression] = useState(undefined); - const [inputExp, setInputExpression] = useState({name:"getContext"}); - const [result, setResult] = useState(undefined); - - const evaluateExpression = () => { - if(props.engineState.engine) { - setResult(props.engineState.engine.resolveExpression(expression)); - } - }; - - const handleExpSelected = (exp: Expression) => { - setInputExpression(exp); - } - return - - { - if (markers.length > 0) { - setHasEditorErrors(true) - } else { - setHasEditorErrors(false) - } - }} - onChange={(value) => { - if (!value) { return } - let config: Expression; - try { - config = JSON.parse(value); - } catch (e: any) { - console.error(e); - return - } - if (!config) { return } - setExpression(config); - }} - /> - -
- Result - -
-
; -} +
+ { if(e.key === "Enter") setSearch(e.currentTarget.value) }}/> +
+ + { buildList(refresh) } + + +} \ No newline at end of file diff --git a/src/components/SurveyInspector/ResponseList.tsx b/src/components/SurveyInspector/ResponseList.tsx new file mode 100644 index 0000000..8a8c24d --- /dev/null +++ b/src/components/SurveyInspector/ResponseList.tsx @@ -0,0 +1,54 @@ +import React, { useState } from 'react'; +import { ListGroup, ListGroupItem } from 'react-bootstrap'; +import { SurveySingleItemResponse } from 'survey-engine/data_types'; + + +interface ResponsesListProps { + responses?: SurveySingleItemResponse[] + meta?: boolean; + json?: boolean; +} + +export const ResponsesList: React.FC = (props) => { + + const [search, setSearch] = useState(undefined); + const [json, setJSON] = useState(false); + + const jsonify = (data: any)=>{ + return
 { JSON.stringify(data, undefined, 2) } 
+ } + + const showResponse = (response : SurveySingleItemResponse, index:number) => { + return +
{response.key}
+ Response + { jsonify(response.response) } + { props.meta ? jsonify(response.meta) : ''} +
+ } + + const selectResponses = (responses: SurveySingleItemResponse[]) => { + if(!search) { + return responses; + } + return responses.filter(r => { + return r.key.includes(search); + }); + } + + const responses = props.responses ? selectResponses(props.responses) : []; + + return +
+ + { if(e.key === "Enter") setSearch(e.currentTarget.value) }}/> +
+ { + json ? + jsonify(responses) : + { responses.map(showResponse) } + } +
+} + + diff --git a/src/components/SurveyInspector/SurveyInspector.tsx b/src/components/SurveyInspector/SurveyInspector.tsx new file mode 100644 index 0000000..520756c --- /dev/null +++ b/src/components/SurveyInspector/SurveyInspector.tsx @@ -0,0 +1,80 @@ +import React, { useState } from 'react'; +import { Button, Tabs, Tab } from 'react-bootstrap'; +import { Expression, } from 'survey-engine/data_types'; +import Editor from '@monaco-editor/react'; +import clsx from 'clsx'; + +import { EngineState, ExpressionList } from "./ExpressionEvaluator"; +import { ResponsesList } from "./ResponseList"; + +interface SurveyInspectorProps { + engineState: EngineState + update: number; +} + + +export const SurveyInspector: React.FC = (props) => { + + const [hasEditorErrors, setHasEditorErrors] = useState(false); + const [expression, setExpression] = useState(undefined); + const [inputExp, setInputExpression] = useState({name:"getContext"}); + const [result, setResult] = useState(undefined); + + const evaluateExpression = () => { + if(props.engineState.engine) { + setResult(props.engineState.engine.resolveExpression(expression)); + } + }; + + const handleExpSelected = (exp: Expression) => { + setInputExpression(exp); + } + + return + + + + + + + + { + if (markers.length > 0) { + setHasEditorErrors(true) + } else { + setHasEditorErrors(false) + } + }} + onChange={(value) => { + if (!value) { return } + let config: Expression; + try { + config = JSON.parse(value); + } catch (e: any) { + console.error(e); + return + } + if (!config) { return } + setExpression(config); + }} + /> + +
+
Result
+ +
+
+
; +} diff --git a/src/components/SurveyInspector/index.ts b/src/components/SurveyInspector/index.ts new file mode 100644 index 0000000..fe77733 --- /dev/null +++ b/src/components/SurveyInspector/index.ts @@ -0,0 +1,2 @@ +export { EngineState } from "./ExpressionEvaluator"; +export { SurveyInspector } from "./SurveyInspector"; \ No newline at end of file diff --git a/src/components/SurveySimulator.tsx b/src/components/SurveySimulator.tsx index c3d92f8..343a588 100644 --- a/src/components/SurveySimulator.tsx +++ b/src/components/SurveySimulator.tsx @@ -1,11 +1,10 @@ -import { AlertBox, SurveyView, Dialog, DialogBtn, Checkbox } from 'case-web-ui'; -import React, { useEffect, useState } from 'react'; -import { Button, Dropdown, DropdownButton } from 'react-bootstrap'; +import { AlertBox, SurveyView, Dialog, DialogBtn } from 'case-web-ui'; +import React, { useState } from 'react'; +import { Button } from 'react-bootstrap'; import { Survey, SurveyContext, SurveySingleItemResponse } from 'survey-engine/data_types'; - import { nl, nlBE, fr, de, it, da, es, pt } from 'date-fns/locale'; import { SurveyEngineCore } from 'survey-engine/engine'; -import { EngineState, SurveyExpressionEvaluator } from './SurveyExpressionEvaluator'; +import { EngineState, SurveyInspector } from './SurveyInspector'; import { CustomSurveyResponseComponent } from 'case-web-ui/build/components/survey/SurveySingleItemView/ResponseComponent/ResponseComponent'; import clsx from 'clsx'; @@ -63,8 +62,9 @@ const SurveySimulator: React.FC = (props) => { const onResponseChanged=(responses: SurveySingleItemResponse[], version: string, engine: SurveyEngineCore) => { console.log(responses, engineState.engine, engine); engineState.setEngine(engine); - setEvaluatorCounter(evaluatorCounter + 1); + engineState.setResponses(responses); engineState.update(); + setEvaluatorCounter(evaluatorCounter + 1); } const toggleEvaluator = () => { @@ -121,7 +121,7 @@ const SurveySimulator: React.FC = (props) => { props.onExit(); }}} variant="warning" className="me-1"> Exit - +
@@ -155,8 +155,7 @@ const SurveySimulator: React.FC = (props) => { }
- Evaluation - { evaluatorCounter ? : '' } + { evaluatorCounter ? : '' }
diff --git a/src/index.scss b/src/index.scss index cd1a1ce..a8f9bfb 100644 --- a/src/index.scss +++ b/src/index.scss @@ -1 +1,12 @@ @import "~case-web-ui/build/scss/theme-default.scss"; + +.nav-tabs .nav-item { + background-color: map-get($theme-colors, "secondary"); + + + .nav-link { + color: map-get($theme-colors ,'grey' ); + text-decoration: none; + } +} + From 3642555db8f055dc816820158044a3ec7d4dd5c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cle=CC=81ment=20Turbelin?= Date: Thu, 23 Feb 2023 17:46:48 +0100 Subject: [PATCH 06/17] use case 1.14.3 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 2629514..e91fcad 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "@types/react": "^17.0.40", "@types/react-dom": "^17.0.13", "bootstrap": "^5.2.2", - "case-web-ui": "^1.14.0", + "case-web-ui": "^1.14.3", "clsx": "^1.2.1", "date-fns": "^2.29.3", "react": "^17.0.2", diff --git a/yarn.lock b/yarn.lock index 16dfc7a..389f804 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3364,10 +3364,10 @@ case-sensitive-paths-webpack-plugin@2.3.0: resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.3.0.tgz#23ac613cc9a856e4f88ff8bb73bbb5e989825cf7" integrity sha512-/4YgnZS8y1UXXmC02xD5rRrBEu6T5ub+mQHLNRj0fzTRbgdBYhsNo2V5EqwgqrExjxsjtF/OpAKAMkKsxbD5XQ== -case-web-ui@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/case-web-ui/-/case-web-ui-1.14.0.tgz#5cf3305868f194c74bd9a815b1ec8cbe1109097b" - integrity sha512-Qbn52iGOdco4DFbcHplrqGYr7t3T1VkTkb68ykWlZ/p9LU5R82rmcYgffcbiSPwE9SriJhwv06Ynhbxli101vQ== +case-web-ui@^1.14.3: + version "1.14.3" + resolved "https://registry.yarnpkg.com/case-web-ui/-/case-web-ui-1.14.3.tgz#43b921daf19fac19fa36bf3800ae72b8e9bf4a5f" + integrity sha512-UQY5mS8jz94fuVeBiXKOc8u/uJIQ6sDAG++VvnbIxFlEty4I0UneTHEnDPSovQMTQjcVRg3vqmFQbGoau5ZMkA== dependencies: "@fortawesome/fontawesome-free" "^5.15.4" From c5ab7f4759488be89a41757c77663f624c0ff672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cle=CC=81ment=20Turbelin?= Date: Fri, 24 Feb 2023 09:05:29 +0100 Subject: [PATCH 07/17] fix callaback --- src/components/SurveySimulator.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/SurveySimulator.tsx b/src/components/SurveySimulator.tsx index 343a588..bdc6f9d 100644 --- a/src/components/SurveySimulator.tsx +++ b/src/components/SurveySimulator.tsx @@ -59,13 +59,18 @@ const SurveySimulator: React.FC = (props) => { const [showKeys, setShowKeys ] = useState(props.config.showKeys); - const onResponseChanged=(responses: SurveySingleItemResponse[], version: string, engine: SurveyEngineCore) => { + const onResponseChanged=(responses: SurveySingleItemResponse[], version: string, engine?: SurveyEngineCore) => { console.log(responses, engineState.engine, engine); - engineState.setEngine(engine); - engineState.setResponses(responses); - engineState.update(); - setEvaluatorCounter(evaluatorCounter + 1); + if(engine) { + engineState.setEngine(engine); + engineState.setResponses(responses); + engineState.update(); + setEvaluatorCounter(evaluatorCounter + 1); + } else { + console.warn("Engine instance is not passed to callback, not able to use evaluator"); + } } + const toggleEvaluator = () => { setShowEvaluator(!showEvaluator) From 33cea1f68658ac98eb94fd5271583e4e1a3f8f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cle=CC=81ment=20Turbelin?= Date: Fri, 24 Feb 2023 16:16:01 +0100 Subject: [PATCH 08/17] refactor UI : merge menu with Simulator & setup --- src/App.tsx | 100 ++-------------- src/components/SimulationSetup.tsx | 39 +------ .../SurveyInspector/SurveyInspector.tsx | 2 +- src/components/SurveyMenu.tsx | 107 +++++++++++------- src/components/SurveySimulator.tsx | 1 - src/index.scss | 15 ++- 6 files changed, 87 insertions(+), 177 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 9c80422..c9b3f07 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,30 +1,21 @@ import React, { useState } from 'react'; -import { Survey, SurveyContext, SurveySingleItemResponse } from 'survey-engine/data_types'; +import { Survey } from 'survey-engine/data_types'; import Navbar from './components/NavbarComp'; -import SimulationSetup, { defaultSimulatorUIConfig, defaultSurveyContext } from './components/SimulationSetup'; import SurveyLoader from './components/SurveyLoader'; import SurveyMenu from './components/SurveyMenu'; import SurveyServiceLoader from './components/SurveyServiceLoader'; -import SurveySimulator, { SimulatorUIConfig } from './components/SurveySimulator'; - interface AppState { selectedLanguage?: string; languageCodes?: string[]; surveyKey?: string; survey?: Survey; - surveyContext: SurveyContext; - prefillsFile?: File; - prefillValues?: SurveySingleItemResponse[], screen: Screens; - simulatorUIConfig: SimulatorUIConfig; } -type Screens = 'loader' | 'menu' | 'simulation-setup' | 'simulator'; +type Screens = 'loader' | 'menu'; const initialState: AppState = { screen: 'loader', - simulatorUIConfig: { ...defaultSimulatorUIConfig }, - surveyContext: { ...defaultSurveyContext }, } const surveyProviderUrl = process.env.REACT_APP_SURVEY_URL ?? ""; @@ -83,104 +74,29 @@ const App: React.FC = () => { }}>
- - { + + { surveyProviderUrl ? : null - } + }
+ case 'menu': if (!appState.selectedLanguage || !appState.survey) { reset(); return null; } return navigateTo('simulation-setup')} onExit={() => { reset() }} /> - case 'simulation-setup': - return navigateTo('simulator')} - onExit={() => navigateTo('menu')} - prefillsFile={appState.prefillsFile} - onPrefillChanged={(prefills?: File) => { - if (prefills) { - const reader = new FileReader() - - reader.onabort = () => console.log('file reading was aborted') - reader.onerror = () => console.log('file reading has failed') - reader.onload = () => { - // Do whatever you want with the file contents - const res = reader.result; - if (!res || typeof (res) !== 'string') { - console.error('TODO: handle file upload error') - return; - } - const content = JSON.parse(res); - setAppState(prev => { - return { - ...prev, - prefillsFile: prefills, - prefillValues: content, - - } - }) - } - reader.readAsText(prefills) - } else { - setAppState(prev => { - return { - ...prev, - prefillsFile: prefills, - prefillValues: [] - - } - }) - } - - - - }} - currentSurveyContext={appState.surveyContext} - onSurveyContextChanged={(config) => setAppState(prev => { - return { - ...prev, - surveyContext: { - ...config - } - } - })} - currentSimulatorUIConfig={appState.simulatorUIConfig} - onSimulatorUIConfigChanged={(config) => setAppState(prev => { - return { - ...prev, - simulatorUIConfig: { - showKeys: config.showKeys, - texts: { ...config.texts } - } - } - })} - /> - case 'simulator': - return navigateTo('simulation-setup')} - /> } } + return (
void; onSurveyContextChanged: (context: SurveyContext) => void; onPrefillChanged: (prefills?: File) => void; - onStart: () => void; - onExit: () => void; } export const defaultSimulatorUIConfig: SimulatorUIConfig = { @@ -42,19 +40,6 @@ const SimulationSetup: React.FC = (props) => { const [hasUILabelEditorErrors, setHasUILabelEditorErrors] = useState(false); const [openSurveyUIConfigUploadDialog, setOpenSurveyUIConfigUploadDialog] = useState(false); - const navButtons = ( -
- - -
- ) - const surveyUIConfigUploader = { @@ -109,12 +94,8 @@ const SimulationSetup: React.FC = (props) => { onClose={() => setOpenSurveyContextUploadDialog(false)} /> - - return (
- {navButtons} - @@ -201,22 +182,7 @@ const SimulationSetup: React.FC = (props) => { - - { - props.onSimulatorUIConfigChanged({ - ...props.currentSimulatorUIConfig, - showKeys: value - }) - }} - label="Show keys" - /> +
Survey UI Labels:
= (props) => {
- {navButtons} {surveyContextUploader} {surveyUIConfigUploader}
diff --git a/src/components/SurveyInspector/SurveyInspector.tsx b/src/components/SurveyInspector/SurveyInspector.tsx index 520756c..13b6b55 100644 --- a/src/components/SurveyInspector/SurveyInspector.tsx +++ b/src/components/SurveyInspector/SurveyInspector.tsx @@ -30,7 +30,7 @@ export const SurveyInspector: React.FC = (props) => { setInputExpression(exp); } - return + return diff --git a/src/components/SurveyMenu.tsx b/src/components/SurveyMenu.tsx index b421a5d..7b554ef 100644 --- a/src/components/SurveyMenu.tsx +++ b/src/components/SurveyMenu.tsx @@ -1,49 +1,76 @@ -import React from 'react'; +import React, { useState } from 'react'; import { SurveyCard } from 'case-web-ui'; import { LocalizedString, Survey } from 'survey-engine/data_types'; +import { Tabs, Tab, Container } from 'react-bootstrap'; +import { SurveyContext, SurveySingleItemResponse } from 'survey-engine/data_types'; +import SimulationSetup, { defaultSimulatorUIConfig, defaultSurveyContext } from './SimulationSetup'; +import SurveySimulator, { SimulatorUIConfig } from './SurveySimulator'; interface SurveyMenuProps { survey: Survey; - selectedLangue: string; - onOpenSimulator: () => void; + selectedLanguage: string; onExit: () => void; } const SurveyMenu: React.FC = (props) => { + const [surveyContext, setSurveyContext] = useState(defaultSurveyContext); + const [prefillValues, setPrefillValues] = useState([]); + const [ simulatorUIConfig, setSimulatorUIConfig ] = useState(defaultSimulatorUIConfig); + const [ prefillsFile, setprefillFile] = useState(undefined); + const surveyDefinition = props.survey.surveyDefinition; - return ( -
-
-
-

Actions

-
- -
-
- -
-
- -
-
-
-

Infos

+ return + + + props.onExit()} + /> + + + { + if (file) { + const reader = new FileReader() + + reader.onabort = () => console.log('file reading was aborted') + reader.onerror = () => console.log('file reading has failed') + reader.onload = () => { + // Do whatever you want with the file contents + const res = reader.result; + if (!res || typeof (res) !== 'string') { + console.error('TODO: handle file upload error') + return; + } + const content = JSON.parse(res); + setprefillFile(file); + setPrefillValues(content); + + } + reader.readAsText(file) + } else { + setprefillFile(file); + setPrefillValues([]); + } + + }} + currentSurveyContext={surveyContext} + onSurveyContextChanged={(config) => setSurveyContext(config)} + currentSimulatorUIConfig={simulatorUIConfig} + onSimulatorUIConfigChanged={(config) => setSimulatorUIConfig(config)} + /> + + +

Survey Infos

Survey Card

{ props.survey.props ? = (props) => { profiles: [], category: 'normal' }} - selectedLanguage={props.selectedLangue} + selectedLanguage={props.selectedLanguage} avatars={[]} /> : null } - -
- -
- -
- ); + +
+ ; }; export default SurveyMenu; diff --git a/src/components/SurveySimulator.tsx b/src/components/SurveySimulator.tsx index bdc6f9d..567b3c6 100644 --- a/src/components/SurveySimulator.tsx +++ b/src/components/SurveySimulator.tsx @@ -169,5 +169,4 @@ const SurveySimulator: React.FC = (props) => { ); }; - export default SurveySimulator; diff --git a/src/index.scss b/src/index.scss index a8f9bfb..dc1f4c6 100644 --- a/src/index.scss +++ b/src/index.scss @@ -1,12 +1,19 @@ @import "~case-web-ui/build/scss/theme-default.scss"; -.nav-tabs .nav-item { - background-color: map-get($theme-colors, "secondary"); - +.nav-tabs { + background-color: map-get($theme-colors, "lightest"); +} +.nav-tabs .nav-item { + background-color: map-get($theme-colors, "lightest"); + .nav-link { - color: map-get($theme-colors ,'grey' ); + color: map-get($theme-colors ,'grey-5' ); text-decoration: none; } + + .nav-link:hover { + color: map-get($theme-colors ,'grey-2' ); + } } From 864d83659a989a585068b64732506e3f87959546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cle=CC=81ment=20Turbelin?= Date: Fri, 24 Feb 2023 16:33:57 +0100 Subject: [PATCH 09/17] handle customresponse components --- src/components/SurveyMenu.tsx | 3 +++ src/components/SurveySimulator.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/SurveyMenu.tsx b/src/components/SurveyMenu.tsx index 7b554ef..727ef7d 100644 --- a/src/components/SurveyMenu.tsx +++ b/src/components/SurveyMenu.tsx @@ -5,10 +5,12 @@ import { Tabs, Tab, Container } from 'react-bootstrap'; import { SurveyContext, SurveySingleItemResponse } from 'survey-engine/data_types'; import SimulationSetup, { defaultSimulatorUIConfig, defaultSurveyContext } from './SimulationSetup'; import SurveySimulator, { SimulatorUIConfig } from './SurveySimulator'; +import { CustomSurveyResponseComponent } from 'case-web-ui'; interface SurveyMenuProps { survey: Survey; selectedLanguage: string; + customResponseComponents?:CustomSurveyResponseComponent[] onExit: () => void; } @@ -33,6 +35,7 @@ const SurveyMenu: React.FC = (props) => { prefills={prefillValues} selectedLanguage={props.selectedLanguage} onExit={() => props.onExit()} + customResponseComponents={props.customResponseComponents} /> diff --git a/src/components/SurveySimulator.tsx b/src/components/SurveySimulator.tsx index 567b3c6..6198e09 100644 --- a/src/components/SurveySimulator.tsx +++ b/src/components/SurveySimulator.tsx @@ -5,7 +5,7 @@ import { Survey, SurveyContext, SurveySingleItemResponse } from 'survey-engine/d import { nl, nlBE, fr, de, it, da, es, pt } from 'date-fns/locale'; import { SurveyEngineCore } from 'survey-engine/engine'; import { EngineState, SurveyInspector } from './SurveyInspector'; -import { CustomSurveyResponseComponent } from 'case-web-ui/build/components/survey/SurveySingleItemView/ResponseComponent/ResponseComponent'; +import { CustomSurveyResponseComponent } from 'case-web-ui'; import clsx from 'clsx'; const dateLocales = [ From 2df2c4ea27dd788a56d26da898778c9b1d60a18a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cle=CC=81ment=20Turbelin?= Date: Fri, 24 Feb 2023 16:34:14 +0100 Subject: [PATCH 10/17] default tab is SurveyMenu --- src/components/SurveyMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SurveyMenu.tsx b/src/components/SurveyMenu.tsx index 727ef7d..777395b 100644 --- a/src/components/SurveyMenu.tsx +++ b/src/components/SurveyMenu.tsx @@ -24,7 +24,7 @@ const SurveyMenu: React.FC = (props) => { const surveyDefinition = props.survey.surveyDefinition; return - + Date: Fri, 24 Feb 2023 16:41:08 +0100 Subject: [PATCH 11/17] add local config to facilitate extension --- src/App.tsx | 7 ++++++- src/localConfig.ts | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src/localConfig.ts diff --git a/src/App.tsx b/src/App.tsx index c9b3f07..14a8e11 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import Navbar from './components/NavbarComp'; import SurveyLoader from './components/SurveyLoader'; import SurveyMenu from './components/SurveyMenu'; import SurveyServiceLoader from './components/SurveyServiceLoader'; +import { registerCustomComponents } from './localConfig'; interface AppState { selectedLanguage?: string; languageCodes?: string[]; @@ -12,6 +13,9 @@ interface AppState { screen: Screens; } +const customResponseComponents = registerCustomComponents(); + + type Screens = 'loader' | 'menu'; const initialState: AppState = { @@ -88,8 +92,9 @@ const App: React.FC = () => { return null; } return { reset() }} diff --git a/src/localConfig.ts b/src/localConfig.ts new file mode 100644 index 0000000..748f0ad --- /dev/null +++ b/src/localConfig.ts @@ -0,0 +1,6 @@ +import { CustomSurveyResponseComponent } from 'case-web-ui'; + +export const registerCustomComponents = () : CustomSurveyResponseComponent[] | undefined => { + return undefined; + // here +} \ No newline at end of file From 9f3083b767a3bf3960afe720d55347a849c91716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cle=CC=81ment=20Turbelin?= Date: Fri, 24 Feb 2023 16:59:56 +0100 Subject: [PATCH 12/17] show a message when no response is selected --- src/components/SurveySimulator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SurveySimulator.tsx b/src/components/SurveySimulator.tsx index 6198e09..6e930b9 100644 --- a/src/components/SurveySimulator.tsx +++ b/src/components/SurveySimulator.tsx @@ -160,7 +160,7 @@ const SurveySimulator: React.FC = (props) => { }
- { evaluatorCounter ? : '' } + { evaluatorCounter ? :

Select at least a response to show the inspector

}
From 0ae45be230450d866391de670b389f19e1a3d61e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cle=CC=81ment=20Turbelin?= Date: Fri, 24 Feb 2023 17:05:00 +0100 Subject: [PATCH 13/17] ui fixes --- src/components/SurveyInspector/ExpressionEvaluator.tsx | 4 ++-- src/components/SurveyInspector/ResponseList.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/SurveyInspector/ExpressionEvaluator.tsx b/src/components/SurveyInspector/ExpressionEvaluator.tsx index c26772d..3473d36 100644 --- a/src/components/SurveyInspector/ExpressionEvaluator.tsx +++ b/src/components/SurveyInspector/ExpressionEvaluator.tsx @@ -181,7 +181,7 @@ export const ExpressionList: React.FC = (props) => { const ExpItem = (itemKey: string, ref: ExpressionRef, index: number) => { return - {itemKey}{ref.key ? ' [' + ref.key + ']' : ''} + {itemKey}{ref.key ? ' [' + ref.key + ']' : ''} {ref.field}

{ toFunc(ref.exp) }

@@ -203,7 +203,7 @@ export const ExpressionList: React.FC = (props) => { } return -
+
{ if(e.key === "Enter") setSearch(e.currentTarget.value) }}/>
diff --git a/src/components/SurveyInspector/ResponseList.tsx b/src/components/SurveyInspector/ResponseList.tsx index 8a8c24d..10f3d2a 100644 --- a/src/components/SurveyInspector/ResponseList.tsx +++ b/src/components/SurveyInspector/ResponseList.tsx @@ -39,7 +39,7 @@ export const ResponsesList: React.FC = (props) => { const responses = props.responses ? selectResponses(props.responses) : []; return -
+
{ if(e.key === "Enter") setSearch(e.currentTarget.value) }}/>
From 4b0de28bc73a980047d93a6ada2a7c8a62fc1dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cle=CC=81ment=20Turbelin?= Date: Thu, 19 Oct 2023 15:35:42 +0200 Subject: [PATCH 14/17] participant flag editor --- README.md | 102 ++++++++++++++++++++------- src/App.tsx | 5 +- src/components/SimulationSetup.tsx | 106 +++++++++++++++++++++++++++++ src/components/SurveyMenu.tsx | 3 + src/localConfig.ts | 7 +- src/types/flags.ts | 13 ++++ 6 files changed, 207 insertions(+), 29 deletions(-) create mode 100644 src/types/flags.ts diff --git a/README.md b/README.md index b58e0af..affb38f 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,96 @@ -# Getting Started with Create React App +# Survey Viewer App -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +Survey Viewer is an online application to view/run a Survey from a json Survey Definition used by [Influenzanet Survey Engine](https://github.com/influenzanet/survey-engine.ts) -## Available Scripts +## Installing -In the project directory, you can run: +This application is built as a Single Page Application with React Framework. -### `yarn start` +To install the application, clone this repo and run yarn to install the dependencies -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +```bash +yarn +```` -The page will reload if you make edits.\ -You will also see any lint errors in the console. +To run in in dev environment +```bash +yarn start +``` -### `yarn test` +To build the app as a standalone website: +```bash +yarn build +``` -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +The deployable website will be in 'build' directory -### `yarn build` +## Survey Definition Source -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. +2 ways are proposed to view a survey : -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! +- Upload a JSON file containing the Survey Definition +- Use a Survey Provider Service : a list of available survey will be loaded from the service and downloaded directly from the service -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. +An implementation for survey provider service is available in [Grippenet repository](https://github.com/grippenet/survey-provider-service). It serves survey list from files in a directory. -### `yarn eject` +To use the service provider, the application must be compiled with some environment variables -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** +- REACT_APP_SURVEY_URL: URL of the survey service +- REACT_APP_CSP_CONNECT_URLS: the URL also must be added to this variable to enable the app to connect to the survey service -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. +Example +```bash +REACT_APP_SURVEY_URL=https://your.survey.service +# You may add +REACT_APP_CSP_CONNECT_URLS=$REACT_APP_SURVEY_URL ... +``` -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. +# Customize -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. +Warning: These customizations are useable only after the application is built. So you need to use either dev server (`yarn start`) or to rebuild the static website (`yarn build`) -## Learn More +## Add a custom Response Component -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). +To register a customComponent, you have to modify the script 'localConfig.ts' and make the function registerCustomComponents return the list of your component -To learn React, check out the [React documentation](https://reactjs.org/). +```ts +export const registerCustomComponents = () : CustomSurveyResponseComponent[] | undefined => { + return [ + { + name: 'awesomeComponent', // Name of your component as it will be used in `role` field of the survey definition + component: MyAwesomeComponent + } + ]; +} +``` + +## Add Flags + +You can tell to the survey viewer which flags are handled by your platform (and for each what are the expected values). The Survey Context editor will propose the known list and enable more friendly editor than json editor. + +You had to customize the + +```ts +export const registerParticipantFlags = () : ParticipantFlags => { + return { + 'myflagkey': { + label: 'The label to tell the user what is the purpose of this flag' + values: [ + { + value: '1' // The value the flag can have + label: 'A friendly label to explain what is this value' + } + ] + }, + // A real example (the 'prev' flag used in Influenzanet platform) + 'prev': { + label:"Has ongoing symptoms", + values: [ + {value:"0", label:"Does not have ongoing symptoms"} + {value: "1", label:"Has ongoing symptoms"} + ], + }, + }; +} + +``` \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 14a8e11..b1dfbd7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,7 @@ import Navbar from './components/NavbarComp'; import SurveyLoader from './components/SurveyLoader'; import SurveyMenu from './components/SurveyMenu'; import SurveyServiceLoader from './components/SurveyServiceLoader'; -import { registerCustomComponents } from './localConfig'; +import { registerCustomComponents, registerParticipantFlags } from './localConfig'; interface AppState { selectedLanguage?: string; languageCodes?: string[]; @@ -14,7 +14,7 @@ interface AppState { } const customResponseComponents = registerCustomComponents(); - +const participantFlags = registerParticipantFlags(); type Screens = 'loader' | 'menu'; @@ -92,6 +92,7 @@ const App: React.FC = () => { return null; } return void; onSurveyContextChanged: (context: SurveyContext) => void; @@ -123,6 +126,9 @@ const SimulationSetup: React.FC = (props) => {
Survey context:
+

Participant Flags

+ +

Edit the JSON directly:

= (props) => { ); }; + +interface HelpTooltipProps { + label: string; +} + +export const HelpTooltip : React.FC = (props) => { +return {props.label}}>? +} + +interface FlagsEditorProps { + availableFlags: ParticipantFlags + surveyContext: SurveyContext + onChange: (ctx: SurveyContext)=>void; +} + +export const FlagsEditor : React.FC = (props) => { + + const [newFlag, setNewFlag] = useState(''); + + + const knownFlags = new Map(Object.entries(props.availableFlags)); + const curCtx = props.surveyContext.participantFlags ?? {}; + + const flagUpdated = (name: string, value: string) => { + if(name === '') { + return; + } + const ctx = { ...props.surveyContext}; + ctx.participantFlags[name] = value; + props.onChange(ctx); + } + + const removeFlag = (name:string) => { + if(name === '') { + return; + } + const ctx = { ...props.surveyContext}; + delete ctx.participantFlags[name]; + props.onChange(ctx); + } + + const addCustomFlag = ()=> { + flagUpdated(newFlag, ''); + } + + const FlagLabel = (key: string, def?: ParticipantFlag)=> { + return + {key} + {def ? : ''} + + } + + const showFlag = (key:string) => { + + const def = knownFlags.get(key); + + const value = props.surveyContext.participantFlags[key] ?? ''; + return
+
{ FlagLabel(key, def) }
+
+ flagUpdated(key, ev.target.value)} className='form-control'/> +
+
+ {def ? + ( + Acceptable values : + + ) + : ''} + +
+
+ + } + + const usedKeys = Array.from(Object.keys(curCtx)); + // List of missing flags in context, to be added + const missing = Array.from(knownFlags.keys()).filter(k=>!usedKeys.includes(k)); + + return
+ { usedKeys.map(showFlag) } + {missing.length > 0 ? ( +
+ Add a known flag : + +
+ ) : ''} + Add custom flag + setNewFlag(ev.target.value)}/> + +
+} + export default SimulationSetup; + diff --git a/src/components/SurveyMenu.tsx b/src/components/SurveyMenu.tsx index 777395b..e14cc7c 100644 --- a/src/components/SurveyMenu.tsx +++ b/src/components/SurveyMenu.tsx @@ -6,11 +6,13 @@ import { SurveyContext, SurveySingleItemResponse } from 'survey-engine/data_type import SimulationSetup, { defaultSimulatorUIConfig, defaultSurveyContext } from './SimulationSetup'; import SurveySimulator, { SimulatorUIConfig } from './SurveySimulator'; import { CustomSurveyResponseComponent } from 'case-web-ui'; +import { ParticipantFlag, ParticipantFlags } from '../types/flags'; interface SurveyMenuProps { survey: Survey; selectedLanguage: string; customResponseComponents?:CustomSurveyResponseComponent[] + participantFlags: ParticipantFlags onExit: () => void; } @@ -40,6 +42,7 @@ const SurveyMenu: React.FC = (props) => { { if (file) { diff --git a/src/localConfig.ts b/src/localConfig.ts index 748f0ad..bafc421 100644 --- a/src/localConfig.ts +++ b/src/localConfig.ts @@ -1,6 +1,11 @@ import { CustomSurveyResponseComponent } from 'case-web-ui'; +import { ParticipantFlags } from './types/flags'; export const registerCustomComponents = () : CustomSurveyResponseComponent[] | undefined => { return undefined; // here -} \ No newline at end of file +} + +export const registerParticipantFlags = () : ParticipantFlags => { + return {}; +} diff --git a/src/types/flags.ts b/src/types/flags.ts new file mode 100644 index 0000000..3a64c30 --- /dev/null +++ b/src/types/flags.ts @@ -0,0 +1,13 @@ + +interface FlagValue { + value: string; + label: string; +} + +// Describe an available participant flag +export interface ParticipantFlag { + label: string + values: FlagValue[] +} + +export type ParticipantFlags = Record; \ No newline at end of file From 0074930e1aa95d4baa8f97a2f19cc2bd8b6a9c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cle=CC=81ment=20Turbelin?= Date: Thu, 19 Oct 2023 15:52:19 +0200 Subject: [PATCH 15/17] upgrade deps --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index e91fcad..674264c 100644 --- a/package.json +++ b/package.json @@ -13,15 +13,15 @@ "@types/node": "^16.11.12", "@types/react": "^17.0.40", "@types/react-dom": "^17.0.13", - "bootstrap": "^5.2.2", - "case-web-ui": "^1.14.3", + "bootstrap": "^5.2.3", + "case-web-ui": "^1.14.7", "clsx": "^1.2.1", "date-fns": "^2.29.3", "react": "^17.0.2", - "react-bootstrap": "^2.5.0", + "react-bootstrap": "^2.7.3", "react-dom": "^17.0.2", "react-scripts": "4.0.3", - "survey-engine": "1.2.0", + "survey-engine": "1.2.1", "typescript": "^4.5.5", "web-vitals": "^1.0.1" }, From 2aacd9ed0acf0d3459dcdfae83f5930cf5476238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cle=CC=81ment=20Turbelin?= Date: Fri, 20 Oct 2023 16:07:56 +0200 Subject: [PATCH 16/17] refactor prefills, can use several and save response as future prefills --- src/components/Card.tsx | 56 +++- src/components/HelpComponents.tsx | 11 + src/components/SimulationSetup.tsx | 314 ++++++++++-------- src/components/SurveyInfo.tsx | 48 +++ .../SurveyInspector/ResponseList.tsx | 3 +- src/components/SurveyLoader.tsx | 6 +- src/components/SurveyMenu.tsx | 119 ++++--- src/components/SurveyServiceLoader.tsx | 6 +- src/components/SurveySimulator.tsx | 43 ++- src/types/index.ts | 2 + src/types/survey.ts | 17 + 11 files changed, 403 insertions(+), 222 deletions(-) create mode 100644 src/components/HelpComponents.tsx create mode 100644 src/components/SurveyInfo.tsx create mode 100644 src/types/index.ts create mode 100644 src/types/survey.ts diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 40462c5..3f5b157 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -1,21 +1,55 @@ -import React from 'react'; +import clsx from 'clsx'; +import React, { ReactNode } from 'react'; +import { CardBody, CardHeader, Card } from 'react-bootstrap'; + + +type VariantsType = 'primary' | 'grey' | 'info'; interface CardProps { - title: string; + title: ReactNode; className?: string; + variant?: VariantsType; + headerTag?: 'h3' | 'h4' |'h5' |'h6'; +} + +interface VariantProps { + header: string; + body: string; +} + +const variants: Record = { + 'primary': { + header:'bg-primary text-white', + body:'bg-grey-1' + }, + 'grey': { + header:'bg-grey-1', + body:'' + }, + 'info': { + header:'bg-info', + body:'' + } } -const Card: React.FC = (props) => { + +const CardTitled: React.FC = (props) => { + + const v = variants[props.variant ?? 'primary']; + + const hTag = props.headerTag ?? 'h4'; + const h = React.createElement(hTag, {className: "fw-bold m-0"}, props.title); + return ( -
-
-

{props.title}

-
-
+ + + {h} + + {props.children} -
-
+ + ); }; -export default Card; +export default CardTitled; diff --git a/src/components/HelpComponents.tsx b/src/components/HelpComponents.tsx new file mode 100644 index 0000000..32ed65c --- /dev/null +++ b/src/components/HelpComponents.tsx @@ -0,0 +1,11 @@ + +import { Badge, OverlayTrigger, Tooltip } from 'react-bootstrap'; +import React, { useState } from 'react'; + +interface HelpTooltipProps { + label: string; +} + +export const HelpTooltip: React.FC = (props) => { + return {props.label}}>? +} diff --git a/src/components/SimulationSetup.tsx b/src/components/SimulationSetup.tsx index e89e0c1..7dc598c 100644 --- a/src/components/SimulationSetup.tsx +++ b/src/components/SimulationSetup.tsx @@ -3,21 +3,23 @@ import { FileDropzone } from 'case-web-ui'; import clsx from 'clsx'; import React, { useState } from 'react'; import { SurveyContext } from 'survey-engine/data_types'; -import Card from './Card'; +import CardTitled from './Card'; import { acceptJSON } from './constants'; import { SimulatorUIConfig, SurveyUILabels } from './SurveySimulator'; import UploadDialog from './UploadDialog'; import { ParticipantFlag, ParticipantFlags } from '../types/flags'; -import { Badge, Button, Collapse, OverlayTrigger, Tooltip } from 'react-bootstrap'; +import { Badge, Button, ListGroup, ListGroupItem} from 'react-bootstrap'; +import { PrefillEntry, PrefillsRegistry } from '../types'; +import { HelpTooltip } from './HelpComponents'; interface SimulationSetupProps { currentSimulatorUIConfig: SimulatorUIConfig; currentSurveyContext: SurveyContext; participantFlags: ParticipantFlags - prefillsFile?: File; + prefillRegistry: PrefillsRegistry onSimulatorUIConfigChanged: (config: SimulatorUIConfig) => void; onSurveyContextChanged: (context: SurveyContext) => void; - onPrefillChanged: (prefills?: File) => void; + onPrefillChanged: (registry: PrefillsRegistry) => void; } export const defaultSimulatorUIConfig: SimulatorUIConfig = { @@ -97,98 +99,152 @@ const SimulationSetup: React.FC = (props) => { onClose={() => setOpenSurveyContextUploadDialog(false)} /> + const updatePrefillRegistry = (registry: PrefillsRegistry) => { + const r = { ...registry }; + props.onPrefillChanged(r); + } + + const uploadPrefill = (file?: File) => { + if (!file) { + return; + } + const reader = new FileReader() + + reader.onabort = () => console.log('file reading was aborted') + reader.onerror = () => console.log('file reading has failed') + reader.onload = () => { + // Do whatever you want with the file contents + const res = reader.result; + if (!res || typeof (res) !== 'string') { + console.error('TODO: handle file upload error') + return; + } + const content = JSON.parse(res); + console.log("prefill values", content); + + const registry = props.prefillRegistry; + const n = registry.prefills.push({ name: file.name, data: content }); + registry.current = n - 1; + updatePrefillRegistry(registry); + } + reader.readAsText(file) + } + + const handleChangePrefill = (index?: number) => { + const registry = props.prefillRegistry; + registry.current = index; + updatePrefillRegistry(registry); + } + + const prefillEntry = (entry: PrefillEntry, index: number) => { + const active = index === props.prefillRegistry.current; + return handleChangePrefill(index)} style={{"cursor":"pointer"}}> + {entry.name} {active ? Current : ''} + + } + return (
- -
Prefill:
- { - if (acceptedFiles.length > 0) { - props.onPrefillChanged(acceptedFiles[0]); - } - }} - /> - - -
Survey context:
-

Participant Flags

- -

Edit the JSON directly:

- { - if (markers.length > 0) { - setHasSurveyContextEditorErrors(true) - } else { - setHasSurveyContextEditorErrors(false) - } - }} - onChange={(value) => { - if (!value) { return } - let context: SurveyContext; - try { - context = JSON.parse(value); - } catch (e: any) { - console.error(e); - return - } - if (!context) { return } - props.onSurveyContextChanged({ - ...context - }) - - }} - /> - {hasSurveyContextEditorErrors ? -

- Check the editor for errors -

- : null} -
+ + Prefills headerTag='h5'> + List of the prefill datasets available. Each one prefills a full survey. Click on one element of the list to use it to prefill the survey. + + {props.prefillRegistry.prefills.map(prefillEntry)} + + Prefills can be added by downloading them from a file or by filling a survey and saving the response as prefills +
+ Add a prefill dataset by downloading a file + { + if (acceptedFiles.length > 0) { + uploadPrefill(acceptedFiles[0]); + } + }} + /> +
- -
+ onChange={(value) => { + if (!value) { return } + let context: SurveyContext; + try { + context = JSON.parse(value); + } catch (e: any) { + console.error(e); + return + } + if (!context) { return } + props.onSurveyContextChanged({ + ...context + }) + }} + /> + {hasSurveyContextEditorErrors ? +

+ Check the editor for errors +

+ : null} +
+ + +
+ -
+ - +
Survey UI Labels:
= (props) => { >Upload config
- + {surveyContextUploader} {surveyUIConfigUploader}
@@ -258,102 +314,94 @@ const SimulationSetup: React.FC = (props) => { }; -interface HelpTooltipProps { - label: string; -} - -export const HelpTooltip : React.FC = (props) => { -return {props.label}}>? -} - interface FlagsEditorProps { availableFlags: ParticipantFlags surveyContext: SurveyContext - onChange: (ctx: SurveyContext)=>void; + onChange: (ctx: SurveyContext) => void; } -export const FlagsEditor : React.FC = (props) => { - +export const FlagsEditor: React.FC = (props) => { + const [newFlag, setNewFlag] = useState(''); - - + + const knownFlags = new Map(Object.entries(props.availableFlags)); const curCtx = props.surveyContext.participantFlags ?? {}; - + const flagUpdated = (name: string, value: string) => { - if(name === '') { + if (name === '') { return; } - const ctx = { ...props.surveyContext}; + const ctx = { ...props.surveyContext }; ctx.participantFlags[name] = value; props.onChange(ctx); } - const removeFlag = (name:string) => { - if(name === '') { + const removeFlag = (name: string) => { + if (name === '') { return; } - const ctx = { ...props.surveyContext}; + const ctx = { ...props.surveyContext }; delete ctx.participantFlags[name]; props.onChange(ctx); } - const addCustomFlag = ()=> { + const addCustomFlag = () => { flagUpdated(newFlag, ''); - } + } - const FlagLabel = (key: string, def?: ParticipantFlag)=> { + const FlagLabel = (key: string, def?: ParticipantFlag) => { return - {key} - {def ? : ''} + {key} + {def ? : ''} } - const showFlag = (key:string) => { - + const showFlag = (key: string) => { + const def = knownFlags.get(key); const value = props.surveyContext.participantFlags[key] ?? ''; return
-
{ FlagLabel(key, def) }
+
{FlagLabel(key, def)}
- flagUpdated(key, ev.target.value)} className='form-control'/> + flagUpdated(key, ev.target.value)} className='form-control' />
- {def ? + {def ? ( Acceptable values : - ) - : ''} - + ) + : ''} +
- + } const usedKeys = Array.from(Object.keys(curCtx)); // List of missing flags in context, to be added - const missing = Array.from(knownFlags.keys()).filter(k=>!usedKeys.includes(k)); - + const missing = Array.from(knownFlags.keys()).filter(k => !usedKeys.includes(k)); + return
- { usedKeys.map(showFlag) } - {missing.length > 0 ? ( -
- Add a known flag : - { flagUpdated(ev.target.value, '') }}> - {missing.map(n=> )} - -
- ) : ''} - Add custom flag - setNewFlag(ev.target.value)}/> - -
+ {missing.map(n => )} + +
+ ) : ''} + Add custom flag + setNewFlag(ev.target.value)} /> + + } export default SimulationSetup; diff --git a/src/components/SurveyInfo.tsx b/src/components/SurveyInfo.tsx new file mode 100644 index 0000000..7ba44d6 --- /dev/null +++ b/src/components/SurveyInfo.tsx @@ -0,0 +1,48 @@ +import { SurveyCard, } from 'case-web-ui'; +import React, { useState } from 'react'; +import { Card, CardBody} from 'react-bootstrap'; +import { Survey, LocalizedString } from 'survey-engine/data_types'; + +interface SurveyInfoProps { + survey: Survey; + languageCode?: string; +} + +const SurveyInfo: React.FC = (props) => { + const [showDetails, setShowDetails] = useState(false); + const surveyDefinition = props.survey.surveyDefinition; + const surveyProps = props.survey.props; + const surveyCard = surveyProps ? : ''; + + return ( + + + setShowDetails(!showDetails)}>Toggle info +

Survey {surveyDefinition.key}

+ {showDetails ? ( +
+ {surveyCard} +
+ ) : ''} +
+
+ ); +}; + +export default SurveyInfo; \ No newline at end of file diff --git a/src/components/SurveyInspector/ResponseList.tsx b/src/components/SurveyInspector/ResponseList.tsx index 10f3d2a..d884c96 100644 --- a/src/components/SurveyInspector/ResponseList.tsx +++ b/src/components/SurveyInspector/ResponseList.tsx @@ -40,7 +40,8 @@ export const ResponsesList: React.FC = (props) => { return
- + { if(e.key === "Enter") setSearch(e.currentTarget.value) }}/>
{ diff --git a/src/components/SurveyLoader.tsx b/src/components/SurveyLoader.tsx index 0a2f73f..9c619a2 100644 --- a/src/components/SurveyLoader.tsx +++ b/src/components/SurveyLoader.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { DialogBtn, FileDropzone } from 'case-web-ui'; import { Survey } from 'survey-engine/data_types'; -import Card from './Card'; +import CardTitled from './Card'; import { acceptJSON } from './constants'; interface SurveyLoaderProps { @@ -38,7 +38,7 @@ const SurveyLoader: React.FC = (props) => { } return ( - + = (props) => { >{texts.btn.useUrl} - + ); }; diff --git a/src/components/SurveyMenu.tsx b/src/components/SurveyMenu.tsx index e14cc7c..609ebc2 100644 --- a/src/components/SurveyMenu.tsx +++ b/src/components/SurveyMenu.tsx @@ -1,17 +1,18 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { SurveyCard } from 'case-web-ui'; import { LocalizedString, Survey } from 'survey-engine/data_types'; -import { Tabs, Tab, Container } from 'react-bootstrap'; +import { Tabs, Tab, Container, Button } from 'react-bootstrap'; import { SurveyContext, SurveySingleItemResponse } from 'survey-engine/data_types'; import SimulationSetup, { defaultSimulatorUIConfig, defaultSurveyContext } from './SimulationSetup'; import SurveySimulator, { SimulatorUIConfig } from './SurveySimulator'; import { CustomSurveyResponseComponent } from 'case-web-ui'; -import { ParticipantFlag, ParticipantFlags } from '../types/flags'; +import { ParticipantFlags, PrefillsRegistry, SurveyAndContext } from '../types'; +import { format } from 'date-fns'; interface SurveyMenuProps { survey: Survey; selectedLanguage: string; - customResponseComponents?:CustomSurveyResponseComponent[] + customResponseComponents?: CustomSurveyResponseComponent[] participantFlags: ParticipantFlags onExit: () => void; } @@ -20,54 +21,75 @@ const SurveyMenu: React.FC = (props) => { const [surveyContext, setSurveyContext] = useState(defaultSurveyContext); const [prefillValues, setPrefillValues] = useState([]); - const [ simulatorUIConfig, setSimulatorUIConfig ] = useState(defaultSimulatorUIConfig); - const [ prefillsFile, setprefillFile] = useState(undefined); - + const [simulatorUIConfig, setSimulatorUIConfig] = useState(defaultSimulatorUIConfig); + const [surveyAndContext, setSurveyAndContext] = useState({ + survey: props.survey, + context: surveyContext + }); + + // Show survey is used in the reset process. When ShowSurvey is set to false, a timer will + const [showSurvey, setShowSurvey] = useState(true); + + const [prefillRegistry, SetPrefillRegistry] = useState({current: undefined, prefills:[]}); + + useEffect(() => { + if(showSurvey) { + return; + } + const timer = setTimeout(() => setShowSurvey(true), 500); + return () => clearTimeout(timer); + }, [showSurvey]); + + + const updateRegistry = (registry: PrefillsRegistry) => { + SetPrefillRegistry(registry); + if(typeof(registry.current) != "undefined") { + const e = registry.prefills[registry.current]; + setPrefillValues(e.data); + } else { + setPrefillValues([]); + } + setShowSurvey(false); + } + + const handleSaveAsPrefill = (data: SurveySingleItemResponse[]) => { + const name = 'response_'+ format(Date.now(), 'yyyy-MM-dd hh:mm:ss'); + const n = prefillRegistry.prefills.push({name: name, data: data}); + prefillRegistry.current = n - 1; + updateRegistry({'current': n-1, prefills: prefillRegistry.prefills }); + } + + const handleReset = () => { + console.log('reset'); + setSurveyAndContext({survey: props.survey, context: surveyContext}); + setShowSurvey(false); + }; + const surveyDefinition = props.survey.surveyDefinition; return + {showSurvey ? props.onExit()} + onReset={handleReset} customResponseComponents={props.customResponseComponents} - /> + onSaveAsPrefill={handleSaveAsPrefill} + />: + + } { - if (file) { - const reader = new FileReader() - - reader.onabort = () => console.log('file reading was aborted') - reader.onerror = () => console.log('file reading has failed') - reader.onload = () => { - // Do whatever you want with the file contents - const res = reader.result; - if (!res || typeof (res) !== 'string') { - console.error('TODO: handle file upload error') - return; - } - const content = JSON.parse(res); - setprefillFile(file); - setPrefillValues(content); - - } - reader.readAsText(file) - } else { - setprefillFile(file); - setPrefillValues([]); - } - + prefillRegistry={prefillRegistry} + onPrefillChanged={(r: PrefillsRegistry) => { + updateRegistry(r); }} currentSurveyContext={surveyContext} onSurveyContextChanged={(config) => setSurveyContext(config)} @@ -75,29 +97,6 @@ const SurveyMenu: React.FC = (props) => { onSimulatorUIConfigChanged={(config) => setSimulatorUIConfig(config)} /> - -

Survey Infos

-

Survey Card

- { - props.survey.props ? : null - } -
; }; diff --git a/src/components/SurveyServiceLoader.tsx b/src/components/SurveyServiceLoader.tsx index e1608a0..1eabf16 100644 --- a/src/components/SurveyServiceLoader.tsx +++ b/src/components/SurveyServiceLoader.tsx @@ -2,7 +2,7 @@ import { Survey } from 'survey-engine/data_types'; import { Badge, ListGroup, Accordion } from 'react-bootstrap'; import useLoadJSON from '../hooks/useLoadJSON'; import { useEffect, useState } from 'react'; -import Card from './Card'; +import CardTitled from './Card'; import { parseISO } from 'date-fns'; interface localisedString { @@ -177,9 +177,9 @@ const SurveyServiceLoader: React.FC = (props) => { } return ( - + - + ); }; diff --git a/src/components/SurveySimulator.tsx b/src/components/SurveySimulator.tsx index 6e930b9..1430650 100644 --- a/src/components/SurveySimulator.tsx +++ b/src/components/SurveySimulator.tsx @@ -1,12 +1,14 @@ import { AlertBox, SurveyView, Dialog, DialogBtn } from 'case-web-ui'; import React, { useState } from 'react'; -import { Button } from 'react-bootstrap'; +import { Button, Card, CardBody } from 'react-bootstrap'; import { Survey, SurveyContext, SurveySingleItemResponse } from 'survey-engine/data_types'; import { nl, nlBE, fr, de, it, da, es, pt } from 'date-fns/locale'; import { SurveyEngineCore } from 'survey-engine/engine'; import { EngineState, SurveyInspector } from './SurveyInspector'; import { CustomSurveyResponseComponent } from 'case-web-ui'; import clsx from 'clsx'; +import { SurveyAndContext } from '../types'; +import SurveyInfo from './SurveyInfo'; const dateLocales = [ { code: 'nl', locale: nl, format: 'dd-MM-yyyy' }, @@ -35,14 +37,13 @@ export interface SimulatorUIConfig { interface SurveySimulatorProps { config: SimulatorUIConfig; - surveyAndContext?: { - survey: Survey; - context: SurveyContext; - }; + surveyAndContext?: SurveyAndContext; prefills?: SurveySingleItemResponse[]; selectedLanguage?: string; customResponseComponents?:CustomSurveyResponseComponent[] onExit: () => void; + onSaveAsPrefill: (p: SurveySingleItemResponse[])=>void; + onReset: () => void; } @@ -71,13 +72,13 @@ const SurveySimulator: React.FC = (props) => { } } - const toggleEvaluator = () => { setShowEvaluator(!showEvaluator) } const surveySubmitDialog = { setSurveyResponseData([]); setOpenSurveyEndDialog(false); @@ -98,6 +99,18 @@ const SurveySimulator: React.FC = (props) => { label="Exit without Save" outlined={true} /> + { + const response = surveyResponseData; + setSurveyResponseData([]); + props.onSaveAsPrefill(response); + props.onReset(); + setOpenSurveyEndDialog(false); + }} + label="Store as prefill for next survey" + outlined={true} + /> { @@ -112,12 +125,16 @@ const SurveySimulator: React.FC = (props) => { setOpenSurveyEndDialog(false) props.onExit() }} - label="Save and Exit" + label="Save as file and Exit" /> + console.log('prefills', props.prefills); + + const languageCode = props.selectedLanguage ? props.selectedLanguage : 'en'; + return (
@@ -126,21 +143,24 @@ const SurveySimulator: React.FC = (props) => { props.onExit(); }}} variant="warning" className="me-1"> Exit - + +
- {props.surveyAndContext ? + {props.surveyAndContext ? ( +
+ { setSurveyResponseData(responses.slice()) setOpenSurveyEndDialog(true); @@ -152,7 +172,8 @@ const SurveySimulator: React.FC = (props) => { invalidResponseText={props.config.texts.invalidResponseText} dateLocales={dateLocales} customResponseComponents={props.customResponseComponents} - /> : + /> +
) : Date: Thu, 17 Oct 2024 13:07:56 +0200 Subject: [PATCH 17/17] language selection in simulator, auto select from default language if available --- src/App.tsx | 39 ++++++++++++++---------- src/components/NavbarComp.tsx | 4 +-- src/components/SurveyLoader.tsx | 2 +- src/components/SurveyMenu.tsx | 4 +-- src/components/SurveySimulator.tsx | 48 +++++++++++++++++++++++------- src/index.scss | 7 +++++ src/localConfig.ts | 2 +- src/types/index.ts | 3 +- src/types/language.ts | 4 +++ 9 files changed, 80 insertions(+), 33 deletions(-) create mode 100644 src/types/language.ts diff --git a/src/App.tsx b/src/App.tsx index b1dfbd7..d8c0e7b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,14 +5,17 @@ import SurveyLoader from './components/SurveyLoader'; import SurveyMenu from './components/SurveyMenu'; import SurveyServiceLoader from './components/SurveyServiceLoader'; import { registerCustomComponents, registerParticipantFlags } from './localConfig'; + interface AppState { - selectedLanguage?: string; - languageCodes?: string[]; + selectedAppLanguage?: string; // Language of the Application (for future translation) surveyKey?: string; survey?: Survey; screen: Screens; } +// Application translations, currently only english is provided. +const availableAppLanguages : string[] = ['en']; + const customResponseComponents = registerCustomComponents(); const participantFlags = registerParticipantFlags(); @@ -26,6 +29,18 @@ const surveyProviderUrl = process.env.REACT_APP_SURVEY_URL ?? ""; console.log("Using provider "+ surveyProviderUrl); +// Default languages to show in the survey. If several use the first available in the survey. +// You can customize the default survey languages by definiing REACT_APP_DEFAULT_SURVEY_LANGUAGES env variable +const defaultSurveyLanguages : string[] = (()=>{ + let defaultLang: string[] = ['en']; + const envLang = process.env.REACT_APP_DEFAULT_SURVEY_LANGUAGES ?? ""; + if(envLang) { + defaultLang = envLang.split(',').map(v => v.trim()); + } + return defaultLang; +})(); + + const App: React.FC = () => { const [appState, setAppState] = useState({ ...initialState @@ -37,11 +52,6 @@ const App: React.FC = () => { return; } - const languageCodes = surveyObject.props?.name?.map(o => o.code); - if (!languageCodes || languageCodes.length < 1) { - alert('Languages cannot be extracted'); - return; - } const surveyDef = surveyObject.surveyDefinition; @@ -49,8 +59,7 @@ const App: React.FC = () => { setAppState({ ...initialState, - selectedLanguage: languageCodes[0], - languageCodes: languageCodes, + selectedAppLanguage: availableAppLanguages[0], surveyKey: surveyKey, screen: 'menu', survey: surveyObject @@ -78,22 +87,22 @@ const App: React.FC = () => { }}>
- { surveyProviderUrl ? : null } +
case 'menu': - if (!appState.selectedLanguage || !appState.survey) { + if (!appState.selectedAppLanguage || !appState.survey) { reset(); return null; } return { @@ -110,13 +119,13 @@ const App: React.FC = () => { }}> { setAppState(prev => { return { ...prev, - selectedLanguage: code + selectedAppLanguage: code } }) }} diff --git a/src/components/NavbarComp.tsx b/src/components/NavbarComp.tsx index 3db204b..55f932f 100644 --- a/src/components/NavbarComp.tsx +++ b/src/components/NavbarComp.tsx @@ -12,7 +12,7 @@ const NavbarComp: React.FC = (props) => { const languageSelector = (codes: string[]) => { return (
- console.log('prefills', props.prefills); + const languageSelector = (code:string, index:number) => { + const btn = code == languageCode ? 'btn-primary' : 'btn-info'; + return
  • setLanguageCode(code)}>{code}
  • ; + } - const languageCode = props.selectedLanguage ? props.selectedLanguage : 'en'; + console.log('prefills', props.prefills); return (
    - - - - - -
    + + + + + +
    + Languages : +
      + { languageCodes.map(languageSelector) } +
    +
    +
    diff --git a/src/index.scss b/src/index.scss index dc1f4c6..de3e91c 100644 --- a/src/index.scss +++ b/src/index.scss @@ -17,3 +17,10 @@ } } +.list-inline { + display: inline; +} + +.list-inline li { + display: inline-block; +} diff --git a/src/localConfig.ts b/src/localConfig.ts index bafc421..d4839e9 100644 --- a/src/localConfig.ts +++ b/src/localConfig.ts @@ -8,4 +8,4 @@ export const registerCustomComponents = () : CustomSurveyResponseComponent[] | u export const registerParticipantFlags = () : ParticipantFlags => { return {}; -} +} \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index da9015d..ae37547 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,2 +1,3 @@ export * from './flags'; -export * from './survey'; \ No newline at end of file +export * from './survey'; +export * from './language'; \ No newline at end of file diff --git a/src/types/language.ts b/src/types/language.ts new file mode 100644 index 0000000..922cd04 --- /dev/null +++ b/src/types/language.ts @@ -0,0 +1,4 @@ +export interface LanguageConfig { + availableLanguages : string[] + defaultLanguage: string +} \ No newline at end of file