Skip to content

Commit

Permalink
(feat) O3-3946: Add support for markdown questions (#355)
Browse files Browse the repository at this point in the history
* feat: Add support from markdown questions

* Fix conditional statements

---------

Co-authored-by: Nethmi Rodrigo <[email protected]>
  • Loading branch information
Twiineenock and NethmiRodrigo authored Dec 6, 2024
1 parent 76df205 commit eb469f8
Show file tree
Hide file tree
Showing 17 changed files with 641 additions and 133 deletions.
2 changes: 1 addition & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
**/dist/*
**/node_modules/*
**/*.d.tsx
__mocks__/*
__mocks__/*
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"import/no-duplicates": "error",
"react-hooks/exhaustive-deps": "warn",
"react-hooks/rules-of-hooks": "error",
"@typescript-eslint/no-explicit-any": "off",
// not hugely concerned about accidental implicit type coercions for now https://typescript-eslint.io/rules/no-base-to-string
"@typescript-eslint/no-base-to-string": "off",
// The following rules need `noImplicitAny` to be set to `true` in our tsconfig. They are too restrictive for now, but should be reconsidered in future
Expand Down
5 changes: 5 additions & 0 deletions __mocks__/react-markdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react';

export default function ({ children }) {
return <>{children}</>;
}
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {
'lodash-es': 'lodash',
'^dexie$': '<rootDir>/node_modules/dexie',
'^react-i18next$': '<rootDir>/__mocks__/react-i18next.js',
'react-markdown': '<rootDir>/__mocks__/react-markdown.tsx',
},
setupFilesAfterEnv: ['<rootDir>/src/setup-tests.ts'],
testEnvironment: 'jsdom',
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
"fuzzy": "^0.1.3",
"lodash-es": "^4.17.21",
"react-ace": "^11.0.1",
"react-markdown": "^9.0.1",
"react-mde": "^11.5.0",
"sass": "^1.67.0"
},
"peerDependencies": {
Expand Down
104 changes: 55 additions & 49 deletions src/components/interactive-builder/add-question.modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { useConceptLookup } from '../../hooks/useConceptLookup';
import { usePatientIdentifierTypes } from '../../hooks/usePatientIdentifierTypes';
import { usePersonAttributeTypes } from '../../hooks/usePersonAttributeTypes';
import { useProgramWorkStates, usePrograms } from '../../hooks/useProgramStates';
import MarkdownQuestion from './markdown-question.component';
import styles from './question-modal.scss';

interface AddQuestionModalProps {
Expand Down Expand Up @@ -109,6 +110,7 @@ const AddQuestionModal: React.FC<AddQuestionModalProps> = ({
const [min, setMin] = useState('');
const [questionId, setQuestionId] = useState('');
const [questionLabel, setQuestionLabel] = useState('');
const [questionValue, setQuestionValue] = useState('');
const [questionType, setQuestionType] = useState<QuestionType | null>(null);
const [rows, setRows] = useState('');
const [selectedAnswers, setSelectedAnswers] = useState<
Expand Down Expand Up @@ -149,7 +151,6 @@ const AddQuestionModal: React.FC<AddQuestionModalProps> = ({
const [toggleLabelFalse, setToggleLabelFalse] = useState('');

const renderTypeOptions = {
control: ['text'],
encounterDatetime: ['date'],
encounterLocation: ['ui-select-extended'],
encounterProvider: ['ui-select-extended'],
Expand Down Expand Up @@ -250,8 +251,9 @@ const AddQuestionModal: React.FC<AddQuestionModalProps> = ({
const computedQuestionId = `question${questionIndex + 1}Section${sectionIndex + 1}Page-${pageIndex + 1}`;

const newQuestion = {
label: questionLabel,
type: questionType,
...(questionLabel && {label: questionLabel}),
...((renderingType === 'markdown') && {value: questionValue}),
type: questionType ? questionType : 'control',
required: isQuestionRequired,
id: questionId ?? computedQuestionId,
...((renderingType === 'date' || renderingType === 'datetime') &&
Expand Down Expand Up @@ -361,14 +363,15 @@ const AddQuestionModal: React.FC<AddQuestionModalProps> = ({
<ModalBody hasScrollingContent>
<FormGroup legendText={''}>
<Stack gap={5}>
<TextInput
{renderingType === 'markdown' ? <MarkdownQuestion onValueChange={setQuestionValue}/> : (
<TextInput
id="questionLabel"
labelText={<RequiredLabel isRequired={isQuestionRequired} text={t('questionLabel', 'Label')} t={t} />}
placeholder={t('labelPlaceholder', 'e.g. Type of Anaesthesia')}
value={questionLabel}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setQuestionLabel(event.target.value)}
required
/>
)}

<TextInput
id="questionId"
Expand Down Expand Up @@ -400,45 +403,48 @@ const AddQuestionModal: React.FC<AddQuestionModalProps> = ({
required
/>

<RadioButtonGroup
defaultSelected="optional"
name="isQuestionRequired"
legendText={t(
'isQuestionRequiredOrOptional',
'Is this question a required or optional field? Required fields must be answered before the form can be submitted.',
)}
>
<RadioButton
id="questionIsNotRequired"
defaultChecked={true}
labelText={t('optional', 'Optional')}
onClick={() => setIsQuestionRequired(false)}
value="optional"
/>
<RadioButton
id="questionIsRequired"
defaultChecked={false}
labelText={t('required', 'Required')}
onClick={() => setIsQuestionRequired(true)}
value="required"
/>
</RadioButtonGroup>

<Select
value={questionType}
onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
setQuestionType(event.target.value as QuestionType)
}
id="questionType"
invalidText={t('typeRequired', 'Type is required')}
labelText={t('questionType', 'Question type')}
required
>
{!questionType && <SelectItem text={t('chooseQuestionType', 'Choose a question type')} value="" />}
{questionTypes.map((questionType, key) => (
<SelectItem text={questionType} value={questionType} key={key} />
))}
</Select>
{renderingType !== 'markdown' && (
<>
<RadioButtonGroup
defaultSelected="optional"
name="isQuestionRequired"
legendText={t(
'isQuestionRequiredOrOptional',
'Is this question a required or optional field? Required fields must be answered before the form can be submitted.',
)}
>
<RadioButton
id="questionIsNotRequired"
defaultChecked={true}
labelText={t('optional', 'Optional')}
onClick={() => setIsQuestionRequired(false)}
value="optional"
/>
<RadioButton
id="questionIsRequired"
defaultChecked={false}
labelText={t('required', 'Required')}
onClick={() => setIsQuestionRequired(true)}
value="required"
/>
</RadioButtonGroup>
<Select
value={questionType}
onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
setQuestionType(event.target.value as QuestionType)
}
id="questionType"
invalidText={t('typeRequired', 'Type is required')}
labelText={t('questionType', 'Question type')}
required
>
{!questionType && <SelectItem text={t('chooseQuestionType', 'Choose a question type')} value="" />}
{questionTypes.map((questionType, key) => (
<SelectItem text={questionType} value={questionType} key={key} />
))}
</Select>
</>
)}

<Select
value={renderingType}
Expand All @@ -451,12 +457,13 @@ const AddQuestionModal: React.FC<AddQuestionModalProps> = ({
required
>
{!renderingType && <SelectItem text={t('chooseRenderingType', 'Choose a rendering type')} value="" />}

{questionTypes.filter((questionType) => questionType !== 'obs').includes(questionType)
{questionTypes.filter((questionType) => !['obs', 'control'].includes(questionType)).includes(questionType as Exclude<QuestionType, 'obs' | 'control'>)
? renderTypeOptions[questionType].map((type, key) => (
<SelectItem key={`${questionType}-${key}`} text={type} value={type} />
))
: fieldTypes.map((type, key) => <SelectItem key={key} text={type} value={type} />)}
: questionType === 'obs'
? fieldTypes.filter(type => type !== 'markdown').map((type, key) => <SelectItem key={key} text={type} value={type} />)
: fieldTypes.map((type, key) => <SelectItem key={key} text={type} value={type} />)}
</Select>

{questionType === 'personAttribute' && (
Expand Down Expand Up @@ -1003,7 +1010,6 @@ const AddQuestionModal: React.FC<AddQuestionModalProps> = ({
</Button>
<Button
disabled={
!questionLabel ||
!questionId ||
questionIdExists(questionId) ||
!renderingType ||
Expand Down Expand Up @@ -1031,4 +1037,4 @@ function RequiredLabel({ isRequired, text, t }: RequiredLabelProps) {
);
}

export default AddQuestionModal;
export default AddQuestionModal;
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CopyButton, IconButton } from '@carbon/react';
import { Draggable, Edit, TrashCan } from '@carbon/react/icons';
import { showModal } from '@openmrs/esm-framework';
import type { Question, Schema } from '../../types';
import MarkdownWrapper from './markdown-wrapper';
import styles from './draggable-question.scss';

interface DraggableQuestionProps {
Expand Down Expand Up @@ -89,7 +90,9 @@ const DraggableQuestion: React.FC<DraggableQuestionProps> = ({
<Draggable />
</IconButton>
</div>
<p className={styles.questionLabel}>{question.label}</p>
<p className={styles.questionLabel}>
{question.questionOptions.rendering === 'markdown' ? <MarkdownWrapper markdown={question.value} /> : question.label}
</p>
</div>
<div className={styles.buttonsContainer}>
<CopyButton
Expand Down
2 changes: 1 addition & 1 deletion src/components/interactive-builder/draggable-question.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

.dragContainer {
display: flex;
height: 3rem;
min-height: 3rem;
justify-content: space-between;
align-items: center;
width: 100%;
Expand Down
102 changes: 56 additions & 46 deletions src/components/interactive-builder/edit-question.modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { usePersonAttributeName } from '../../hooks/usePersonAttributeName';
import { usePersonAttributeTypes } from '../../hooks/usePersonAttributeTypes';
import { usePrograms, useProgramWorkStates } from '../../hooks/useProgramStates';
import { getDatePickerType } from './add-question.modal';
import MarkdownQuestion from './markdown-question.component';
import styles from './question-modal.scss';

interface EditQuestionModalProps {
Expand Down Expand Up @@ -105,6 +106,7 @@ const EditQuestionModal: React.FC<EditQuestionModalProps> = ({
const [min, setMin] = useState(questionToEdit.questionOptions.min ?? '');
const [questionId, setQuestionId] = useState('');
const [questionLabel, setQuestionLabel] = useState('');
const [questionValue, setQuestionValue] = useState(questionToEdit.value);
const [questionType, setQuestionType] = useState<QuestionType | null>(null);
const [datePickerType, setDatePickerType] = useState<DatePickerType | null>(
questionToEdit.datePickerFormat ?? 'both',
Expand Down Expand Up @@ -314,7 +316,8 @@ const EditQuestionModal: React.FC<EditQuestionModalProps> = ({

try {
const data = {
label: questionLabel ? questionLabel : questionToEdit.label,
...(questionLabel && { label: questionLabel }),
...(questionValue && { value: questionValue }),
type: questionType ? questionType : questionToEdit.type,
required: isQuestionRequired ? isQuestionRequired : /true/.test(questionToEdit?.required?.toString()),
id: questionId ? questionId : questionToEdit.id,
Expand Down Expand Up @@ -449,13 +452,16 @@ const EditQuestionModal: React.FC<EditQuestionModalProps> = ({
<Form className={styles.form} onSubmit={(event: React.SyntheticEvent) => event.preventDefault()}>
<ModalBody hasScrollingContent>
<Stack gap={5}>
<TextInput
defaultValue={questionToEdit.label}
id={questionToEdit.id}
labelText={t('questionLabel', 'Label')}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setQuestionLabel(event.target.value)}
required
/>
{questionToEdit.questionOptions.rendering === 'markdown' || fieldType === 'markdown' ? (
<MarkdownQuestion placeholder={questionToEdit.value} onValueChange={setQuestionValue} />
) : (
<TextInput
defaultValue={questionToEdit.label}
id={questionToEdit.id}
labelText={t('questionLabel', 'Label')}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setQuestionLabel(event.target.value)}
/>
)}
<TextInput
defaultValue={questionToEdit.id}
id="questionId"
Expand All @@ -472,44 +478,48 @@ const EditQuestionModal: React.FC<EditQuestionModalProps> = ({
)}
required
/>
<RadioButtonGroup
defaultSelected={/true/.test(questionToEdit?.required?.toString()) ? 'required' : 'optional'}
name="isQuestionRequired"
legendText={t(
'isQuestionRequiredOrOptional',
'Is this question a required or optional field? Required fields must be answered before the form can be submitted.',
)}
>
<RadioButton
id="questionIsNotRequired"
defaultChecked={true}
labelText={t('optional', 'Optional')}
onClick={() => setIsQuestionRequired(false)}
value="optional"
/>
<RadioButton
id="questionIsRequired"
defaultChecked={false}
labelText={t('required', 'Required')}
onClick={() => setIsQuestionRequired(true)}
value="required"
/>
</RadioButtonGroup>
<Select
defaultValue={questionToEdit.type}
onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
setQuestionType(event.target.value as QuestionType)
}
id={'questionType'}
invalidText={t('typeRequired', 'Type is required')}
labelText={t('questionType', 'Question type')}
required
>
{!questionType && <SelectItem text={t('chooseQuestionType', 'Choose a question type')} value="" />}
{questionTypes.map((questionType, key) => (
<SelectItem text={questionType} value={questionType} key={key} />
))}
</Select>
{(questionToEdit.questionOptions.rendering !== 'markdown' || fieldType !== 'markdown') && (
<>
<RadioButtonGroup
defaultSelected={/true/.test(questionToEdit?.required?.toString()) ? 'required' : 'optional'}
name="isQuestionRequired"
legendText={t(
'isQuestionRequiredOrOptional',
'Is this question a required or optional field? Required fields must be answered before the form can be submitted.',
)}
>
<RadioButton
id="questionIsNotRequired"
defaultChecked={true}
labelText={t('optional', 'Optional')}
onClick={() => setIsQuestionRequired(false)}
value="optional"
/>
<RadioButton
id="questionIsRequired"
defaultChecked={false}
labelText={t('required', 'Required')}
onClick={() => setIsQuestionRequired(true)}
value="required"
/>
</RadioButtonGroup>
<Select
defaultValue={questionToEdit.type}
onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
setQuestionType(event.target.value as QuestionType)
}
id={'questionType'}
invalidText={t('typeRequired', 'Type is required')}
labelText={t('questionType', 'Question type')}
required
>
{!questionType && <SelectItem text={t('chooseQuestionType', 'Choose a question type')} value="" />}
{questionTypes.map((questionType, key) => (
<SelectItem text={questionType} value={questionType} key={key} />
))}
</Select>
</>
)}
<Select
defaultValue={questionToEdit.questionOptions.rendering}
onChange={(event: React.ChangeEvent<HTMLSelectElement>) => setFieldType(event.target.value as RenderType)}
Expand Down
Loading

0 comments on commit eb469f8

Please sign in to comment.