@openmrs/esm-form-engine-lib 2.1.0-pre.1575 → 2.1.0-pre.1581
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/openmrs-esm-form-engine-lib.js +1 -1
- package/package.json +1 -1
- package/src/adapters/obs-adapter.ts +1 -1
- package/src/components/renderer/field/fieldLogic.ts +3 -0
- package/src/components/renderer/field/form-field-renderer.component.tsx +0 -2
- package/src/components/renderer/form/form-renderer.component.tsx +20 -6
- package/src/components/renderer/page/page.renderer.component.tsx +12 -5
- package/src/components/sidebar/page-observer.ts +58 -0
- package/src/components/sidebar/sidebar.component.tsx +68 -94
- package/src/components/sidebar/sidebar.scss +56 -72
- package/src/components/sidebar/useCurrentActivePage.test.ts +222 -0
- package/src/components/sidebar/useCurrentActivePage.ts +137 -0
- package/src/components/sidebar/usePageObserver.ts +45 -0
- package/src/form-engine.component.tsx +34 -22
- package/src/hooks/useEvaluateFormFieldExpressions.ts +4 -1
- package/src/hooks/useFormStateHelpers.ts +1 -1
- package/src/hooks/useFormWorkspaceSize.test.ts +117 -0
- package/src/hooks/useFormWorkspaceSize.ts +52 -0
- package/src/lifecycle.ts +2 -0
- package/src/processors/encounter/encounter-form-processor.ts +1 -1
- package/src/provider/form-factory-provider.tsx +0 -4
- package/src/transformers/default-schema-transformer.test.ts +7 -0
- package/src/transformers/default-schema-transformer.ts +8 -5
- package/src/types/schema.ts +2 -0
- package/src/utils/common-utils.ts +10 -0
- package/src/utils/form-helper.test.ts +64 -1
- package/src/utils/form-helper.ts +20 -1
- package/src/hooks/useWorkspaceLayout.ts +0 -29
@@ -0,0 +1,52 @@
|
|
1
|
+
import { useLayoutEffect, useMemo, useState } from 'react';
|
2
|
+
import { pxToRem } from '../utils/common-utils';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* The width of the supported workspace variants in rem
|
6
|
+
*/
|
7
|
+
const narrowWorkspaceWidth = 26.25;
|
8
|
+
const widerWorkspaceWidth = 32.25;
|
9
|
+
const extraWideWorkspaceWidth = 48.25;
|
10
|
+
|
11
|
+
type WorkspaceSize = 'narrow' | 'wider' | 'extra-wide' | 'ultra-wide';
|
12
|
+
|
13
|
+
/**
|
14
|
+
* This hook evaluates the size of the current workspace based on the width of the container element
|
15
|
+
*/
|
16
|
+
export function useFormWorkspaceSize(rootRef: React.RefObject<HTMLDivElement>): WorkspaceSize {
|
17
|
+
// width in rem
|
18
|
+
const [containerWidth, setContainerWidth] = useState(0);
|
19
|
+
const size = useMemo(() => {
|
20
|
+
if (containerWidth <= narrowWorkspaceWidth) {
|
21
|
+
return 'narrow';
|
22
|
+
} else if (containerWidth <= widerWorkspaceWidth) {
|
23
|
+
return 'wider';
|
24
|
+
} else if (containerWidth <= extraWideWorkspaceWidth) {
|
25
|
+
return 'extra-wide';
|
26
|
+
} else {
|
27
|
+
return 'ultra-wide';
|
28
|
+
}
|
29
|
+
}, [containerWidth]);
|
30
|
+
|
31
|
+
useLayoutEffect(() => {
|
32
|
+
const handleResize = () => {
|
33
|
+
const containerWidth = rootRef.current?.parentElement?.offsetWidth;
|
34
|
+
const rootFontSize = parseInt(getComputedStyle(document.documentElement).fontSize);
|
35
|
+
containerWidth && setContainerWidth(pxToRem(containerWidth, rootFontSize));
|
36
|
+
};
|
37
|
+
handleResize();
|
38
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
39
|
+
handleResize();
|
40
|
+
});
|
41
|
+
|
42
|
+
if (rootRef.current?.parentElement) {
|
43
|
+
resizeObserver.observe(rootRef.current?.parentElement);
|
44
|
+
}
|
45
|
+
|
46
|
+
return () => {
|
47
|
+
resizeObserver.disconnect();
|
48
|
+
};
|
49
|
+
}, [rootRef]);
|
50
|
+
|
51
|
+
return size;
|
52
|
+
}
|
package/src/lifecycle.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import { pageObserver } from './components/sidebar/page-observer';
|
1
2
|
import setupFormEngineLibI18n from './setupI18n';
|
2
3
|
import { type FormFieldValueAdapter } from './types';
|
3
4
|
|
@@ -30,4 +31,5 @@ export function teardown() {
|
|
30
31
|
}
|
31
32
|
});
|
32
33
|
formFieldAdapters.clear();
|
34
|
+
pageObserver.clear();
|
33
35
|
}
|
@@ -318,7 +318,7 @@ export class EncounterFormProcessor extends FormProcessor {
|
|
318
318
|
patient: patient,
|
319
319
|
previousEncounter: previousDomainObjectValue,
|
320
320
|
});
|
321
|
-
return extractObsValueAndDisplay(field, value);
|
321
|
+
return value ? extractObsValueAndDisplay(field, value) : null;
|
322
322
|
}
|
323
323
|
if (previousDomainObjectValue && field.questionOptions.enablePreviousValue) {
|
324
324
|
return await adapter.getPreviousValue(field, previousDomainObjectValue, context);
|
@@ -28,7 +28,6 @@ interface FormFactoryProviderContextProps {
|
|
28
28
|
provider: OpenmrsResource;
|
29
29
|
isFormExpanded: boolean;
|
30
30
|
registerForm: (formId: string, isSubForm: boolean, context: FormContextProps) => void;
|
31
|
-
setCurrentPage: (page: string) => void;
|
32
31
|
handleConfirmQuestionDeletion?: (question: Readonly<FormField>) => Promise<void>;
|
33
32
|
setIsFormDirty: (isFormDirty: boolean) => void;
|
34
33
|
}
|
@@ -52,7 +51,6 @@ interface FormFactoryProviderProps {
|
|
52
51
|
handleClose: () => void;
|
53
52
|
};
|
54
53
|
hideFormCollapseToggle: () => void;
|
55
|
-
setCurrentPage: (page: string) => void;
|
56
54
|
handleConfirmQuestionDeletion?: (question: Readonly<FormField>) => Promise<void>;
|
57
55
|
setIsFormDirty: (isFormDirty: boolean) => void;
|
58
56
|
}
|
@@ -72,7 +70,6 @@ export const FormFactoryProvider: React.FC<FormFactoryProviderProps> = ({
|
|
72
70
|
children,
|
73
71
|
formSubmissionProps,
|
74
72
|
hideFormCollapseToggle,
|
75
|
-
setCurrentPage,
|
76
73
|
handleConfirmQuestionDeletion,
|
77
74
|
setIsFormDirty,
|
78
75
|
}) => {
|
@@ -170,7 +167,6 @@ export const FormFactoryProvider: React.FC<FormFactoryProviderProps> = ({
|
|
170
167
|
provider,
|
171
168
|
isFormExpanded,
|
172
169
|
registerForm,
|
173
|
-
setCurrentPage,
|
174
170
|
handleConfirmQuestionDeletion,
|
175
171
|
setIsFormDirty,
|
176
172
|
}}>
|
@@ -9,6 +9,7 @@ const expectedTransformedSchema = {
|
|
9
9
|
{
|
10
10
|
label: 'Page 1',
|
11
11
|
readonly: false,
|
12
|
+
id: 'page-Page1-0',
|
12
13
|
sections: [
|
13
14
|
{
|
14
15
|
label: 'Section 1',
|
@@ -33,6 +34,7 @@ const expectedTransformedSchema = {
|
|
33
34
|
],
|
34
35
|
meta: {
|
35
36
|
submission: null,
|
37
|
+
pageId: 'page-Page1-0',
|
36
38
|
},
|
37
39
|
},
|
38
40
|
{
|
@@ -53,6 +55,7 @@ const expectedTransformedSchema = {
|
|
53
55
|
],
|
54
56
|
meta: {
|
55
57
|
submission: null,
|
58
|
+
pageId: 'page-Page1-0',
|
56
59
|
},
|
57
60
|
},
|
58
61
|
{
|
@@ -73,6 +76,7 @@ const expectedTransformedSchema = {
|
|
73
76
|
],
|
74
77
|
meta: {
|
75
78
|
submission: null,
|
79
|
+
pageId: 'page-Page1-0',
|
76
80
|
},
|
77
81
|
},
|
78
82
|
{
|
@@ -100,11 +104,13 @@ const expectedTransformedSchema = {
|
|
100
104
|
],
|
101
105
|
meta: {
|
102
106
|
submission: null,
|
107
|
+
pageId: 'page-Page1-0',
|
103
108
|
},
|
104
109
|
},
|
105
110
|
],
|
106
111
|
meta: {
|
107
112
|
submission: null,
|
113
|
+
pageId: 'page-Page1-0',
|
108
114
|
},
|
109
115
|
},
|
110
116
|
{
|
@@ -141,6 +147,7 @@ const expectedTransformedSchema = {
|
|
141
147
|
],
|
142
148
|
meta: {
|
143
149
|
submission: null,
|
150
|
+
pageId: 'page-Page1-0',
|
144
151
|
},
|
145
152
|
},
|
146
153
|
],
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { type FormField, type
|
1
|
+
import { type FormField, type FormSchema, type FormSchemaTransformer, type RenderType, type FormPage } from '../types';
|
2
2
|
import { isTrue } from '../utils/boolean-utils';
|
3
3
|
import { hasRendering } from '../utils/common-utils';
|
4
4
|
|
@@ -7,7 +7,9 @@ export type RenderTypeExtended = 'multiCheckbox' | 'numeric' | RenderType;
|
|
7
7
|
export const DefaultFormSchemaTransformer: FormSchemaTransformer = {
|
8
8
|
transform: (form: FormSchema) => {
|
9
9
|
parseBooleanTokenIfPresent(form, 'readonly');
|
10
|
-
form.pages.forEach((page) => {
|
10
|
+
form.pages.forEach((page, index) => {
|
11
|
+
const label = page.label ?? '';
|
12
|
+
page.id = `page-${label.replace(/\s/g, '')}-${index}`;
|
11
13
|
parseBooleanTokenIfPresent(page, 'readonly');
|
12
14
|
if (page.sections) {
|
13
15
|
page.sections.forEach((section) => {
|
@@ -15,7 +17,7 @@ export const DefaultFormSchemaTransformer: FormSchemaTransformer = {
|
|
15
17
|
section.questions = handleQuestionsWithObsComments(section.questions);
|
16
18
|
parseBooleanTokenIfPresent(section, 'readonly');
|
17
19
|
parseBooleanTokenIfPresent(section, 'isExpanded');
|
18
|
-
section?.questions?.forEach((question, index) => handleQuestion(question, form));
|
20
|
+
section?.questions?.forEach((question, index) => handleQuestion(question, page, form));
|
19
21
|
});
|
20
22
|
}
|
21
23
|
});
|
@@ -26,7 +28,7 @@ export const DefaultFormSchemaTransformer: FormSchemaTransformer = {
|
|
26
28
|
},
|
27
29
|
};
|
28
30
|
|
29
|
-
function handleQuestion(question: FormField, form: FormSchema) {
|
31
|
+
function handleQuestion(question: FormField, page: FormPage, form: FormSchema) {
|
30
32
|
if (question.type === 'programState') {
|
31
33
|
const formMeta = form.meta ?? {};
|
32
34
|
formMeta.programs = formMeta.programs
|
@@ -40,8 +42,9 @@ function handleQuestion(question: FormField, form: FormSchema) {
|
|
40
42
|
transformByType(question);
|
41
43
|
transformByRendering(question);
|
42
44
|
if (question?.questions?.length) {
|
43
|
-
question.questions.forEach((question) => handleQuestion(question, form));
|
45
|
+
question.questions.forEach((question) => handleQuestion(question, page, form));
|
44
46
|
}
|
47
|
+
question.meta.pageId = page.id;
|
45
48
|
} catch (error) {
|
46
49
|
console.error(error);
|
47
50
|
}
|
package/src/types/schema.ts
CHANGED
@@ -42,6 +42,7 @@ export interface FormPage {
|
|
42
42
|
behaviours?: Array<any>;
|
43
43
|
form: Omit<FormSchema, 'postSubmissionActions'>;
|
44
44
|
};
|
45
|
+
id?: string;
|
45
46
|
}
|
46
47
|
|
47
48
|
export interface FormSection {
|
@@ -125,6 +126,7 @@ export interface QuestionMetaProps {
|
|
125
126
|
wasDeleted?: boolean;
|
126
127
|
};
|
127
128
|
groupId?: string;
|
129
|
+
pageId?: string;
|
128
130
|
[anythingElse: string]: any;
|
129
131
|
}
|
130
132
|
|
@@ -89,3 +89,13 @@ export function updateFormSectionReferences(formJson: FormSchema) {
|
|
89
89
|
});
|
90
90
|
return { ...formJson };
|
91
91
|
}
|
92
|
+
|
93
|
+
/**
|
94
|
+
* Converts a px value to a rem value
|
95
|
+
* @param px - The px value to convert
|
96
|
+
* @param fontSize - The font size to use for the conversion
|
97
|
+
* @returns The rem value
|
98
|
+
*/
|
99
|
+
export function pxToRem(px: number, fontSize: number = 16) {
|
100
|
+
return px / fontSize;
|
101
|
+
}
|
@@ -3,11 +3,12 @@ import {
|
|
3
3
|
evaluateConditionalAnswered,
|
4
4
|
evaluateFieldReadonlyProp,
|
5
5
|
evaluateDisabled,
|
6
|
+
isPageContentVisible,
|
6
7
|
} from './form-helper';
|
7
8
|
import { DefaultValueValidator } from '../validators/default-value-validator';
|
8
9
|
import { type LayoutType } from '@openmrs/esm-framework';
|
9
10
|
import { ConceptTrue } from '../constants';
|
10
|
-
import { type FormField, type OpenmrsEncounter, type SessionMode } from '../types';
|
11
|
+
import { type FormPage, type FormField, type OpenmrsEncounter, type SessionMode } from '../types';
|
11
12
|
|
12
13
|
jest.mock('../validators/default-value-validator');
|
13
14
|
|
@@ -444,4 +445,66 @@ describe('Form Engine Helper', () => {
|
|
444
445
|
);
|
445
446
|
});
|
446
447
|
});
|
448
|
+
|
449
|
+
describe('isPageContentVisible', () => {
|
450
|
+
it('should return false if the page is hidden', () => {
|
451
|
+
const page = { isHidden: true, sections: [] } as FormPage;
|
452
|
+
expect(isPageContentVisible(page)).toBe(false);
|
453
|
+
});
|
454
|
+
|
455
|
+
it('should return false if all sections are hidden', () => {
|
456
|
+
const page = {
|
457
|
+
isHidden: false,
|
458
|
+
sections: [
|
459
|
+
{ isHidden: true, questions: [] },
|
460
|
+
{ isHidden: true, questions: [] },
|
461
|
+
],
|
462
|
+
} as FormPage;
|
463
|
+
expect(isPageContentVisible(page)).toBe(false);
|
464
|
+
});
|
465
|
+
|
466
|
+
it('should return false if all questions in all sections are hidden', () => {
|
467
|
+
const page = {
|
468
|
+
isHidden: false,
|
469
|
+
sections: [
|
470
|
+
{ isHidden: false, questions: [{ isHidden: true }, { isHidden: true }] },
|
471
|
+
{ isHidden: false, questions: [{ isHidden: true }] },
|
472
|
+
],
|
473
|
+
} as FormPage;
|
474
|
+
expect(isPageContentVisible(page)).toBe(false);
|
475
|
+
});
|
476
|
+
|
477
|
+
it('should return false when there are no form fields', () => {
|
478
|
+
const page = {
|
479
|
+
isHidden: false,
|
480
|
+
sections: [
|
481
|
+
{ isHidden: true, questions: [] },
|
482
|
+
{ isHidden: false, questions: [] },
|
483
|
+
],
|
484
|
+
} as FormPage;
|
485
|
+
expect(isPageContentVisible(page)).toBe(false);
|
486
|
+
});
|
487
|
+
|
488
|
+
it('should return true if at least one question in a section is visible', () => {
|
489
|
+
const page = {
|
490
|
+
isHidden: false,
|
491
|
+
sections: [
|
492
|
+
{
|
493
|
+
isHidden: false,
|
494
|
+
questions: [{ isHidden: true }, { isHidden: false }],
|
495
|
+
},
|
496
|
+
{
|
497
|
+
isHidden: true,
|
498
|
+
questions: [{ isHidden: true }],
|
499
|
+
},
|
500
|
+
],
|
501
|
+
} as FormPage;
|
502
|
+
expect(isPageContentVisible(page)).toBe(true);
|
503
|
+
});
|
504
|
+
|
505
|
+
it('should return false for an empty page with no sections', () => {
|
506
|
+
const page = { isHidden: false, sections: [] } as FormPage;
|
507
|
+
expect(isPageContentVisible(page)).toBe(false);
|
508
|
+
});
|
509
|
+
});
|
447
510
|
});
|
package/src/utils/form-helper.ts
CHANGED
@@ -163,7 +163,7 @@ export function scrollIntoView(viewId: string, shouldFocus: boolean = false) {
|
|
163
163
|
const currentElement = document.getElementById(viewId);
|
164
164
|
currentElement?.scrollIntoView({
|
165
165
|
behavior: 'smooth',
|
166
|
-
block: '
|
166
|
+
block: 'start',
|
167
167
|
inline: 'center',
|
168
168
|
});
|
169
169
|
|
@@ -193,3 +193,22 @@ export const extractObsValueAndDisplay = (field: FormField, obs: OpenmrsObs) =>
|
|
193
193
|
};
|
194
194
|
}
|
195
195
|
};
|
196
|
+
|
197
|
+
/**
|
198
|
+
* Checks if a given form page has visible content.
|
199
|
+
*
|
200
|
+
* A page is considered to have visible content if:
|
201
|
+
* - The page itself is not hidden.
|
202
|
+
* - At least one section within the page is visible.
|
203
|
+
* - At least one question within each section is visible.
|
204
|
+
*/
|
205
|
+
export function isPageContentVisible(page: FormPage) {
|
206
|
+
if (page.isHidden) {
|
207
|
+
return false;
|
208
|
+
}
|
209
|
+
return (
|
210
|
+
page.sections?.some((section) => {
|
211
|
+
return !section.isHidden && section.questions?.some((question) => !question.isHidden);
|
212
|
+
}) ?? false
|
213
|
+
);
|
214
|
+
}
|
@@ -1,29 +0,0 @@
|
|
1
|
-
import { useLayoutEffect, useState } from 'react';
|
2
|
-
|
3
|
-
/**
|
4
|
-
* This hook evaluates the layout of the current workspace based on the width of the container element
|
5
|
-
*/
|
6
|
-
export function useWorkspaceLayout(rootRef): 'minimized' | 'maximized' {
|
7
|
-
const [layout, setLayout] = useState<'minimized' | 'maximized'>('minimized');
|
8
|
-
const TABLET_MAX = 1023;
|
9
|
-
useLayoutEffect(() => {
|
10
|
-
const handleResize = () => {
|
11
|
-
const containerWidth = rootRef.current?.parentElement?.offsetWidth;
|
12
|
-
containerWidth && setLayout(containerWidth > TABLET_MAX ? 'maximized' : 'minimized');
|
13
|
-
};
|
14
|
-
handleResize();
|
15
|
-
const resizeObserver = new ResizeObserver((entries) => {
|
16
|
-
handleResize();
|
17
|
-
});
|
18
|
-
|
19
|
-
if (rootRef.current) {
|
20
|
-
resizeObserver.observe(rootRef.current?.parentElement);
|
21
|
-
}
|
22
|
-
|
23
|
-
return () => {
|
24
|
-
resizeObserver.disconnect();
|
25
|
-
};
|
26
|
-
}, [rootRef]);
|
27
|
-
|
28
|
-
return layout;
|
29
|
-
}
|