Loading editor/package.json +1 −1 Original line number Diff line number Diff line { "name": "@inject/editor", "version": "0.19.0", "version": "0.19.1", "description": "Editor module to Inject Exercise Platform", "main": "index.js", "license": "MIT", Loading editor/src/assets/pageContent/injectSpecification.ts +20 −0 Original line number Diff line number Diff line Loading @@ -119,9 +119,29 @@ export const QUESTIONNAIRE_QUESTION_FORM: Form = { tooltip: '', optional: true, }, correctMilestones: { label: 'Correct Milestones', tooltip: '', optional: true, }, incorrectMilestones: { label: 'Incorrect Milestones', tooltip: '', optional: true, }, note: { label: 'Note', tooltip: '', optional: true, }, correctAnswer: { label: 'Correct answer', tooltip: '', optional: false, }, regex: { label: 'Reqex', tooltip: '', optional: true, }, } editor/src/components/InjectSpecification/QuestionnaireForm/AutoFreeformForm.tsx 0 → 100644 +293 −0 Original line number Diff line number Diff line import type { ButtonProps } from '@blueprintjs/core' import { Button, Dialog, DialogBody, DialogFooter, Divider, InputGroup, } from '@blueprintjs/core' import { css } from '@emotion/css' import type { ZustandSetterSingle } from '@inject/shared' import { compute, computed, create, notify, ShallowGet, ShallowGetSet, } from '@inject/shared' import type { FC } from 'react' import { memo, useCallback, useEffect, useState } from 'react' import { GENERIC_CONTENT } from '../../../assets/generalContent' import { QUESTIONNAIRE_QUESTION_FORM } from '../../../assets/pageContent/injectSpecification' import { db } from '../../../indexeddb/db' import { patchQuestionnaireMilestone, updateQuestionnaireQuestion, } from '../../../indexeddb/operations' import type { QuestionnaireQuestion, QuestionnaireQuestionAutoFreeForm, } from '../../../indexeddb/types' import TooltipLabel from '../../Tooltips/TooltipLabel' import TooltipSwitch from '../../Tooltips/TooltipSwitch' import QuestionnaireMilestoneForm from './QuestionnaireMilestoneForm' interface QuestionnaireQuestionFormProps { questionnaireQuestion?: QuestionnaireQuestion autoFreeformQuestion?: QuestionnaireQuestionAutoFreeForm injectInfoId: number buttonProps: ButtonProps } type State< S = { text: string valid: boolean multiline: boolean note?: string update: (id?: number) => Promise<void> correctAnswer: string regex: boolean }, > = S & ZustandSetterSingle<S> const AutoFreeformForm: FC<QuestionnaireQuestionFormProps> = ({ questionnaireQuestion, autoFreeformQuestion, injectInfoId, buttonProps, }) => { const [isOpen, setIsOpen] = useState(false) const state = create<State>( computed((set, get) => ({ text: '', correctAnswer: '', multiline: false, note: '', regex: false, ...compute(get, state => ({ valid: !!state.text && !!state.correctAnswer, })), set: (item, value) => set({ [item]: value }), update: async id => { const { multiline, text, note, correctAnswer, regex } = get() const correctMilestones = ( await db.questionnaireMilestones .where({ injectInfoId }) .and( ms => (ms.questionId === id || ms.questionId === undefined) && ms.correct === true ) .toArray() ).map(it => it.id) const incorrectMilestones = ( await db.questionnaireMilestones .where({ injectInfoId }) .and( ms => (ms.questionId === id || ms.questionId === undefined) && ms.correct === false ) .toArray() ).map(it => it.id) const newId = await updateQuestionnaireQuestion( { id, injectInfoId, type: 'auto-free-form', text, note, }, { correctAnswer, multiline, regex, correctMilestones, incorrectMilestones, } ) await patchQuestionnaireMilestone( [...correctMilestones, ...incorrectMilestones], injectInfoId, newId ) }, })) ) useEffect(() => { state.setState({ text: questionnaireQuestion?.text, multiline: autoFreeformQuestion?.multiline, note: questionnaireQuestion?.note, correctAnswer: autoFreeformQuestion?.correctAnswer, regex: autoFreeformQuestion?.regex, }) }, [isOpen, state, questionnaireQuestion, autoFreeformQuestion]) const handleUpdateButton = useCallback(async () => { try { state.getState().update(questionnaireQuestion?.id) setIsOpen(false) } catch (error) { const { text } = state.getState() notify( `Failed to update questionnaire question '${text}': ${error}`, JSON.stringify(error), { intent: 'danger', } ) } }, [questionnaireQuestion?.id, state]) return ( <> <Button {...buttonProps} onClick={() => setIsOpen(true)} /> <Dialog isOpen={isOpen} onClose={() => setIsOpen(false)} icon={questionnaireQuestion ? 'edit' : 'plus'} title={ questionnaireQuestion ? 'Edit questionnaire question' : 'New questionnaire question' } > <DialogBody> <TooltipLabel label={QUESTIONNAIRE_QUESTION_FORM.text}> <ShallowGetSet store={state} get={({ text }) => text} set={({ set }) => set} > {(text, set) => ( <InputGroup placeholder='Input text' value={text} onChange={e => set('text', e.target.value)} /> )} </ShallowGetSet> </TooltipLabel> <ShallowGetSet store={state} get={({ multiline }) => multiline} set={({ set }) => set} > {(multiline, set) => ( <TooltipSwitch label={QUESTIONNAIRE_QUESTION_FORM.multiline} switchProps={{ checked: multiline, onChange: () => set('multiline', !multiline), }} /> )} </ShallowGetSet> <Divider className={css` margin-block: 1rem !important; `} /> <TooltipLabel label={QUESTIONNAIRE_QUESTION_FORM.correctAnswer}> <ShallowGetSet store={state} get={({ correctAnswer }) => correctAnswer} set={({ set }) => set} > {(correctAnswer, set) => ( <InputGroup placeholder='Input correct answer' value={correctAnswer} onChange={e => set('correctAnswer', e.target.value)} /> )} </ShallowGetSet> </TooltipLabel> <ShallowGetSet store={state} get={({ regex }) => regex} set={({ set }) => set} > {(regex, set) => ( <TooltipSwitch label={QUESTIONNAIRE_QUESTION_FORM.regex} switchProps={{ checked: regex, onChange: () => set('regex', !regex), }} /> )} </ShallowGetSet> <Divider className={css` margin-block: 1rem !important; `} /> <TooltipLabel label={QUESTIONNAIRE_QUESTION_FORM.note}> <ShallowGetSet store={state} get={({ note }) => note} set={({ set }) => set} > {(note, set) => ( <InputGroup placeholder='Input note' value={note} onChange={e => set('note', e.target.value)} /> )} </ShallowGetSet> </TooltipLabel> <QuestionnaireMilestoneForm questionId={questionnaireQuestion?.id} injectInfoId={injectInfoId} title={QUESTIONNAIRE_QUESTION_FORM.correctMilestones.label} correct className={css` margin-bottom: 1rem; `} /> <QuestionnaireMilestoneForm questionId={questionnaireQuestion?.id} injectInfoId={injectInfoId} title={QUESTIONNAIRE_QUESTION_FORM.incorrectMilestones.label} correct={false} /> </DialogBody> <DialogFooter actions={ <ShallowGet store={state} get={({ valid }) => valid}> {isValid => ( <Button disabled={!isValid} onClick={() => handleUpdateButton()} intent='primary' icon={questionnaireQuestion ? 'edit' : 'plus'} text={ questionnaireQuestion ? GENERIC_CONTENT.buttons.save : GENERIC_CONTENT.buttons.add } /> )} </ShallowGet> } /> </Dialog> </> ) } export default memo(AutoFreeformForm) editor/src/components/InjectSpecification/QuestionnaireForm/FreeformForm.tsx +5 −4 Original line number Diff line number Diff line Loading @@ -30,7 +30,7 @@ import type { } from '../../../indexeddb/types' import TooltipLabel from '../../Tooltips/TooltipLabel' import TooltipSwitch from '../../Tooltips/TooltipSwitch' import RelatedMilestoneForm from './RelatedMilestoneForm' import QuestionnaireMilestoneForm from './QuestionnaireMilestoneForm' interface QuestionnaireQuestionFormProps { questionnaireQuestion?: QuestionnaireQuestion Loading Loading @@ -99,9 +99,9 @@ const FreeformForm: FC<QuestionnaireQuestionFormProps> = ({ useEffect(() => { state.setState({ text: questionnaireQuestion?.text, text: questionnaireQuestion?.text ?? '', multiline: freeformQuestion?.multiline, note: questionnaireQuestion?.note, note: questionnaireQuestion?.note ?? '', }) }, [isOpen, state, questionnaireQuestion, freeformQuestion]) Loading Loading @@ -166,9 +166,10 @@ const FreeformForm: FC<QuestionnaireQuestionFormProps> = ({ )} </ShallowGetSet> <RelatedMilestoneForm <QuestionnaireMilestoneForm questionId={freeformQuestion?.id} injectInfoId={injectInfoId} title={QUESTIONNAIRE_QUESTION_FORM.relatedMilestones.label} /> <TooltipLabel label={QUESTIONNAIRE_QUESTION_FORM.note}> <ShallowGetSet Loading editor/src/components/InjectSpecification/QuestionnaireForm/RelatedMilestoneForm.tsx→editor/src/components/InjectSpecification/QuestionnaireForm/QuestionnaireMilestoneForm.tsx +125 −0 Original line number Diff line number Diff line Loading @@ -6,11 +6,11 @@ import { Icon, InputGroup, } from '@blueprintjs/core' import { css, cx } from '@emotion/css' import { useLiveQuery } from 'dexie-react-hooks' import { sortBy } from 'lodash' import type { FC, FormEventHandler } from 'react' import { memo, useRef } from 'react' import { QUESTIONNAIRE_QUESTION_FORM } from '../../../assets/pageContent/injectSpecification' import { db } from '../../../indexeddb/db' import { addQuestionnaireMilestone, Loading @@ -31,7 +31,6 @@ const RelatedMilestoneAdder: FC<{ } return ( <> <form onSubmit={cb}> <ControlGroup fill> <InputGroup type='text' placeholder='Milestone name' inputRef={ref} /> Loading @@ -40,26 +39,47 @@ const RelatedMilestoneAdder: FC<{ </Button> </ControlGroup> </form> </> ) } // TODO: make the state temporary and write only on onSave const RelatedMilestoneForm: FC<{ const QuestionnaireMilestoneForm: FC<{ questionId?: number injectInfoId: number }> = ({ injectInfoId, questionId }) => { title: string correct?: boolean className?: string }> = ({ injectInfoId, questionId, title, correct, className }) => { const milestones = useLiveQuery(() => db.questionnaireMilestones .where({ injectInfoId }) .and(ms => ms.questionId === questionId || ms.questionId === undefined) .and( ms => (ms.questionId === questionId || ms.questionId === undefined) && ms.correct === correct ) .toArray() ) return ( <> <span style={{ flexGrow: '1', marginBottom: '0' }}> {QUESTIONNAIRE_QUESTION_FORM.relatedMilestones.label} <div className={cx( css` display: flex; flex-direction: column; margin-bottom: 1rem; `, className )} > <span className={css` padding-bottom: 0.375rem !important; `} > {title} </span> {milestones && milestones.length > 0 && ( <CardList compact> {sortBy(milestones || [], ms => ms.name).map( ({ id, name, questionId }) => ( Loading Loading @@ -87,18 +107,19 @@ const RelatedMilestoneForm: FC<{ ) )} </CardList> )} <RelatedMilestoneAdder onAdd={name => { addQuestionnaireMilestone({ name, injectInfoId, questionId, correct, }) }} /> </> </div> ) } export default memo(RelatedMilestoneForm) export default memo(QuestionnaireMilestoneForm) Loading
editor/package.json +1 −1 Original line number Diff line number Diff line { "name": "@inject/editor", "version": "0.19.0", "version": "0.19.1", "description": "Editor module to Inject Exercise Platform", "main": "index.js", "license": "MIT", Loading
editor/src/assets/pageContent/injectSpecification.ts +20 −0 Original line number Diff line number Diff line Loading @@ -119,9 +119,29 @@ export const QUESTIONNAIRE_QUESTION_FORM: Form = { tooltip: '', optional: true, }, correctMilestones: { label: 'Correct Milestones', tooltip: '', optional: true, }, incorrectMilestones: { label: 'Incorrect Milestones', tooltip: '', optional: true, }, note: { label: 'Note', tooltip: '', optional: true, }, correctAnswer: { label: 'Correct answer', tooltip: '', optional: false, }, regex: { label: 'Reqex', tooltip: '', optional: true, }, }
editor/src/components/InjectSpecification/QuestionnaireForm/AutoFreeformForm.tsx 0 → 100644 +293 −0 Original line number Diff line number Diff line import type { ButtonProps } from '@blueprintjs/core' import { Button, Dialog, DialogBody, DialogFooter, Divider, InputGroup, } from '@blueprintjs/core' import { css } from '@emotion/css' import type { ZustandSetterSingle } from '@inject/shared' import { compute, computed, create, notify, ShallowGet, ShallowGetSet, } from '@inject/shared' import type { FC } from 'react' import { memo, useCallback, useEffect, useState } from 'react' import { GENERIC_CONTENT } from '../../../assets/generalContent' import { QUESTIONNAIRE_QUESTION_FORM } from '../../../assets/pageContent/injectSpecification' import { db } from '../../../indexeddb/db' import { patchQuestionnaireMilestone, updateQuestionnaireQuestion, } from '../../../indexeddb/operations' import type { QuestionnaireQuestion, QuestionnaireQuestionAutoFreeForm, } from '../../../indexeddb/types' import TooltipLabel from '../../Tooltips/TooltipLabel' import TooltipSwitch from '../../Tooltips/TooltipSwitch' import QuestionnaireMilestoneForm from './QuestionnaireMilestoneForm' interface QuestionnaireQuestionFormProps { questionnaireQuestion?: QuestionnaireQuestion autoFreeformQuestion?: QuestionnaireQuestionAutoFreeForm injectInfoId: number buttonProps: ButtonProps } type State< S = { text: string valid: boolean multiline: boolean note?: string update: (id?: number) => Promise<void> correctAnswer: string regex: boolean }, > = S & ZustandSetterSingle<S> const AutoFreeformForm: FC<QuestionnaireQuestionFormProps> = ({ questionnaireQuestion, autoFreeformQuestion, injectInfoId, buttonProps, }) => { const [isOpen, setIsOpen] = useState(false) const state = create<State>( computed((set, get) => ({ text: '', correctAnswer: '', multiline: false, note: '', regex: false, ...compute(get, state => ({ valid: !!state.text && !!state.correctAnswer, })), set: (item, value) => set({ [item]: value }), update: async id => { const { multiline, text, note, correctAnswer, regex } = get() const correctMilestones = ( await db.questionnaireMilestones .where({ injectInfoId }) .and( ms => (ms.questionId === id || ms.questionId === undefined) && ms.correct === true ) .toArray() ).map(it => it.id) const incorrectMilestones = ( await db.questionnaireMilestones .where({ injectInfoId }) .and( ms => (ms.questionId === id || ms.questionId === undefined) && ms.correct === false ) .toArray() ).map(it => it.id) const newId = await updateQuestionnaireQuestion( { id, injectInfoId, type: 'auto-free-form', text, note, }, { correctAnswer, multiline, regex, correctMilestones, incorrectMilestones, } ) await patchQuestionnaireMilestone( [...correctMilestones, ...incorrectMilestones], injectInfoId, newId ) }, })) ) useEffect(() => { state.setState({ text: questionnaireQuestion?.text, multiline: autoFreeformQuestion?.multiline, note: questionnaireQuestion?.note, correctAnswer: autoFreeformQuestion?.correctAnswer, regex: autoFreeformQuestion?.regex, }) }, [isOpen, state, questionnaireQuestion, autoFreeformQuestion]) const handleUpdateButton = useCallback(async () => { try { state.getState().update(questionnaireQuestion?.id) setIsOpen(false) } catch (error) { const { text } = state.getState() notify( `Failed to update questionnaire question '${text}': ${error}`, JSON.stringify(error), { intent: 'danger', } ) } }, [questionnaireQuestion?.id, state]) return ( <> <Button {...buttonProps} onClick={() => setIsOpen(true)} /> <Dialog isOpen={isOpen} onClose={() => setIsOpen(false)} icon={questionnaireQuestion ? 'edit' : 'plus'} title={ questionnaireQuestion ? 'Edit questionnaire question' : 'New questionnaire question' } > <DialogBody> <TooltipLabel label={QUESTIONNAIRE_QUESTION_FORM.text}> <ShallowGetSet store={state} get={({ text }) => text} set={({ set }) => set} > {(text, set) => ( <InputGroup placeholder='Input text' value={text} onChange={e => set('text', e.target.value)} /> )} </ShallowGetSet> </TooltipLabel> <ShallowGetSet store={state} get={({ multiline }) => multiline} set={({ set }) => set} > {(multiline, set) => ( <TooltipSwitch label={QUESTIONNAIRE_QUESTION_FORM.multiline} switchProps={{ checked: multiline, onChange: () => set('multiline', !multiline), }} /> )} </ShallowGetSet> <Divider className={css` margin-block: 1rem !important; `} /> <TooltipLabel label={QUESTIONNAIRE_QUESTION_FORM.correctAnswer}> <ShallowGetSet store={state} get={({ correctAnswer }) => correctAnswer} set={({ set }) => set} > {(correctAnswer, set) => ( <InputGroup placeholder='Input correct answer' value={correctAnswer} onChange={e => set('correctAnswer', e.target.value)} /> )} </ShallowGetSet> </TooltipLabel> <ShallowGetSet store={state} get={({ regex }) => regex} set={({ set }) => set} > {(regex, set) => ( <TooltipSwitch label={QUESTIONNAIRE_QUESTION_FORM.regex} switchProps={{ checked: regex, onChange: () => set('regex', !regex), }} /> )} </ShallowGetSet> <Divider className={css` margin-block: 1rem !important; `} /> <TooltipLabel label={QUESTIONNAIRE_QUESTION_FORM.note}> <ShallowGetSet store={state} get={({ note }) => note} set={({ set }) => set} > {(note, set) => ( <InputGroup placeholder='Input note' value={note} onChange={e => set('note', e.target.value)} /> )} </ShallowGetSet> </TooltipLabel> <QuestionnaireMilestoneForm questionId={questionnaireQuestion?.id} injectInfoId={injectInfoId} title={QUESTIONNAIRE_QUESTION_FORM.correctMilestones.label} correct className={css` margin-bottom: 1rem; `} /> <QuestionnaireMilestoneForm questionId={questionnaireQuestion?.id} injectInfoId={injectInfoId} title={QUESTIONNAIRE_QUESTION_FORM.incorrectMilestones.label} correct={false} /> </DialogBody> <DialogFooter actions={ <ShallowGet store={state} get={({ valid }) => valid}> {isValid => ( <Button disabled={!isValid} onClick={() => handleUpdateButton()} intent='primary' icon={questionnaireQuestion ? 'edit' : 'plus'} text={ questionnaireQuestion ? GENERIC_CONTENT.buttons.save : GENERIC_CONTENT.buttons.add } /> )} </ShallowGet> } /> </Dialog> </> ) } export default memo(AutoFreeformForm)
editor/src/components/InjectSpecification/QuestionnaireForm/FreeformForm.tsx +5 −4 Original line number Diff line number Diff line Loading @@ -30,7 +30,7 @@ import type { } from '../../../indexeddb/types' import TooltipLabel from '../../Tooltips/TooltipLabel' import TooltipSwitch from '../../Tooltips/TooltipSwitch' import RelatedMilestoneForm from './RelatedMilestoneForm' import QuestionnaireMilestoneForm from './QuestionnaireMilestoneForm' interface QuestionnaireQuestionFormProps { questionnaireQuestion?: QuestionnaireQuestion Loading Loading @@ -99,9 +99,9 @@ const FreeformForm: FC<QuestionnaireQuestionFormProps> = ({ useEffect(() => { state.setState({ text: questionnaireQuestion?.text, text: questionnaireQuestion?.text ?? '', multiline: freeformQuestion?.multiline, note: questionnaireQuestion?.note, note: questionnaireQuestion?.note ?? '', }) }, [isOpen, state, questionnaireQuestion, freeformQuestion]) Loading Loading @@ -166,9 +166,10 @@ const FreeformForm: FC<QuestionnaireQuestionFormProps> = ({ )} </ShallowGetSet> <RelatedMilestoneForm <QuestionnaireMilestoneForm questionId={freeformQuestion?.id} injectInfoId={injectInfoId} title={QUESTIONNAIRE_QUESTION_FORM.relatedMilestones.label} /> <TooltipLabel label={QUESTIONNAIRE_QUESTION_FORM.note}> <ShallowGetSet Loading
editor/src/components/InjectSpecification/QuestionnaireForm/RelatedMilestoneForm.tsx→editor/src/components/InjectSpecification/QuestionnaireForm/QuestionnaireMilestoneForm.tsx +125 −0 Original line number Diff line number Diff line Loading @@ -6,11 +6,11 @@ import { Icon, InputGroup, } from '@blueprintjs/core' import { css, cx } from '@emotion/css' import { useLiveQuery } from 'dexie-react-hooks' import { sortBy } from 'lodash' import type { FC, FormEventHandler } from 'react' import { memo, useRef } from 'react' import { QUESTIONNAIRE_QUESTION_FORM } from '../../../assets/pageContent/injectSpecification' import { db } from '../../../indexeddb/db' import { addQuestionnaireMilestone, Loading @@ -31,7 +31,6 @@ const RelatedMilestoneAdder: FC<{ } return ( <> <form onSubmit={cb}> <ControlGroup fill> <InputGroup type='text' placeholder='Milestone name' inputRef={ref} /> Loading @@ -40,26 +39,47 @@ const RelatedMilestoneAdder: FC<{ </Button> </ControlGroup> </form> </> ) } // TODO: make the state temporary and write only on onSave const RelatedMilestoneForm: FC<{ const QuestionnaireMilestoneForm: FC<{ questionId?: number injectInfoId: number }> = ({ injectInfoId, questionId }) => { title: string correct?: boolean className?: string }> = ({ injectInfoId, questionId, title, correct, className }) => { const milestones = useLiveQuery(() => db.questionnaireMilestones .where({ injectInfoId }) .and(ms => ms.questionId === questionId || ms.questionId === undefined) .and( ms => (ms.questionId === questionId || ms.questionId === undefined) && ms.correct === correct ) .toArray() ) return ( <> <span style={{ flexGrow: '1', marginBottom: '0' }}> {QUESTIONNAIRE_QUESTION_FORM.relatedMilestones.label} <div className={cx( css` display: flex; flex-direction: column; margin-bottom: 1rem; `, className )} > <span className={css` padding-bottom: 0.375rem !important; `} > {title} </span> {milestones && milestones.length > 0 && ( <CardList compact> {sortBy(milestones || [], ms => ms.name).map( ({ id, name, questionId }) => ( Loading Loading @@ -87,18 +107,19 @@ const RelatedMilestoneForm: FC<{ ) )} </CardList> )} <RelatedMilestoneAdder onAdd={name => { addQuestionnaireMilestone({ name, injectInfoId, questionId, correct, }) }} /> </> </div> ) } export default memo(RelatedMilestoneForm) export default memo(QuestionnaireMilestoneForm)