@openmrs/esm-form-engine-lib 3.0.0 → 3.0.1-pre.1652

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 (44) hide show
  1. package/00718f89a2ec9729/00718f89a2ec9729.gz +0 -0
  2. package/121aae94b7b7602c/121aae94b7b7602c.gz +0 -0
  3. package/1f1205a43e369f8a/1f1205a43e369f8a.gz +0 -0
  4. package/56d74d49545e0e3e/56d74d49545e0e3e.gz +0 -0
  5. package/README.md +65 -30
  6. package/__mocks__/forms/rfe-forms/obs-group-test_form.json +188 -124
  7. package/dist/openmrs-esm-form-engine-lib.js +1 -1
  8. package/package.json +5 -4
  9. package/src/components/group/obs-group.component.tsx +5 -5
  10. package/src/components/inputs/content-switcher/content-switcher.component.tsx +6 -1
  11. package/src/components/inputs/date/date.component.tsx +1 -1
  12. package/src/components/inputs/multi-select/multi-select.component.tsx +4 -4
  13. package/src/components/inputs/number/number.component.tsx +2 -2
  14. package/src/components/inputs/radio/radio.component.tsx +1 -1
  15. package/src/components/inputs/select/dropdown.component.tsx +2 -2
  16. package/src/components/inputs/ui-select-extended/ui-select-extended.component.tsx +39 -18
  17. package/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx +0 -1
  18. package/src/components/inputs/workspace-launcher/workspace-launcher.component.tsx +3 -1
  19. package/src/components/sidebar/sidebar.component.tsx +5 -6
  20. package/src/components/sidebar/sidebar.scss +5 -0
  21. package/src/components/value/value.component.tsx +4 -1
  22. package/src/form-engine.component.tsx +17 -13
  23. package/src/form-engine.scss +8 -0
  24. package/src/form-engine.test.tsx +44 -1
  25. package/src/hooks/{useConcepts.tsx → useConcepts.ts} +1 -2
  26. package/src/hooks/useDataSourceDependentValue.ts +1 -1
  27. package/src/hooks/{useEncounter.tsx → useEncounter.ts} +9 -1
  28. package/src/hooks/{useFormJson.tsx → useFormJson.ts} +12 -2
  29. package/src/hooks/{usePatientData.tsx → usePatientData.ts} +1 -1
  30. package/src/hooks/usePatientPrograms.ts +29 -23
  31. package/src/hooks/useProcessorDependencies.ts +14 -4
  32. package/src/processors/encounter/encounter-form-processor.ts +19 -22
  33. package/src/types/domain.ts +4 -0
  34. package/src/types/schema.ts +5 -0
  35. package/src/utils/common-expression-helpers.test.ts +1 -1
  36. package/src/utils/common-expression-helpers.ts +10 -7
  37. package/translations/en.json +3 -3
  38. package/src/hooks/useClobData.tsx +0 -21
  39. package/src/hooks/useFieldValidationResults.ts +0 -18
  40. package/src/hooks/useFormsConfig.tsx +0 -27
  41. package/src/hooks/useRestMaxResultsCount.ts +0 -5
  42. package/src/hooks/useSystemSetting.ts +0 -36
  43. package/src/hooks/{useEncounterRole.tsx → useEncounterRole.ts} +1 -1
  44. package/src/hooks/{useFormCollapse.tsx → useFormCollapse.ts} +1 -1
@@ -9,13 +9,13 @@ import { useTranslation } from 'react-i18next';
9
9
 
10
10
  export const ObsGroup: React.FC<FormFieldInputProps> = ({ field, ...restProps }) => {
11
11
  const { t } = useTranslation();
12
- const { formFieldAdapters } = useFormProviderContext();
13
- const showLabel = useMemo(() => field.questions?.length > 1, [field]);
12
+ const { formFieldAdapters, formFields } = useFormProviderContext();
14
13
 
15
14
  const content = useMemo(
16
15
  () =>
17
16
  field.questions
18
- ?.filter((child) => !child.isHidden)
17
+ .map((child) => formFields.find((field) => field.id === child.id))
18
+ .filter((child) => !child.isHidden)
19
19
  .map((child, index) => {
20
20
  const key = `${child.id}_${index}`;
21
21
 
@@ -35,12 +35,12 @@ export const ObsGroup: React.FC<FormFieldInputProps> = ({ field, ...restProps })
35
35
  );
36
36
  }
37
37
  }),
