Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat (O3-3946): Add support for markdown questions #355

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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__/*
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
98 changes: 52 additions & 46 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 @@ -108,6 +109,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 @@ -211,8 +213,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}),
...((renderingType !== 'markdown') && {type: questionType}),
required: isQuestionRequired,
id: questionId ?? computedQuestionId,
...((renderingType === 'date' || renderingType === 'datetime') &&
Expand Down Expand Up @@ -322,14 +325,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 @@ -361,45 +365,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 @@ -413,7 +420,7 @@ const AddQuestionModal: React.FC<AddQuestionModalProps> = ({
>
{!renderingType && <SelectItem text={t('chooseRenderingType', 'Choose a rendering type')} value="" />}

{questionTypes.filter((questionType) => questionType !== 'obs').includes(questionType)
{questionTypes.filter((questionType) => questionType !== 'obs').includes(questionType as Exclude<QuestionType, 'obs'>)
? renderTypeOptions[questionType].map((type, key) => (
<SelectItem key={`${questionType}-${key}`} text={type} value={type} />
))
Expand Down Expand Up @@ -860,7 +867,6 @@ const AddQuestionModal: React.FC<AddQuestionModalProps> = ({
</Button>
<Button
disabled={
!questionLabel ||
!questionId ||
questionIdExists(questionId) ||
!renderingType ||
Expand Down Expand Up @@ -888,4 +894,4 @@ function RequiredLabel({ isRequired, text, t }: RequiredLabelProps) {
);
}

export default AddQuestionModal;
export default AddQuestionModal;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useCallback } from 'react';
import ReactMarkdown from 'react-markdown';
import classNames from 'classnames';
import { useDraggable } from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities';
Expand Down Expand Up @@ -69,6 +70,45 @@ const DraggableQuestion: React.FC<DraggableQuestionProps> = ({
}
}, [handleDuplicateQuestion, isDragging, question, pageIndex, sectionIndex]);

const MarkdownWrapper: React.FC<{ markdown: string | Array<string> }> = ({ markdown }) => {
const delimiters = ['***', '**', '*', '__', '_'];

function shortenMarkdownText(markdown: string | Array<string>, limit: number, delimiters: Array<string>) {
const inputString = Array.isArray(markdown) ? markdown.join('\n') : markdown;
let truncatedContent = inputString.length <= limit ? inputString : inputString.slice(0, limit).trimEnd();
const delimiterPattern = /[*_#]+$/;
if (delimiterPattern.test(truncatedContent)) {
truncatedContent = truncatedContent.replace(delimiterPattern, '').trimEnd();
}
let mutableString = truncatedContent
const unmatchedDelimiters = [];

for (const delimiter of delimiters) {
const firstIndex = mutableString.indexOf(delimiter);
const secondIndex = mutableString.indexOf(delimiter, firstIndex + delimiter.length);
if (firstIndex !== -1) {
if (secondIndex === -1) {
unmatchedDelimiters.push(delimiter);
mutableString = mutableString.replace(delimiter, '');
} else {
mutableString = mutableString.replace(delimiter, '').replace(delimiter, '');
}
}
}
return truncatedContent + unmatchedDelimiters.reverse().join('') + (inputString.length > limit ? ' ...' : '');
}

const shortMarkdownText = shortenMarkdownText(markdown, 15, delimiters);

return (
<ReactMarkdown
children={shortMarkdownText}
unwrapDisallowed={true}
allowedElements={['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'strong', 'em']}
/>
);
};

return (
<div
className={classNames({
Expand All @@ -89,7 +129,9 @@ const DraggableQuestion: React.FC<DraggableQuestionProps> = ({
<Draggable />
</IconButton>
</div>
<p className={styles.questionLabel}>{question.label}</p>
<p className={styles.questionLabel}>
{question.label ? question.label : <MarkdownWrapper markdown={question.value} />}
</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
Loading