Skip to content

Commit 710f21d

Browse files
Merge branch 'main' of https://github.com/Bharath-K-Shetty/openmrs-esm-form-builder into fix/O3-4320
2 parents 2df050e + 1041616 commit 710f21d

File tree

17 files changed

+668
-364
lines changed

17 files changed

+668
-364
lines changed

e2e/pages/form-builder-page.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export class FormBuilderPage {
4242
readonly pageCreatedMessage = () => this.page.getByText(/new page created/i);
4343
readonly addSectionButton = () => this.page.getByRole('button', { name: /add section/i });
4444
readonly sectionNameInput = () => this.page.getByRole('textbox', { name: /enter a section title/i });
45+
readonly isExpandedCheckbox = () => this.page.getByTestId('keep-section-expanded-checkbox');
4546
readonly editSectionButton = () => this.page.getByRole('button', { name: /edit section/i });
4647
readonly editSectionNameInput = () => this.page.locator('#sectionNameInput');
4748
readonly deleteSectionButton = () => this.page.getByRole('button', { name: /delete section/i });

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

Lines changed: 26 additions & 34 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();
@@ -144,16 +113,29 @@ test('Edit a form using the interactive builder', async ({ page, context }) => {
144113
});
145114

146115
await test.step('And then I fill in the updated section name', async () => {
147-
await formBuilderPage.editSectionNameInput().fill('An edited section');
116+
await formBuilderPage.sectionNameInput().fill('An edited section');
148117
updatedForm.pages[0].sections[0].label = 'An edited section';
149118
});
150119

120+
await test.step('And then I check the expand section checkbox', async () => {
121+
await page.evaluate(() => {
122+
const checkbox = document.querySelector(
123+
'input[data-testid="keep-section-expanded-checkbox"]',
124+
) as HTMLInputElement;
125+
if (checkbox) {
126+
checkbox.click();
127+
}
128+
});
129+
updatedForm.pages[0].sections[0].isExpanded = 'false';
130+
});
131+
151132
await test.step('Then I click the `Save` button', async () => {
133+
await expect(formBuilderPage.saveButton()).toBeEnabled();
152134
await formBuilderPage.saveButton().click();
153135
});
154136

155137
await test.step('Then I should get a success message and the section name should be renamed', async () => {
156-
await expect(formBuilderPage.page.getByText(/section renamed/i)).toBeVisible();
138+
await expect(formBuilderPage.page.getByText(/section edited/i)).toBeVisible();
157139
await expect(formBuilderPage.page.getByRole('heading', { level: 1, name: /an edited section/i })).toBeVisible();
158140
const formTextContent = await formBuilderPage.schemaEditorContent().textContent();
159141
expect(JSON.parse(formTextContent)).toEqual(updatedForm);
@@ -171,11 +153,21 @@ test('Edit a form using the interactive builder', async ({ page, context }) => {
171153
};
172154
});
173155

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+
174166
await test.step('Then I click the `Save` button', async () => {
175167
await formBuilderPage.saveButton().click();
176168
});
177169

178-
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 () => {
179171
await expect(formBuilderPage.page.getByText(/question updated/i)).toBeVisible();
180172
await expect(formBuilderPage.page.locator('p').getByText(/an edited question label/i)).toBeVisible();
181173
const formTextContent = await formBuilderPage.schemaEditorContent().textContent();

src/components/interactive-builder/interactive-builder.component.tsx

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { DndContext, KeyboardSensor, MouseSensor, closestCorners, useSensor, useSensors } from '@dnd-kit/core';
44
import { Accordion, AccordionItem, Button, IconButton, InlineLoading } from '@carbon/react';
5-
import { Add, TrashCan } from '@carbon/react/icons';
5+
import { Add, TrashCan, Edit } from '@carbon/react/icons';
66
import { useParams } from 'react-router-dom';
77
import { showModal, showSnackbar } from '@openmrs/esm-framework';
88
import DraggableQuestion from './draggable/draggable-question.component';
@@ -102,6 +102,21 @@ const InteractiveBuilder: React.FC<InteractiveBuilderProps> = ({
102102
[schema, onSchemaChange],
103103
);
104104

105+
const launchEditSectionModal = useCallback(
106+
(pageIndex: number, sectionIndex: number) => {
107+
const modalType = 'edit';
108+
const dispose = showModal('new-section-modal', {
109+
closeModal: () => dispose(),
110+
pageIndex,
111+
sectionIndex,
112+
schema,
113+
onSchemaChange,
114+
modalType,
115+
});
116+
},
117+
[onSchemaChange, schema],
118+
);
119+
105120
const launchDeleteSectionModal = useCallback(
106121
(pageIndex: number, sectionIndex: number) => {
107122
const dispose = showModal('delete-section-modal', {
@@ -416,12 +431,16 @@ const InteractiveBuilder: React.FC<InteractiveBuilderProps> = ({
416431
<>
417432
<div style={{ display: 'flex', alignItems: 'center' }}>
418433
<div className={styles.editorContainer}>
419-
<EditableValue
420-
elementType="section"
421-
id="sectionNameInput"
422-
value={section.label}
423-
onSave={(name) => renameSection(name, pageIndex, sectionIndex)}
424-
/>
434+
<h1 className={styles['sectionLabel']}>{section.label}</h1>
435+
<IconButton
436+
enterDelayMs={300}
437+
kind="ghost"
438+
label={t('editSection', 'Edit Section')}
439+
onClick={() => launchEditSectionModal(pageIndex, sectionIndex)}
440+
size="md"
441+
>
442+
<Edit />
443+
</IconButton>
425444
</div>
426445
<IconButton
427446
enterDelayMs={300}

src/components/interactive-builder/interactive-builder.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
margin: layout.$spacing-07 0;
2929
}
3030

31+
.sectionLabel {
32+
@include type.type-style('heading-02');
33+
}
34+
3135
.explainer {
3236
margin: layout.$spacing-05 layout.$spacing-03;
3337

src/components/interactive-builder/modals/new-section/section.modal.tsx

Lines changed: 0 additions & 81 deletions
This file was deleted.

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> = [

0 commit comments

Comments
 (0)