38
- [field],
38
+ [field, formFields],
39
39
  );
40
40
 
41
41
  return (
42
42
  <div className={styles.groupContainer}>
43
- {showLabel ? (
43
+ {content.length > 1 ? (
44
44
  <FormGroup legendText={t(field.label)} className={styles.boldLegend}>
45
45
  {content}
46
46
  </FormGroup>
@@ -60,7 +60,12 @@ const ContentSwitcher: React.FC<FormFieldInputProps> = ({ field, value, errors,
60
60
  className={styles.selectedOption}
61
61
  size="md">
62
62
  {field.questionOptions.answers.map((option, index) => (
63
- <Switch name={option.concept || option.value} text={option.label} key={index} disabled={field.isDisabled} />
63
+ <Switch
64
+ name={option.concept || option.value}
65
+ text={t(option.label)}
66
+ key={index}
67
+ disabled={field.isDisabled}
68
+ />
64
69
  ))}
65
70
  </CdsContentSwitcher>
66
71
  </FormGroup>
@@ -113,7 +113,7 @@ const DateField: React.FC<FormFieldInputProps> = ({ field, value: dateValue, err
113
113
  id={field.id}
114
114
  labelText={timePickerLabel}
115
115
  placeholder="HH:MM"
116
- pattern="(1[012]|[1-9]):[0-5][0-9])$"
116
+ pattern="(1[012]|[1-9]):[0-5][0-9]$"
117
117
  type="time"
118
118
  disabled={field.datePickerFormat === 'timer' ? field.isDisabled : !dateValue ? true : false}
119
119
  invalid={errors.length > 0}
@@ -22,7 +22,7 @@ const MultiSelect: React.FC<FormFieldInputProps> = ({ field, value, errors, warn
22
22
  .map((answer, index) => ({
23
23
  id: `${field.id}-${answer.concept}`,
24
24
  concept: answer.concept,
25
- label: answer.label,
25
+ label: t(answer.label),
26
26
  key: index,
27
27
  disabled: answer.disable?.isDisabled,
28
28
  readonly: isTrue(field.readonly),
@@ -97,14 +97,14 @@ const MultiSelect: React.FC<FormFieldInputProps> = ({ field, value, errors, warn
97
97
  <Layer>
98
98
  {isSearchable ? (
99
99
  <FilterableMultiSelect
100
+ id={field.id}
101
+ key={field.id}
100
102
  placeholder={t('search', 'Search') + '...'}
101
103
  onChange={handleSelectItemsChange}
102
- id={t(field.label)}
103
104
  items={selectOptions}
104
105
  initialSelectedItems={initiallySelectedQuestionItems}
105
106
  label={''}
106
107
  titleText={label}
107
- key={field.id}
108
108
  itemToString={(item) => (item ? item.label : ' ')}
109
109
  disabled={field.isDisabled}
110
110
  invalid={errors.length > 0}
@@ -120,7 +120,7 @@ const MultiSelect: React.FC<FormFieldInputProps> = ({ field, value, errors, warn
120
120
  <Checkbox
121
121
  key={`${field.id}-${value.concept}`}
122
122
  className={styles.checkbox}
123
- labelText={value.label}
123
+ labelText={t(value.label)}
124
124
  id={`${field.id}-${value.concept}`}
125
125
  onChange={() => {
126
126
  handleSelectCheckbox(value);
@@ -70,14 +70,14 @@ const NumberField: React.FC<FormFieldInputProps> = ({ field, value, errors, warn
70
70
  onBlur={onBlur}
71
71
  allowEmpty={true}
72
72
  size="lg"
73
- hideSteppers={true}
73
+ hideSteppers={field.hideSteppers ?? false}
74
74
  onWheel={(e) => e.target.blur()}
75
75
  disabled={field.isDisabled}
76
76
  readOnly={isTrue(field.readonly)}
77
77
  className={classNames(styles.controlWidthConstrained, styles.boldedLabel)}
78
78
  warn={warnings.length > 0}
79
79
  warnText={warnings[0]?.message}
80
- step={0.01}
80
+ step={field.questionOptions.step ?? 0.01}
81
81
  />
82
82
  </Layer>
83
83
  )
@@ -50,7 +50,7 @@ const Radio: React.FC<FormFieldInputProps> = ({ field, value, errors, warnings,
50
50
  return (
51
51
  <RadioButton
52
52
  id={`${field.id}-${answer.label}`}
53
- labelText={answer.label ?? ''}
53
+ labelText={t(answer.label) ?? ''}
54
54
  value={answer.concept}
55
55
  key={index}
56
56
  onClick={(e) => {
@@ -27,9 +27,9 @@ const Dropdown: React.FC<FormFieldInputProps> = ({ field, value, errors, warning
27
27
  let answer = field.questionOptions.answers.find((opt) => {
28
28
  return opt.value ? opt.value == item : opt.concept == item;
29
29
  });
30
- return answer?.label;
30
+ return answer ? t(answer.label) : '';
31
31
  },
32
- [field.questionOptions.answers],
32
+ [field.questionOptions.answers, t],
33
33
  );
34
34
 
35
35
  const items = useMemo(() => {
@@ -1,21 +1,21 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { debounce } from 'lodash-es';
3
3
  import { ComboBox, DropdownSkeleton, Layer, InlineLoading } from '@carbon/react';
4
- import { isTrue } from '../../../utils/boolean-utils';
5
4
  import { useTranslation } from 'react-i18next';
6
- import { getRegisteredDataSource } from '../../../registry/registry';
5
+ import { useWatch } from 'react-hook-form';
6
+ import { type OpenmrsResource } from '@openmrs/esm-framework';
7
7
  import { getControlTemplate } from '../../../registry/inbuilt-components/control-templates';
8
- import { type DataSource, type FormFieldInputProps } from '../../../types';
8
+ import { getRegisteredDataSource } from '../../../registry/registry';
9
9
  import { isEmpty } from '../../../validators/form-validator';
10
+ import { isTrue } from '../../../utils/boolean-utils';
11
+ import { isViewMode } from '../../../utils/common-utils';
10
12
  import { shouldUseInlineLayout } from '../../../utils/form-helper';
11
- import FieldValueView from '../../value/view/field-value-view.component';
12
- import styles from './ui-select-extended.scss';
13
+ import { type DataSource, type FormFieldInputProps } from '../../../types';
13
14
  import { useFormProviderContext } from '../../../provider/form-provider';
14
- import FieldLabel from '../../field-label/field-label.component';
15
- import { useWatch } from 'react-hook-form';
16
15
  import useDataSourceDependentValue from '../../../hooks/useDataSourceDependentValue';
17
- import { isViewMode } from '../../../utils/common-utils';
18
- import { type OpenmrsResource } from '@openmrs/esm-framework';
16
+ import FieldLabel from '../../field-label/field-label.component';
17
+ import FieldValueView from '../../value/view/field-value-view.component';
18
+ import styles from './ui-select-extended.scss';
19
19
 
20
20
  const UiSelectExtended: React.FC<FormFieldInputProps> = ({ field, errors, warnings, setFieldValue }) => {
21
21
  const { t } = useTranslation();
@@ -49,6 +49,7 @@ const UiSelectExtended: React.FC<FormFieldInputProps> = ({ field, errors, warnin
49
49
 
50
50
  const debouncedSearch = debounce((searchTerm: string, dataSource: DataSource<OpenmrsResource>) => {
51
51
  setIsSearching(true);
52
+
52
53
  dataSource
53
54
  .fetchData(searchTerm, config)
54
55
  .then((dataItems) => {
@@ -86,22 +87,33 @@ const UiSelectExtended: React.FC<FormFieldInputProps> = ({ field, errors, warnin
86
87
  }, [field.questionOptions?.datasource]);
87
88
 
88
89
  useEffect(() => {
90
+ let ignore = false;
91
+
89
92
  // If not searchable, preload the items
90
93
  if (dataSource && !isTrue(field.questionOptions.isSearchable)) {
91
94
  setItems([]);
92
95
  setIsLoading(true);
96
+
93
97
  dataSource
94
98
  .fetchData(null, { ...config, referencedValue: dataSourceDependentValue })
95
99
  .then((dataItems) => {
96
- setItems(dataItems.map(dataSource.toUuidAndDisplay));
97
- setIsLoading(false);
100
+ if (!ignore) {
101
+ setItems(dataItems.map(dataSource.toUuidAndDisplay));
102
+ setIsLoading(false);
103
+ }
98
104
  })
99
105
  .catch((err) => {
100
- console.error(err);
101
- setIsLoading(false);
102
- setItems([]);
106
+ if (!ignore) {
107
+ console.error(err);
108
+ setIsLoading(false);
109
+ setItems([]);
110
+ }
103
111
  });
104
112
  }
113
+
114
+ return () => {
115
+ ignore = true;
116
+ };
105
117
  }, [dataSource, config, dataSourceDependentValue]);
106
118
 
107
119
  useEffect(() => {
@@ -111,19 +123,28 @@ const UiSelectExtended: React.FC<FormFieldInputProps> = ({ field, errors, warnin
111
123
  }, [dataSource, searchTerm, config]);
112
124
 
113
125
  useEffect(() => {
126
+ let ignore = false;
114
127
  if (value && !isDirty && dataSource && isSearchable && sessionMode !== 'enter' && !items.length) {
115
128
  // While in edit mode, search-based instances should fetch the initial item (previously selected value) to resolve its display property
116
129
  setIsLoading(true);
117
130
  try {
118
131
  dataSource.fetchSingleItem(value).then((item) => {
119
- setItems([dataSource.toUuidAndDisplay(item)]);
120
- setIsLoading(false);
132
+ if (!ignore) {
133
+ setItems([dataSource.toUuidAndDisplay(item)]);
134
+ setIsLoading(false);
135
+ }
121
136
  });
122
137
  } catch (error) {
123
- console.error(error);
124
- setIsLoading(false);
138
+ if (!ignore) {
139
+ console.error(error);
140
+ setIsLoading(false);
141
+ }
125
142
  }
126
143
  }
144
+
145
+ return () => {
146
+ ignore = true;
147
+ };
127
148
  }, [value, isDirty, sessionMode, dataSource, isSearchable, items]);
128
149
 
129
150
  if (isLoading) {
@@ -14,7 +14,6 @@ const mockUsePatient = jest.mocked(usePatient);
14
14
  const mockUseSession = jest.mocked(useSession);
15
15
  global.ResizeObserver = require('resize-observer-polyfill');
16
16
 
17
- jest.mock('../../../hooks/useRestMaxResultsCount', () => jest.fn().mockReturnValue({ systemSetting: { value: '50' } }));
18
17
  jest.mock('lodash-es/debounce', () => jest.fn((fn) => fn));
19
18
 
20
19
  jest.mock('lodash-es', () => ({
@@ -28,7 +28,9 @@ const WorkspaceLauncher: React.FC<FormFieldInputProps> = ({ field }) => {
28
28
  <div>
29
29
  <div className={styles.label}>{t(field.label)}</div>
30
30
  <div className={styles.workspaceButton}>
31
- <Button disabled={isTrue(field.readonly)} onClick={handleLaunchWorkspace}>{field.questionOptions?.buttonLabel ?? t('launchWorkspace')}</Button>
31
+ <Button disabled={isTrue(field.readonly)} onClick={handleLaunchWorkspace}>
32
+ {t(field.questionOptions?.buttonLabel) ?? t('launchWorkspace', 'Launch Workspace')}
33
+ </Button>
32
34
  </div>
33
35
  </div>
34
36
  )
@@ -1,13 +1,12 @@
1
1
  import React from 'react';
2
2
  import classNames from 'classnames';
3
3
  import { useTranslation } from 'react-i18next';
4
- import { Button } from '@carbon/react';
5
- import { type FormPage, type SessionMode } from '../../types';
6
- import styles from './sidebar.scss';
7
- import { usePageObserver } from './usePageObserver';
8
- import { useCurrentActivePage } from './useCurrentActivePage';
4
+ import { Button, InlineLoading } from '@carbon/react';
9
5
  import { isPageContentVisible } from '../../utils/form-helper';
10
- import { InlineLoading } from '@carbon/react';
6
+ import { useCurrentActivePage } from './useCurrentActivePage';
7
+ import { usePageObserver } from './usePageObserver';
8
+ import type { FormPage, SessionMode } from '../../types';
9
+ import styles from './sidebar.scss';
11
10
 
12
11
  interface SidebarProps {
13
12
  defaultPage: string;
@@ -1,4 +1,5 @@
1
1
  @use '@carbon/colors';
2
+ @use '@carbon/layout';
2
3
  @use '@carbon/type';
3
4
 
4
5
  .pageLink {
@@ -95,6 +96,10 @@
95
96
  .saveButton {
96
97
  @extend .button;
97
98
  margin-bottom: 0.625rem;
99
+
100
+ &:global(.cds--inline-loading) {
101
+ min-height: layout.$spacing-05;
102
+ }
98
103
  }
99
104
 
100
105
  .closeButton {
@@ -1,10 +1,13 @@
1
1
  import React from 'react';
2
2
  import styles from './value.scss';
3
+ import { useTranslation } from 'react-i18next';
3
4
 
4
5
  export const ValueEmpty = () => {
6
+ const { t } = useTranslation();
7
+
5
8
  return (
6
9
  <div>
7
- <span className={styles.empty}>(Blank)</span>
10
+ <span className={styles.empty}>({t('blank', 'Blank')})</span>
8
11
  </div>
9
12
  );
10
13
  };
@@ -1,24 +1,24 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import type { FormField, FormSchema, SessionMode } from './types';
3
- import { useSession, type Visit } from '@openmrs/esm-framework';
4
- import { isEmpty, useFormJson } from '.';
5
- import FormProcessorFactory from './components/processor-factory/form-processor-factory.component';
6
- import Loader from './components/loaders/loader.component';
7
- import { usePatientData } from './hooks/usePatientData';
8
- import { FormFactoryProvider } from './provider/form-factory-provider';
9
2
  import classNames from 'classnames';
10
- import styles from './form-engine.scss';
11
3
  import { Button, ButtonSet, InlineLoading } from '@carbon/react';
12
4
  import { I18nextProvider, useTranslation } from 'react-i18next';
13
- import PatientBanner from './components/patient-banner/patient-banner.component';
14
- import MarkdownWrapper from './components/inputs/markdown/markdown-wrapper.component';
5
+ import { useSession, type Visit } from '@openmrs/esm-framework';
6
+ import { FormFactoryProvider } from './provider/form-factory-provider';
15
7
  import { init, teardown } from './lifecycle';
16
- import { reportError } from './utils/error-utils';
8
+ import { isEmpty, useFormJson } from '.';
17
9
  import { moduleName } from './globals';
10
+ import { reportError } from './utils/error-utils';
18
11
  import { useFormCollapse } from './hooks/useFormCollapse';
19
- import Sidebar from './components/sidebar/sidebar.component';
20
12
  import { useFormWorkspaceSize } from './hooks/useFormWorkspaceSize';
21
13
  import { usePageObserver } from './components/sidebar/usePageObserver';
14
+ import { usePatientData } from './hooks/usePatientData';
15
+ import type { FormField, FormSchema, SessionMode } from './types';
16
+ import FormProcessorFactory from './components/processor-factory/form-processor-factory.component';
17
+ import Loader from './components/loaders/loader.component';
18
+ import MarkdownWrapper from './components/inputs/markdown/markdown-wrapper.component';
19
+ import PatientBanner from './components/patient-banner/patient-banner.component';
20
+ import Sidebar from './components/sidebar/sidebar.component';
21
+ import styles from './form-engine.scss';
22
22
 
23
23
  interface FormEngineProps {
24
24
  patientUUID: string;
@@ -176,7 +176,11 @@ const FormEngine = ({
176
176
  }}>
177
177
  {mode === 'view' ? t('close', 'Close') : t('cancel', 'Cancel')}
178
178
  </Button>
179
- <Button type="submit" disabled={isLoadingDependencies || mode === 'view'}>
179
+ <Button
180
+ className={styles.saveButton}
181
+ disabled={isLoadingDependencies || isSubmitting || mode === 'view'}
182
+ kind="primary"
183
+ type="submit">
180
184
  {isSubmitting ? (
181
185
  <InlineLoading description={t('submitting', 'Submitting') + '...'} />
182
186
  ) : (
@@ -1,3 +1,5 @@
1
+ @use '@carbon/layout';
2
+
1
3
  // replaces .formEngine
2
4
  .form {
3
5
  height: 100%;
@@ -138,3 +140,9 @@
138
140
  width: 10%;
139
141
  }
140
142
  }
143
+
144
+ .saveButton {
145
+ &:global(.cds--inline-loading) {
146
+ min-height: layout.$spacing-05;
147
+ }
148
+ }
@@ -92,7 +92,6 @@ jest.mock('../src/api', () => {
92
92
  };
93
93
  });
94
94
 
95
- jest.mock('./hooks/useRestMaxResultsCount', () => jest.fn().mockReturnValue({ systemSetting: { value: '50' } }));
96
95
  jest.mock('./hooks/useEncounterRole', () => ({
97
96
  useEncounterRole: jest.fn().mockReturnValue({
98
97
  isLoading: false,
@@ -877,6 +876,50 @@ describe('Form engine component', () => {
877
876
  });
878
877
 
879
878
  describe('Obs group', () => {
879
+ it('should not render empty obs group', async () => {
880
+ await act(async () => {
881
+ renderForm(null, obsGroupTestForm);
882
+ });
883
+
884
+ // Check that only one obs group is initially rendered
885
+ const initialGroups = screen.getAllByRole('group', { name: /My Group|Dependents Group/i });
886
+ expect(initialGroups.length).toBe(1);
887
+ const dependentTypeRadios = screen.queryAllByRole('radio', { name: /child|spouse/i });
888
+ expect(dependentTypeRadios.length).toBe(0);
889
+
890
+ // Select "Yes" for having dependents
891
+ const yesRadio = screen.getByRole('radio', { name: /yes/i });
892
+ await user.click(yesRadio);
893
+
894
+ // Now the dependent type radios should be visible
895
+ const visibleDependentTypeRadios = screen.getAllByRole('radio', { name: /child|spouse/i });
896
+ expect(visibleDependentTypeRadios.length).toBe(2);
897
+
898
+ // Check that the group label is still hidden since it only has one visible field
899
+ const dependentsGroupResults = screen.queryAllByRole('group', { name: /Dependents Group/i });
900
+ expect(dependentsGroupResults.length).toBe(0);
901
+
902
+ // Check that dependent name and age are still hidden
903
+ const hiddenDependentNameInput = screen.queryByRole('textbox', { name: /dependent name/i });
904
+ const hiddenDependentAgeInput = screen.queryByRole('spinbutton', { name: /dependent age/i });
905
+ expect(hiddenDependentNameInput).toBeNull();
906
+ expect(hiddenDependentAgeInput).toBeNull();
907
+
908
+ // Select "Child" as dependent type
909
+ await user.click(visibleDependentTypeRadios[0]);
910
+
911
+ // Check the visibility of the group label
912
+ const dependentsGroup = screen.getAllByRole('group', { name: /Dependents Group/i })[0];
913
+ expect(dependentsGroup).toBeInTheDocument();
914
+
915
+ // Check that dependent name and age are now visible
916
+ const dependentNameInput = screen.getByRole('textbox', { name: /dependent name/i });
917
+ const dependentAgeInput = screen.getByRole('spinbutton', { name: /dependent age/i });
918
+
919
+ expect(dependentNameInput).toBeInTheDocument();
920
+ expect(dependentAgeInput).toBeInTheDocument();
921
+ });
922
+
880
923
  it('should save obs group on form submission', async () => {
881
924
  const saveEncounterMock = jest.spyOn(api, 'saveEncounter');
882
925
  await act(async () => {
@@ -1,6 +1,6 @@
1
1
  import { useMemo } from 'react';
2
- import { type FetchResponse, type OpenmrsResource, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
3
2
  import useSWRInfinite from 'swr/infinite';
3
+ import { type FetchResponse, type OpenmrsResource, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
4
4
 
5
5
  type ConceptFetchResponse = FetchResponse<{ results: Array<OpenmrsResource> }>;
6
6
 
@@ -12,7 +12,6 @@ export function useConcepts(references: Set<string>): {
12
12
  isLoading: boolean;
13
13
  error: Error | undefined;
14
14
  } {
15
-
16
15
  const chunkSize = 100;
17
16
  const totalCount = references.size;
18
17
  const totalPages = Math.ceil(totalCount / chunkSize);
@@ -1,6 +1,6 @@
1
+ import { useWatch } from 'react-hook-form';
1
2
  import { type FormField } from '../types';
2
3
  import { useFormProviderContext } from '../provider/form-provider';
3
- import { useWatch } from 'react-hook-form';
4
4
 
5
5
  const useDataSourceDependentValue = (field: FormField) => {
6
6
  const dependentField = field.questionOptions['config']?.referencedField;
@@ -11,8 +11,12 @@ export function useEncounter(formJson: FormSchema) {
11
11
  const [error, setError] = useState(null);
12
12
 
13
13
  useEffect(() => {
14
+ const abortController = new AbortController();
15
+
14
16
  if (!isEmpty(formJson.encounter) && isString(formJson.encounter)) {
15
- openmrsFetch(`${restBaseUrl}/encounter/${formJson.encounter}?v=${encounterRepresentation}`)
17
+ openmrsFetch(`${restBaseUrl}/encounter/${formJson.encounter}?v=${encounterRepresentation}`, {
18
+ signal: abortController.signal,
19
+ })
16
20
  .then((response) => {
17
21
  setEncounter(response.data);
18
22
  setIsLoading(false);
@@ -26,6 +30,10 @@ export function useEncounter(formJson: FormSchema) {
26
30
  } else {
27
31
  setIsLoading(false);
28
32
  }
33
+
34
+ return () => {
35
+ abortController.abort();
36
+ };
29
37
  }, [formJson.encounter]);
30
38
 
31
39
  return { encounter: encounter, error, isLoading };
@@ -9,7 +9,10 @@ import { moduleName } from '../globals';
9
9
  export function useFormJson(formUuid: string, rawFormJson: any, encounterUuid: string, formSessionIntent: string) {
10
10
  const [formJson, setFormJson] = useState<FormSchema>(null);
11
11
  const [error, setError] = useState(validateFormsArgs(formUuid, rawFormJson));
12
+
12
13
  useEffect(() => {
14
+ const abortController = new AbortController();
15
+
13
16
  const setFormJsonWithTranslations = (formJson: FormSchema) => {
14
17
  if (formJson?.translations) {
15
18
  const language = window.i18next.language;
@@ -17,14 +20,21 @@ export function useFormJson(formUuid: string, rawFormJson: any, encounterUuid: s
17
20
  }
18
21
  setFormJson(formJson);
19
22
  };
23
+
20
24
  loadFormJson(formUuid, rawFormJson, formSessionIntent)
21
25
  .then((formJson) => {
22
26
  setFormJsonWithTranslations({ ...formJson, encounter: encounterUuid });
23
27
  })
24
28
  .catch((error) => {
25
- console.error(error);
26
- setError(new Error('Error loading form JSON: ' + error.message));
29
+ if (error.name !== 'AbortError') {
30
+ console.error(error);
31
+ setError(new Error('Error loading form JSON: ' + error.message));
32
+ }
27
33
  });
34
+
35
+ return () => {
36
+ abortController.abort();
37
+ };
28
38
  }, [formSessionIntent, formUuid, rawFormJson, encounterUuid]);
29
39
 
30
40
  return {
@@ -24,7 +24,7 @@ const patientGenderMap = {
24
24
  export const usePatientData = (patientUuid) => {
25
25
  const { patient, isLoading: isLoadingPatient, error: patientError } = usePatient(patientUuid);
26
26
  if (patient && !isLoadingPatient) {
27
- // This is to support backward compatibility with the AMPATH JSON format
27
+ // This is for backward compatibility with the Angular form engine
28
28
  patient['age'] = calculateAge(new Date(patient?.birthDate));
29
29
  patient['sex'] = patientGenderMap[patient.gender] ?? 'U';
30
30
  }
@@ -1,32 +1,38 @@
1
+ import useSWR from 'swr';
1
2
  import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
- import { useEffect, useState } from 'react';
3
- import { type FormSchema, type PatientProgram } from '../types';
4
- const customRepresentation = `custom:(uuid,display,program:(uuid,name,allWorkflows),dateEnrolled,dateCompleted,location:(uuid,display),states:(startDate,endDate,state:(uuid,name,retired,concept:(uuid),programWorkflow:(uuid)))`;
3
+ import type { FormSchema, ProgramsFetchResponse } from '../types';
5
4
 
6
- export const usePatientPrograms = (patientUuid: string, formJson: FormSchema) => {
7
- const [patientPrograms, setPatientPrograms] = useState<Array<PatientProgram>>([]);
8
- const [isLoading, setIsLoading] = useState(true);
9
- const [error, setError] = useState(null);
5
+ const useActiveProgramEnrollments = (patientUuid: string) => {
6
+ const customRepresentation = `custom:(uuid,display,program:(uuid,name,allWorkflows),dateEnrolled,dateCompleted,location:(uuid,display),states:(startDate,endDate,state:(uuid,name,retired,concept:(uuid),programWorkflow:(uuid)))`;
7
+ const apiUrl = `${restBaseUrl}/programenrollment?patient=${patientUuid}&v=${customRepresentation}`;
8
+
9
+ const { data, error, isLoading } = useSWR<{ data: ProgramsFetchResponse }, Error>(
10
+ patientUuid ? apiUrl : null,
11
+ openmrsFetch,
12
+ );
13
+
14
+ const sortedEnrollments =
15
+ data?.data?.results.length > 0
16
+ ? data?.data.results.sort((a, b) => (b.dateEnrolled > a.dateEnrolled ? 1 : -1))
17
+ : null;
10
18
 
11
- useEffect(() => {
12
- if (formJson.meta?.programs?.hasProgramFields) {
13
- openmrsFetch(`${restBaseUrl}/programenrollment?patient=${patientUuid}&v=${customRepresentation}`)
14
- .then((response) => {
15
- setPatientPrograms(response.data.results.filter((enrollment) => enrollment.dateCompleted === null));
16
- setIsLoading(false);
17
- })
18
- .catch((error) => {
19
- setError(error);
20
- setIsLoading(false);
21
- });
22
- } else {
23
- setIsLoading(false);
24
- }
25
- }, [formJson]);
19
+ const activePrograms = sortedEnrollments?.filter((enrollment) => !enrollment.dateCompleted);
26
20
 
27
21
  return {
28
- patientPrograms,
22
+ activePrograms,
29
23
  error,
30
24
  isLoading,
31
25
  };
32
26
  };
27
+
28
+ export const usePatientPrograms = (patientUuid: string, formJson: FormSchema) => {
29
+ const { activePrograms, error, isLoading } = useActiveProgramEnrollments(
30
+ formJson.meta?.programs?.hasProgramFields ? patientUuid : null,
31
+ );
32
+
33
+ return {
34
+ patientPrograms: activePrograms,
35
+ errorFetchingPatientPrograms: error,
36
+ isLoadingPatientPrograms: isLoading,
37
+ };
38
+ };
@@ -13,17 +13,27 @@ const useProcessorDependencies = (
13
13
  const { loadDependencies } = formProcessor;
14
14
 
15
15
  useEffect(() => {
16
+ let ignore = false;
17
+
16
18
  if (loadDependencies) {
17
19
  setIsLoading(true);
18
20
  loadDependencies(context, setContext)
19
- .then((results) => {
20
- setIsLoading(false);
21
+ .then(() => {
22
+ if (!ignore) {
23
+ setIsLoading(false);
24
+ }
21
25
  })
22
26
  .catch((error) => {
23
- setError(error);
24
- reportError(error, 'Encountered error while loading dependencies');
27
+ if (!ignore) {
28
+ setError(error);
29
+ reportError(error, 'Encountered error while loading dependencies');
30
+ }
25
31
  });
26
32
  }
33
+
34
+ return () => {
35
+ ignore = true;
36
+ };
27
37
  }, [loadDependencies]);
28
38
 
29
39
  return { isLoading, error };