@openmrs/esm-form-engine-lib 2.1.0-pre.1565 → 2.1.0-pre.1577

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.
Files changed (31) hide show
  1. package/b5c5a7c4dc0a8acf/b5c5a7c4dc0a8acf.gz +0 -0
  2. package/d7b6803241c9380f/d7b6803241c9380f.gz +0 -0
  3. package/dist/openmrs-esm-form-engine-lib.js +1 -1
  4. package/package.json +1 -1
  5. package/src/components/renderer/form/form-renderer.component.tsx +20 -6
  6. package/src/components/renderer/page/page.renderer.component.tsx +12 -5
  7. package/src/components/repeat/repeat.component.tsx +2 -0
  8. package/src/components/sidebar/page-observer.ts +58 -0
  9. package/src/components/sidebar/sidebar.component.tsx +68 -94
  10. package/src/components/sidebar/sidebar.scss +56 -72
  11. package/src/components/sidebar/useCurrentActivePage.test.ts +222 -0
  12. package/src/components/sidebar/useCurrentActivePage.ts +137 -0
  13. package/src/components/sidebar/usePageObserver.ts +45 -0
  14. package/src/form-engine.component.tsx +34 -22
  15. package/src/hooks/useEvaluateFormFieldExpressions.ts +4 -1
  16. package/src/hooks/useFormStateHelpers.ts +1 -1
  17. package/src/hooks/useFormWorkspaceSize.test.ts +117 -0
  18. package/src/hooks/useFormWorkspaceSize.ts +52 -0
  19. package/src/lifecycle.ts +2 -0
  20. package/src/provider/form-factory-provider.tsx +0 -4
  21. package/src/transformers/default-schema-transformer.test.ts +7 -0
  22. package/src/transformers/default-schema-transformer.ts +8 -5
  23. package/src/types/schema.ts +2 -0
  24. package/src/utils/common-utils.ts +10 -0
  25. package/src/utils/form-helper.test.ts +64 -1
  26. package/src/utils/form-helper.ts +20 -1
  27. package/d356d971fe6c1116/d356d971fe6c1116.gz +0 -0
  28. package/fa0d48f72865e16b/fa0d48f72865e16b.gz +0 -0
  29. package/src/hooks/useWorkspaceLayout.ts +0 -29
  30. /package/{f2738e729719827e/f2738e729719827e.gz → 63ee6b397e426495/63ee6b397e426495.gz} +0 -0
  31. /package/{c0c82ed82c8a1f6d/c0c82ed82c8a1f6d.gz → 96c41d63e46e82a5/96c41d63e46e82a5.gz} +0 -0
@@ -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
  }
@@ -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 FormSchemaTransformer, type FormSchema, type RenderType } from '../types';
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
  }
@@ -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
  });
@@ -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: 'center',
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
- }