Skip to content

Commit 1041616

Browse files
(fix) O3-4493: Disable save question button when concept is invalid (#411)
* disable Save button when concept is invalid * update interactive builder e2e test to use a valid concept ID * Move concept validity check to context * Fix e2e tests * Fix unit tests * Fix bug in question modal --------- Co-authored-by: Nethmi Rodrigo <[email protected]>
1 parent 3c25f86 commit 1041616

File tree

8 files changed

+42
-47
lines changed

8 files changed

+42
-47
lines changed

e2e/specs/edit-form-with-interactive-builder.spec.ts

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -47,37 +47,6 @@ test.beforeEach(async ({ api }) => {
4747

4848
test('Edit a form using the interactive builder', async ({ page, context }) => {
4949
const formBuilderPage = new FormBuilderPage(page);
50-
const formDetails = {
51-
encounterType: 'e22e39fd-7db2-45e7-80f1-60fa0d5a4378',
52-
name: 'UI Select Form Test',
53-
pages: [
54-
{
55-
label: 'UI Select Test',
56-
sections: [
57-
{
58-
label: 'Visit Details',
59-
isExpanded: 'true',
60-
questions: [
61-
{
62-
label: 'Select Provider',
63-
type: 'obs',
64-
questionOptions: {
65-
rendering: 'text',
66-
concept: 'a-system-defined-concept-uuid',
67-
},
68-
id: 'sampleQuestion',
69-
},
70-
],
71-
},
72-
],
73-
},
74-
],
75-
processor: 'EncounterFormProcessor',
76-
referencedForms: [],
77-
uuid: 'xxx',
78-
version: '1',
79-
description: 'This is test description',
80-
};
8150

8251
await test.step('When I visit the form builder', async () => {
8352
await formBuilderPage.gotoFormBuilder();
@@ -184,11 +153,21 @@ test('Edit a form using the interactive builder', async ({ page, context }) => {
184153
};
185154
});
186155

156+
await test.step('And then I edit the question concept', async () => {
157+
await formBuilderPage.conceptSearchInput().fill('Tested for COVID 19');
158+
await formBuilderPage.conceptSearchInput().press('Enter');
159+
await formBuilderPage.page.getByRole('menuitem', { name: /tested for covid 19/i }).click();
160+
updatedForm.pages[0].sections[0].questions[0].questionOptions = {
161+
...updatedForm.pages[0].sections[0].questions[0].questionOptions,
162+
concept: '89c5bc03-8ce2-40d8-a77d-20b5a62a1ca1',
163+
};
164+
});
165+
187166
await test.step('Then I click the `Save` button', async () => {
188167
await formBuilderPage.saveButton().click();
189168
});
190169

191-
await test.step('Then I should get a success message and the question name should be renamed', async () => {
170+
await test.step('Then I should get a success message and the question label should be renamed', async () => {
192171
await expect(formBuilderPage.page.getByText(/question updated/i)).toBeVisible();
193172
await expect(formBuilderPage.page.locator('p').getByText(/an edited question label/i)).toBeVisible();
194173
const formTextContent = await formBuilderPage.schemaEditorContent().textContent();

src/components/interactive-builder/modals/question/form-field-context.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ interface FormFieldContextType {
77
setFormField: (value: FormField | ((prev: FormField) => FormField)) => void;
88
concept: Concept;
99
setConcept: React.Dispatch<React.SetStateAction<Concept>>;
10+
isConceptValid?: boolean;
11+
setIsConceptValid?: React.Dispatch<React.SetStateAction<boolean>>;
1012
updateParentFormField?: (updatedFormField: FormField) => void;
1113
isObsGrouped?: boolean;
1214
}
@@ -22,6 +24,7 @@ export const FormFieldProvider: React.FC<{
2224
}> = ({ children, initialFormField, isObsGrouped = false, selectedConcept = null, updateParentFormField }) => {
2325
const [formField, setFormFieldInternal] = useState<FormField>(initialFormField);
2426
const [concept, setConcept] = useState<Concept | null>(selectedConcept);
27+
const [isConceptValid, setIsConceptValid] = useState<boolean>(true);
2528

2629
const setFormField = useCallback(
2730
(valueOrUpdater: FormField | ((prev: FormField) => FormField)) => {
@@ -43,6 +46,8 @@ export const FormFieldProvider: React.FC<{
4346
setFormField,
4447
concept,
4548
setConcept,
49+
isConceptValid,
50+
setIsConceptValid,
4651
updateParentFormField,
4752
isObsGrouped,
4853
}}

src/components/interactive-builder/modals/question/question-form/common/concept-search/concept-search.component.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const ConceptSearch: React.FC<ConceptSearchProps> = ({
3535
isLoadingConcept,
3636
} = useConceptId(defaultConcept);
3737
const [selectedConcept, setSelectedConcept] = useState<Concept>(initialConcept);
38-
const { concept, setConcept } = useFormField();
38+
const { concept, setConcept, setIsConceptValid } = useFormField();
3939

4040
useEffect(() => {
4141
if (retainConceptInContextAfterSearch && initialConcept && !concept) {
@@ -48,27 +48,35 @@ const ConceptSearch: React.FC<ConceptSearchProps> = ({
4848
[],
4949
);
5050

51+
useEffect(() => {
52+
if (conceptLookupError || conceptNameLookupError) {
53+
setIsConceptValid(false);
54+
}
55+
}, [conceptLookupError, conceptNameLookupError, setIsConceptValid]);
56+
5157
const handleConceptSelect = useCallback(
5258
(concept: Concept) => {
5359
setConceptToLookup('');
5460
setSelectedConcept(concept);
5561
onSelectConcept(concept);
62+
setIsConceptValid(true);
5663
},
57-
[onSelectConcept],
64+
[onSelectConcept, setIsConceptValid],
5865
);
5966

6067
const clearSelectedConcept = useCallback(() => {
6168
setSelectedConcept(null);
6269
setConceptToLookup('');
70+
setIsConceptValid(true);
6371
if (onClearSelectedConcept) onClearSelectedConcept();
64-
}, [onClearSelectedConcept]);
72+
}, [onClearSelectedConcept, setIsConceptValid]);
6573

6674
return (
6775
<>
6876
<FormLabel className={styles.label}>
6977
{label ?? t('searchForBackingConcept', 'Search for a backing concept')}
7078
</FormLabel>
71-
{conceptLookupError || conceptNameLookupError ? (
79+
{(conceptLookupError || conceptNameLookupError) && (
7280
<InlineNotification
7381
kind="error"
7482
lowContrast
@@ -86,7 +94,7 @@ const ConceptSearch: React.FC<ConceptSearchProps> = ({
8694
})
8795
}
8896
/>
89-
) : null}
97+
)}
9098
{isLoadingConcept ? (
9199
<InlineLoading className={styles.loader} description={t('loading', 'Loading') + '...'} />
92100
) : (
@@ -135,9 +143,8 @@ const ConceptSearch: React.FC<ConceptSearchProps> = ({
135143
<strong>"{debouncedConceptToLookup}".</strong>
136144
</span>
137145
</Tile>
138-
139146
<div className={styles.oclLauncherBanner}>
140-
{<p className={styles.bodyShort01}>{t('conceptSearchHelpText', "Can't find a concept?")}</p>}
147+
<p className={styles.bodyShort01}>{t('conceptSearchHelpText', "Can't find a concept?")}</p>
141148
<a
142149
className={styles.oclLink}
143150
target="_blank"

src/components/interactive-builder/modals/question/question-form/common/concept-search/concept-search.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@ jest.mock('@hooks/useConceptId', () => ({
4545
const onSelectConcept = jest.fn();
4646
const mockSetFormField = jest.fn();
4747
const setConcept = jest.fn();
48+
const setIsConceptValid = jest.fn();
4849
jest.mock('../../../form-field-context', () => ({
4950
...jest.requireActual('../../../form-field-context'),
50-
useFormField: () => ({ formField, setFormField: mockSetFormField, setConcept }),
51+
useFormField: () => ({ formField, setFormField: mockSetFormField, setConcept, setIsConceptValid }),
5152
}));
5253

5354
describe('Concept search component', () => {

src/components/interactive-builder/modals/question/question-form/question-types/inputs/obs/obs-type-question.component.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useMemo, useState } from 'react';
1+
import React, { useCallback, useMemo } from 'react';
22
import { FormLabel, InlineNotification, FormGroup, Stack } from '@carbon/react';
33
import { useTranslation } from 'react-i18next';
44
import ConceptSearch from '../../../common/concept-search/concept-search.component';
@@ -10,9 +10,8 @@ const ObsTypeQuestion: React.FC = () => {
1010
const { t } = useTranslation();
1111
const { formField, setFormField, concept, setConcept } = useFormField();
1212

13-
const getDatePickerType = useCallback((concept: Concept): DatePickerType | null => {
14-
const conceptDataType = concept.datatype.name;
15-
switch (conceptDataType) {
13+
const getDatePickerType = useCallback((selectedConcept: Concept): DatePickerType | null => {
14+
switch (selectedConcept.datatype.name) {
1615
case 'Datetime':
1716
return 'both';
1817
case 'Date':

src/components/interactive-builder/modals/question/question-form/question-types/inputs/obs/obs-type-question.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { Concept } from '@types';
1010

1111
const mockSetFormField = jest.fn();
1212
const setConcept = jest.fn();
13+
const setIsConceptValid = jest.fn();
1314
const formField: FormField = {
1415
id: '1',
1516
type: 'obs',
@@ -20,7 +21,7 @@ const formField: FormField = {
2021

2122
jest.mock('../../../../form-field-context', () => ({
2223
...jest.requireActual('../../../../form-field-context'),
23-
useFormField: () => ({ formField, setFormField: mockSetFormField, setConcept }),
24+
useFormField: () => ({ formField, setFormField: mockSetFormField, setConcept, setIsConceptValid }),
2425
}));
2526

2627
const concepts: Array<Concept> = [

src/components/interactive-builder/modals/question/question-form/question-types/question-type.component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const componentMap: Partial<Record<QuestionType, React.FC>> = {
1212

1313
const QuestionTypeComponent: React.FC = () => {
1414
const { formField } = useFormField();
15-
const Component = componentMap[formField.type];
15+
const Component = componentMap[formField.type as QuestionType];
1616
if (!Component) {
1717
console.error(`No component found for questiontype: ${formField.type}`);
1818
return null;

src/components/interactive-builder/modals/question/question.modal.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const QuestionModalContent: React.FC<QuestionModalProps> = ({
4444
onSchemaChange,
4545
}) => {
4646
const { t } = useTranslation();
47-
const { formField, setFormField } = useFormField();
47+
const { formField, setFormField, isConceptValid } = useFormField();
4848

4949
/**
5050
* NOTE - this does not support nested obsGroup questions
@@ -205,6 +205,9 @@ const QuestionModalContent: React.FC<QuestionModalProps> = ({
205205
disabled={
206206
!formField ||
207207
!formField.id ||
208+
!isConceptValid ||
209+
(formField.type === 'obs' &&
210+
(!formField.questionOptions?.concept || formField.questionOptions?.concept === '')) ||
208211
(!formField.questions && checkIfQuestionIdExists(formField.id)) ||
209212
!formField.questionOptions?.rendering
210213
}

0 commit comments

Comments
 (0)