@openmrs/esm-patient-forms-app 11.3.1-patch.9310 → 11.3.1-patch.9508

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 (69) hide show
  1. package/.turbo/turbo-build.log +20 -22
  2. package/dist/1123.js +2 -0
  3. package/dist/1123.js.LICENSE.txt +9 -0
  4. package/dist/1123.js.map +1 -0
  5. package/dist/2773.js +1 -0
  6. package/dist/2773.js.map +1 -0
  7. package/dist/4055.js +1 -1
  8. package/dist/4078.js +1 -1
  9. package/dist/4078.js.map +1 -1
  10. package/dist/439.js +1 -0
  11. package/dist/5670.js +1 -1
  12. package/dist/5696.js +1 -0
  13. package/dist/5696.js.map +1 -0
  14. package/dist/6199.js +2 -0
  15. package/dist/6199.js.map +1 -0
  16. package/dist/6589.js +1 -0
  17. package/dist/7003.js +1 -1
  18. package/dist/7003.js.map +1 -1
  19. package/dist/7007.js +1 -1
  20. package/dist/7007.js.map +1 -1
  21. package/dist/707.js +1 -1
  22. package/dist/707.js.map +1 -1
  23. package/dist/7906.js +2 -0
  24. package/dist/{5792.js.LICENSE.txt → 7906.js.LICENSE.txt} +0 -10
  25. package/dist/7906.js.map +1 -0
  26. package/dist/8371.js +1 -0
  27. package/dist/8803.js +1 -0
  28. package/dist/8803.js.map +1 -0
  29. package/dist/main.js +1 -1
  30. package/dist/main.js.map +1 -1
  31. package/dist/openmrs-esm-patient-forms-app.js +1 -1
  32. package/dist/openmrs-esm-patient-forms-app.js.buildmanifest.json +205 -109
  33. package/dist/openmrs-esm-patient-forms-app.js.map +1 -1
  34. package/dist/routes.json +1 -1
  35. package/package.json +2 -2
  36. package/src/clinical-form-action-button.component.tsx +40 -13
  37. package/src/clinical-form-action-button.test.tsx +22 -23
  38. package/src/config-schema.ts +1 -1
  39. package/src/forms/form-entry.test.tsx +28 -14
  40. package/src/forms/form-entry.workspace.tsx +99 -19
  41. package/src/forms/forms-dashboard.component.tsx +58 -13
  42. package/src/forms/forms-dashboard.test.tsx +15 -3
  43. package/src/forms/forms-dashboard.workspace.tsx +10 -33
  44. package/src/forms/forms-list.component.tsx +41 -20
  45. package/src/forms/forms-list.test.tsx +2 -2
  46. package/src/forms/forms-table.component.tsx +1 -1
  47. package/src/hooks/use-forms.ts +4 -3
  48. package/src/htmlformentry/html-form-entry.workspace.tsx +54 -0
  49. package/src/index.ts +9 -1
  50. package/src/offline-forms/use-offline-form-encounters.ts +2 -2
  51. package/src/offline.ts +7 -5
  52. package/src/routes.json +44 -13
  53. package/translations/cs.json +22 -0
  54. package/translations/fr.json +2 -2
  55. package/translations/sq.json +22 -0
  56. package/translations/zh_TW.json +22 -0
  57. package/dist/2621.js +0 -2
  58. package/dist/2621.js.map +0 -1
  59. package/dist/4341.js +0 -1
  60. package/dist/4341.js.map +0 -1
  61. package/dist/5792.js +0 -2
  62. package/dist/5792.js.map +0 -1
  63. package/dist/9649.js +0 -1
  64. package/dist/9649.js.map +0 -1
  65. package/src/forms/exported-form-entry.workspace.tsx +0 -32
  66. package/src/forms/exported-forms-dashboard.workspace.tsx +0 -45
  67. package/src/forms/form-entry.component.tsx +0 -138
  68. package/src/forms/form-entry.resources.ts +0 -37
  69. /package/dist/{2621.js.LICENSE.txt → 6199.js.LICENSE.txt} +0 -0
@@ -1,27 +1,54 @@
1
1
  import React, { type ComponentProps } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
- import { ActionMenuButton2, DocumentIcon } from '@openmrs/esm-framework';
4
- import { type PatientChartWorkspaceActionButtonProps, useStartVisitIfNeeded } from '@openmrs/esm-patient-common-lib';
3
+ import { ActionMenuButton, DocumentIcon, launchWorkspace, useWorkspaces } from '@openmrs/esm-framework';
4
+ import {
5
+ clinicalFormsWorkspace,
6
+ formEntryWorkspace,
7
+ htmlFormEntryWorkspace,
8
+ useLaunchWorkspaceRequiringVisit,
9
+ } from '@openmrs/esm-patient-common-lib';
5
10
 
6
11
  /**
7
12
  * This button uses the patient chart store and MUST only be used
8
13
  * within the patient chart
9
14
  */
10
- const ClinicalFormActionButton: React.FC<PatientChartWorkspaceActionButtonProps> = ({
11
- groupProps: { patientUuid },
12
- }) => {
15
+ const ClinicalFormActionButton: React.FC<{ patientUuid: string }> = ({ patientUuid }) => {
13
16
  const { t } = useTranslation();
14
- const startVisitIfNeeded = useStartVisitIfNeeded(patientUuid);
17
+ const { workspaces } = useWorkspaces();
18
+ const launchFormsWorkspace = useLaunchWorkspaceRequiringVisit(patientUuid, clinicalFormsWorkspace);
19
+
20
+ const formEntryWorkspaces = workspaces.filter((w) => w.name === formEntryWorkspace);
21
+ const recentlyOpenedForm = formEntryWorkspaces[0];
22
+
23
+ const htmlFormEntryWorkspaces = workspaces.filter((w) => w.name === htmlFormEntryWorkspace);
24
+ const recentlyOpenedHtmlForm = htmlFormEntryWorkspaces[0];
25
+
26
+ const isFormOpen = formEntryWorkspaces?.length >= 1;
27
+ const isHtmlFormOpen = htmlFormEntryWorkspaces?.length >= 1;
28
+
29
+ const launchPatientWorkspaceCb = () => {
30
+ if (isFormOpen) {
31
+ launchWorkspace(formEntryWorkspace, {
32
+ workspaceTitle: recentlyOpenedForm?.additionalProps?.['workspaceTitle'],
33
+ });
34
+ }
35
+ // we aren't currently supporting keeping HTML Form workspaces open, but just in case
36
+ else if (isHtmlFormOpen) {
37
+ launchWorkspace(htmlFormEntryWorkspace, {
38
+ workspaceTitle: recentlyOpenedHtmlForm?.additionalProps?.['workspaceTitle'],
39
+ });
40
+ } else {
41
+ launchFormsWorkspace();
42
+ }
43
+ };
15
44
 
16
45
  return (
17
- <ActionMenuButton2
18
- icon={(props: ComponentProps<typeof DocumentIcon>) => <DocumentIcon {...props} />}
46
+ <ActionMenuButton
47
+ getIcon={(props: ComponentProps<typeof DocumentIcon>) => <DocumentIcon {...props} />}
19
48
  label={t('clinicalForms', 'Clinical forms')}
20
- workspaceToLaunch={{
21
- workspaceName: 'clinical-forms-workspace',
22
- workspaceProps: {},
23
- }}
24
- onBeforeWorkspaceLaunch={startVisitIfNeeded}
49
+ iconDescription={t('clinicalForms', 'Clinical forms')}
50
+ handler={launchPatientWorkspaceCb}
51
+ type={'clinical-form'}
25
52
  />
26
53
  );
27
54
  };
@@ -1,47 +1,46 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
- import { ActionMenuButton2, useLayoutType } from '@openmrs/esm-framework';
3
+ import { ActionMenuButton, useLayoutType, useWorkspaces } from '@openmrs/esm-framework';
4
4
  import ClinicalFormActionButton from './clinical-form-action-button.component';
5
- import { mockPatient } from 'tools';
6
5
 
7
- const mockActionMenuButton = jest.mocked(ActionMenuButton2);
8
6
  const mockUseLayoutType = jest.mocked(useLayoutType);
7
+ const mockUseWorkspaces = useWorkspaces as jest.Mock;
8
+ const mockActionMenuButton = jest.mocked(ActionMenuButton);
9
9
 
10
- mockActionMenuButton.mockImplementation(({ label, tagContent }) => (
11
- <button>
10
+ mockActionMenuButton.mockImplementation(({ handler, label, tagContent }) => (
11
+ <button onClick={handler}>
12
12
  {tagContent} {label}
13
13
  </button>
14
14
  ));
15
15
 
16
- jest.mock('@openmrs/esm-patient-common-lib', () => {
17
- const originalModule = jest.requireActual('@openmrs/esm-patient-common-lib');
18
-
19
- return {
20
- ...originalModule,
21
- useStartVisitIfNeeded: jest.fn(() => () => Promise.resolve(true)),
22
- };
23
- });
16
+ mockUseWorkspaces.mockImplementation(() => ({
17
+ active: true,
18
+ windowState: 'normal',
19
+ workspaces: [
20
+ {
21
+ canHide: false,
22
+ name: 'clinical-forms-workspace',
23
+ title: 'Clinical forms',
24
+ preferredWindowSize: 'normal',
25
+ type: 'form',
26
+ },
27
+ ],
28
+ workspaceWindowState: 'normal',
29
+ prompt: null,
30
+ }));
24
31
 
25
32
  describe('<ClinicalFormActionButton>', () => {
26
33
  test('should display clinical form action button on tablet view', () => {
27
34
  mockUseLayoutType.mockReturnValue('tablet');
28
35
 
29
- render(
30
- <ClinicalFormActionButton
31
- groupProps={{ patientUuid: mockPatient.id, patient: mockPatient, visitContext: null, mutateVisitContext: null }}
32
- />,
33
- );
36
+ render(<ClinicalFormActionButton patientUuid={'some-uuid'} />);
34
37
  expect(screen.getByRole('button', { name: /Clinical forms/i })).toBeInTheDocument();
35
38
  });
36
39
 
37
40
  test('should display clinical form action button on desktop view', () => {
38
41
  mockUseLayoutType.mockReturnValue('small-desktop');
39
42
 
40
- render(
41
- <ClinicalFormActionButton
42
- groupProps={{ patientUuid: mockPatient.id, patient: mockPatient, visitContext: null, mutateVisitContext: null }}
43
- />,
44
- );
43
+ render(<ClinicalFormActionButton patientUuid={'some-uuid'} />);
45
44
  const clinicalActionButton = screen.getByRole('button', { name: /Form/i });
46
45
  expect(clinicalActionButton).toBeInTheDocument();
47
46
  });
@@ -123,7 +123,7 @@ export interface FormsSection {
123
123
  forms: Array<string>;
124
124
  }
125
125
 
126
- export interface FormEntryConfigSchema {
126
+ export interface ConfigObject {
127
127
  htmlFormEntryForms: Array<HtmlFormEntryForm>;
128
128
  formSections: Array<FormsSection>;
129
129
  customFormsUrl: string;
@@ -1,25 +1,39 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
3
  import { BehaviorSubject } from 'rxjs';
4
- import { ExtensionSlot, useConnectivity } from '@openmrs/esm-framework';
4
+ import { ExtensionSlot, useConnectivity, useFeatureFlag } from '@openmrs/esm-framework';
5
5
  import { mockPatient } from 'tools';
6
- import FormEntry, { type FormEntryProps } from './form-entry.component';
6
+ import FormEntry from './form-entry.workspace';
7
7
 
8
- const defaultProps: FormEntryProps = {
9
- form: {
10
- uuid: 'some-form-uuid',
11
- name: '',
12
- version: '',
13
- published: false,
14
- retired: false,
15
- resources: [],
8
+ const mockCurrentVisit = {
9
+ uuid: '17f512b4-d264-4113-a6fe-160cb38cb46e',
10
+ encounters: [],
11
+ patient: { uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' },
12
+ visitType: {
13
+ uuid: '7b0f5697-27e3-40c4-8bae-f4049abfb4ed',
14
+ display: 'Facility Visit',
16
15
  },
17
- encounterUuid: 'some-encounter-uuid',
16
+ attributes: [],
17
+ startDatetime: '2021-03-16T08:16:00.000+0000',
18
+ stopDatetime: null,
19
+ location: {
20
+ uuid: '6351fcf4-e311-4a19-90f9-35667d99a8af',
21
+ name: 'Registration Desk',
22
+ display: 'Registration Desk',
23
+ },
24
+ };
25
+
26
+ const testProps = {
27
+ closeWorkspace: jest.fn(),
28
+ closeWorkspaceWithSavedChanges: jest.fn(),
29
+ promptBeforeClosing: jest.fn(),
18
30
  patientUuid: mockPatient.id,
19
31
  patient: mockPatient,
20
- visitContext: null,
32
+ formInfo: { formUuid: 'some-form-uuid' },
33
+ mutateForm: jest.fn(),
34
+ setTitle: jest.fn(),
35
+ visitContext: mockCurrentVisit,
21
36
  mutateVisitContext: null,
22
- closeWorkspace: jest.fn(),
23
37
  };
24
38
 
25
39
  const mockFormEntrySub = jest.fn();
@@ -35,7 +49,7 @@ describe('FormEntry', () => {
35
49
  );
36
50
  mockExtensionSlot.mockImplementation((ext) => ext.name as any);
37
51
 
38
- render(<FormEntry {...defaultProps} />);
52
+ render(<FormEntry {...testProps} />);
39
53
 
40
54
  await screen.findByText(/form-widget-slot/);
41
55
  expect(screen.getByText(/form-widget-slot/)).toBeInTheDocument();
@@ -1,28 +1,108 @@
1
- import React from 'react';
2
- import { type Form, type PatientWorkspace2DefinitionProps } from '@openmrs/esm-patient-common-lib';
3
- import FormEntry from './form-entry.component';
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { useSWRConfig } from 'swr';
3
+ import { ExtensionSlot, useConnectivity } from '@openmrs/esm-framework';
4
+ import {
5
+ clinicalFormsWorkspace,
6
+ invalidateVisitAndEncounterData,
7
+ type DefaultPatientWorkspaceProps,
8
+ type FormEntryProps,
9
+ } from '@openmrs/esm-patient-common-lib';
4
10
 
5
- interface FormEntryWorkspaceProps {
6
- form: Form;
7
- encounterUuid: string;
11
+ interface FormEntryComponentProps extends DefaultPatientWorkspaceProps {
12
+ mutateForm: () => void;
13
+ formInfo: FormEntryProps;
14
+ clinicalFormsWorkspaceName?: string;
8
15
  }
9
16
 
10
- const FormEntryWorkspace: React.FC<PatientWorkspace2DefinitionProps<FormEntryWorkspaceProps, {}>> = ({
17
+ const FormEntry: React.FC<FormEntryComponentProps> = ({
18
+ patientUuid,
19
+ patient,
20
+ clinicalFormsWorkspaceName = clinicalFormsWorkspace,
11
21
  closeWorkspace,
12
- workspaceProps: { form, encounterUuid },
13
- groupProps: { patientUuid, patient, visitContext, mutateVisitContext },
22
+ closeWorkspaceWithSavedChanges,
23
+ promptBeforeClosing,
24
+ mutateForm,
25
+ formInfo,
26
+ visitContext,
27
+ mutateVisitContext,
14
28
  }) => {
29
+ const { encounterUuid, formUuid, visitStartDatetime, visitStopDatetime, visitTypeUuid, visitUuid, additionalProps } =
30
+ formInfo || {};
31
+ const [showForm, setShowForm] = useState(true);
32
+ const isOnline = useConnectivity();
33
+ const { mutate: globalMutate } = useSWRConfig();
34
+
35
+ const state = useMemo(
36
+ () => ({
37
+ view: 'form',
38
+ formUuid: formUuid ?? null,
39
+ visitUuid: visitUuid ?? visitContext?.uuid ?? null,
40
+ visitTypeUuid: visitTypeUuid ?? visitContext?.visitType?.uuid ?? null,
41
+ visitStartDatetime: visitStartDatetime ?? visitContext?.startDatetime ?? null,
42
+ visitStopDatetime: visitStopDatetime ?? visitContext?.stopDatetime ?? null,
43
+ isOffline: !isOnline,
44
+ patientUuid: patientUuid ?? null,
45
+ patient,
46
+ encounterUuid: encounterUuid ?? null,
47
+ closeWorkspace: () => {
48
+ typeof mutateForm === 'function' && mutateForm();
49
+ closeWorkspace();
50
+ },
51
+ closeWorkspaceWithSavedChanges: () => {
52
+ typeof mutateForm === 'function' && mutateForm();
53
+ // Update current visit data for critical components
54
+ mutateVisitContext?.();
55
+
56
+ // Also invalidate visit history and encounter tables since form submission may create/update encounters
57
+ invalidateVisitAndEncounterData(globalMutate, patientUuid);
58
+
59
+ closeWorkspaceWithSavedChanges();
60
+ },
61
+ promptBeforeClosing,
62
+ additionalProps,
63
+ clinicalFormsWorkspaceName,
64
+ }),
65
+ [
66
+ additionalProps,
67
+ clinicalFormsWorkspaceName,
68
+ closeWorkspace,
69
+ closeWorkspaceWithSavedChanges,
70
+ visitContext?.startDatetime,
71
+ visitContext?.stopDatetime,
72
+ visitContext?.uuid,
73
+ visitContext?.visitType?.uuid,
74
+ encounterUuid,
75
+ formUuid,
76
+ globalMutate,
77
+ isOnline,
78
+ mutateVisitContext,
79
+ mutateForm,
80
+ patient,
81
+ patientUuid,
82
+ promptBeforeClosing,
83
+ visitStartDatetime,
84
+ visitStopDatetime,
85
+ visitTypeUuid,
86
+ visitUuid,
87
+ ],
88
+ );
89
+
90
+ // FIXME: This logic triggers a reload of the form when the formUuid changes. It's a workaround for the fact that the form doesn't reload when the formUuid changes.
91
+ useEffect(() => {
92
+ if (state.formUuid) {
93
+ setShowForm(false);
94
+ setTimeout(() => {
95
+ setShowForm(true);
96
+ });
97
+ }
98
+ }, [state]);
99
+
15
100
  return (
16
- <FormEntry
17
- form={form}
18
- encounterUuid={encounterUuid}
19
- patient={patient}
20
- patientUuid={patientUuid}
21
- visitContext={visitContext}
22
- mutateVisitContext={mutateVisitContext}
23
- closeWorkspace={closeWorkspace}
24
- />
101
+ <div>
102
+ <ExtensionSlot name="visit-context-header-slot" state={{ patientUuid }} />
103
+ {showForm && formInfo && patientUuid && patient && <ExtensionSlot name="form-widget-slot" state={state} />}
104
+ </div>
25
105
  );
26
106
  };
27
107
 
28
- export default FormEntryWorkspace;
108
+ export default FormEntry;
@@ -1,28 +1,73 @@
1
- import React, { useMemo } from 'react';
1
+ import React, { useCallback, useMemo } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { Tile } from '@carbon/react';
4
- import { ResponsiveWrapper, useConfig, useConnectivity, type Visit } from '@openmrs/esm-framework';
5
- import { EmptyDataIllustration, type Form } from '@openmrs/esm-patient-common-lib';
6
- import type { FormEntryConfigSchema } from '../config-schema';
4
+ import { ResponsiveWrapper, useConfig, useConnectivity } from '@openmrs/esm-framework';
5
+ import {
6
+ type DefaultPatientWorkspaceProps,
7
+ EmptyDataIllustration,
8
+ type Form,
9
+ launchFormEntryOrHtmlForms,
10
+ mapFormsToHtmlFormEntryForms,
11
+ } from '@openmrs/esm-patient-common-lib';
12
+ import type { ConfigObject } from '../config-schema';
7
13
  import { useForms } from '../hooks/use-forms';
8
14
  import FormsList from './forms-list.component';
9
15
  import styles from './forms-dashboard.scss';
10
16
 
11
- interface FormsDashbaordProps {
12
- handleFormOpen: (form: Form, encounterUuid: string) => void;
13
- patient: fhir.Patient;
14
- visitContext: Visit;
17
+ interface FormsDashboardProps extends DefaultPatientWorkspaceProps {
18
+ clinicalFormsWorkspaceName?: string;
19
+ formEntryWorkspaceName?: string;
20
+ htmlFormEntryWorkspaceName?: string;
15
21
  }
16
22
 
17
- const FormsDashboard: React.FC<FormsDashbaordProps> = ({ handleFormOpen, patient, visitContext }) => {
23
+ const FormsDashboard: React.FC<FormsDashboardProps> = ({
24
+ patientUuid,
25
+ clinicalFormsWorkspaceName,
26
+ formEntryWorkspaceName,
27
+ htmlFormEntryWorkspaceName,
28
+ visitContext,
29
+ }) => {
18
30
  const { t } = useTranslation();
19
- const config = useConfig<FormEntryConfigSchema>();
31
+ const config = useConfig<ConfigObject>();
20
32
  const isOnline = useConnectivity();
21
33
  const {
22
34
  data: forms,
35
+ allForms,
23
36
  error,
24
37
  mutateForms,
25
- } = useForms(patient.id, visitContext?.uuid, undefined, undefined, !isOnline, config.orderBy);
38
+ } = useForms(patientUuid, visitContext?.uuid, undefined, undefined, !isOnline, config.orderBy);
39
+
40
+ const htmlFormEntryForms = useMemo(() => {
41
+ return mapFormsToHtmlFormEntryForms(allForms, config.htmlFormEntryForms);
42
+ }, [config.htmlFormEntryForms, allForms]);
43
+
44
+ const handleFormOpen = useCallback(
45
+ (form: Form, encounterUuid: string) => {
46
+ launchFormEntryOrHtmlForms(
47
+ htmlFormEntryForms,
48
+ patientUuid,
49
+ form,
50
+ visitContext?.uuid,
51
+ encounterUuid,
52
+ visitContext?.visitType.uuid,
53
+ visitContext?.startDatetime,
54
+ visitContext?.stopDatetime,
55
+ mutateForms,
56
+ clinicalFormsWorkspaceName,
57
+ formEntryWorkspaceName,
58
+ htmlFormEntryWorkspaceName,
59
+ );
60
+ },
61
+ [
62
+ visitContext,
63
+ htmlFormEntryForms,
64
+ mutateForms,
65
+ patientUuid,
66
+ clinicalFormsWorkspaceName,
67
+ formEntryWorkspaceName,
68
+ htmlFormEntryWorkspaceName,
69
+ ],
70
+ );
26
71
 
27
72
  const sections = useMemo(() => {
28
73
  return config.formSections?.map((formSection) => ({
@@ -47,14 +92,14 @@ const FormsDashboard: React.FC<FormsDashbaordProps> = ({ handleFormOpen, patient
47
92
  return (
48
93
  <div className={styles.container}>
49
94
  {sections.length === 0 ? (
50
- <FormsList forms={forms} error={error} handleFormOpen={handleFormOpen} />
95
+ <FormsList completedForms={forms} error={error} handleFormOpen={handleFormOpen} />
51
96
  ) : (
52
97
  sections.map((section) => {
53
98
  return (
54
99
  <FormsList
55
100
  key={`form-section-${section.name}`}
56
101
  sectionName={section.name}
57
- forms={section.availableForms}
102
+ completedForms={section.availableForms}
58
103
  error={error}
59
104
  handleFormOpen={handleFormOpen}
60
105
  />
@@ -1,11 +1,12 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
3
  import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
4
- import { configSchema, type FormEntryConfigSchema } from '../config-schema';
4
+ import { configSchema, type ConfigObject } from '../config-schema';
5
+ import { mockCurrentVisit } from '__mocks__';
5
6
  import FormsDashboard from './forms-dashboard.component';
6
7
  import { mockPatient } from 'tools';
7
8
 
8
- const mockUseConfig = jest.mocked(useConfig<FormEntryConfigSchema>);
9
+ const mockUseConfig = jest.mocked(useConfig<ConfigObject>);
9
10
 
10
11
  jest.mock('../hooks/use-forms', () => ({
11
12
  useForms: jest.fn().mockReturnValueOnce({
@@ -20,7 +21,18 @@ mockUseConfig.mockReturnValue({ ...getDefaultsFromConfigSchema(configSchema), ht
20
21
 
21
22
  describe('FormsDashboard', () => {
22
23
  test('renders an empty state if there are no forms persisted on the server', async () => {
23
- render(<FormsDashboard handleFormOpen={jest.fn()} patient={mockPatient} visitContext={null} />);
24
+ render(
25
+ <FormsDashboard
26
+ promptBeforeClosing={jest.fn()}
27
+ closeWorkspace={jest.fn()}
28
+ closeWorkspaceWithSavedChanges={jest.fn()}
29
+ patientUuid={mockPatient.id}
30
+ patient={mockPatient}
31
+ setTitle={jest.fn()}
32
+ visitContext={mockCurrentVisit}
33
+ mutateVisitContext={null}
34
+ />,
35
+ );
24
36
 
25
37
  expect(screen.getByText(/there are no forms to display/i)).toBeInTheDocument();
26
38
  });
@@ -1,39 +1,16 @@
1
- import React, { useCallback } from 'react';
2
- import { useTranslation } from 'react-i18next';
1
+ import React from 'react';
3
2
  import FormsDashboard from './forms-dashboard.component';
4
3
  import styles from './forms-dashboard-workspace.scss';
5
- import { type Form, type PatientWorkspace2DefinitionProps } from '@openmrs/esm-patient-common-lib';
6
- import { ExtensionSlot, Workspace2 } from '@openmrs/esm-framework';
4
+ import { type DefaultPatientWorkspaceProps } from '@openmrs/esm-patient-common-lib';
5
+ import { ExtensionSlot } from '@openmrs/esm-framework';
7
6
 
8
- /**
9
- * This workspace lists a table of available forms. When clicking on a row, it launches
10
- * either the form-entry workspace or the html-form-entry workspace.
11
- *
12
- * This workspace should only be used within the patient chart
13
- */
14
- const FormsDashboardWorkspace: React.FC<PatientWorkspace2DefinitionProps<{}, {}>> = ({
15
- launchChildWorkspace,
16
- groupProps: { patient, patientUuid, visitContext },
17
- }) => {
18
- const { t } = useTranslation();
19
- const handleFormOpen = useCallback(
20
- (form: Form, encounterUuid: string) => {
21
- launchChildWorkspace('patient-form-entry-workspace', {
22
- form,
23
- encounterUuid,
24
- });
25
- },
26
- [launchChildWorkspace],
27
- );
7
+ export default function FormsWorkspace(props: DefaultPatientWorkspaceProps) {
8
+ const { patientUuid } = props;
28
9
 
29
10
  return (
30
- <Workspace2 title={t('clinicalForms', 'Clinical forms')} hasUnsavedChanges={false}>
31
- <div className={styles.container}>
32
- <ExtensionSlot name="visit-context-header-slot" state={{ patientUuid }} />
33
- <FormsDashboard {...{ patient, visitContext, handleFormOpen }} />
34
- </div>
35
- </Workspace2>
11
+ <div className={styles.container}>
12
+ <ExtensionSlot name="visit-context-header-slot" state={{ patientUuid }} />
13
+ <FormsDashboard {...props} />
14
+ </div>
36
15
  );
37
- };
38
-
39
- export default FormsDashboardWorkspace;
16
+ }
@@ -9,7 +9,7 @@ import FormsTable from './forms-table.component';
9
9
  import styles from './forms-list.scss';
10
10
 
11
11
  export type FormsListProps = {
12
- forms?: Array<CompletedFormInfo>;
12
+ completedForms?: Array<CompletedFormInfo>;
13
13
  error?: any;
14
14
  sectionName?: string;
15
15
  handleFormOpen: (form: Form, encounterUuid: string) => void;
@@ -20,23 +20,32 @@ export type FormsListProps = {
20
20
  * t('forms', 'Forms')
21
21
  */
22
22
 
23
- const FormsList: React.FC<FormsListProps> = ({ forms, error, sectionName, handleFormOpen }) => {
23
+ const FormsList: React.FC<FormsListProps> = ({ completedForms, error, sectionName = 'forms', handleFormOpen }) => {
24
24
  const { t } = useTranslation();
25
25
  const [searchTerm, setSearchTerm] = useState('');
26
26
  const isTablet = useLayoutType() === 'tablet';
27
+ const [locale, setLocale] = useState(window.i18next.language ?? navigator.language);
28
+
29
+ useEffect(() => {
30
+ if (window.i18next?.on) {
31
+ const languageChanged = (lng: string) => setLocale(lng);
32
+ window.i18next.on('languageChanged', languageChanged);
33
+ return () => window.i18next.off('languageChanged', languageChanged);
34
+ }
35
+ }, []);
27
36
 
28
37
  const handleSearch = useMemo(() => debounce((searchTerm) => setSearchTerm(searchTerm), 300), []);
29
38
 
30
39
  const filteredForms = useMemo(() => {
31
40
  if (!searchTerm) {
32
- return forms;
41
+ return completedForms;
33
42
  }
34
43
 
35
44
  return fuzzy
36
- .filter(searchTerm, forms, { extract: (formInfo) => formInfo.form.display ?? formInfo.form.name })
45
+ .filter(searchTerm, completedForms, { extract: (formInfo) => formInfo.form.display ?? formInfo.form.name })
37
46
  .sort((r1, r2) => r1.score - r2.score)
38
47
  .map((result) => result.original);
39
- }, [forms, searchTerm]);
48
+ }, [completedForms, searchTerm]);
40
49
 
41
50
  const tableHeaders = useMemo(() => {
42
51
  return [
@@ -66,30 +75,42 @@ const FormsList: React.FC<FormsListProps> = ({ forms, error, sectionName, handle
66
75
  [filteredForms],
67
76
  );
68
77
 
69
- if (!forms && !error) {
78
+ if (!completedForms && !error) {
70
79
  return <DataTableSkeleton role="progressbar" />;
71
80
  }
72
81
 
73
- if (forms?.length === 0) {
82
+ if (completedForms?.length === 0) {
74
83
  return <></>;
75
84
  }
76
85
 
77
- return (
78
- <ResponsiveWrapper>
79
- {sectionName && (
86
+ if (sectionName === 'forms') {
87
+ return (
88
+ <ResponsiveWrapper>
89
+ <FormsTable
90
+ tableHeaders={tableHeaders}
91
+ tableRows={tableRows}
92
+ isTablet={isTablet}
93
+ handleSearch={handleSearch}
94
+ handleFormOpen={handleFormOpen}
95
+ />
96
+ </ResponsiveWrapper>
97
+ );
98
+ } else {
99
+ return (
100
+ <ResponsiveWrapper>
80
101
  <div className={isTablet ? styles.tabletHeading : styles.desktopHeading}>
81
102
  <h4>{t(sectionName)}</h4>
82
103
  </div>
83
- )}
84
- <FormsTable
85
- tableHeaders={tableHeaders}
86
- tableRows={tableRows}
87
- isTablet={isTablet}
88
- handleSearch={handleSearch}
89
- handleFormOpen={handleFormOpen}
90
- />
91
- </ResponsiveWrapper>
92
- );
104
+ <FormsTable
105
+ tableHeaders={tableHeaders}
106
+ tableRows={tableRows}
107
+ isTablet={isTablet}
108
+ handleSearch={handleSearch}
109
+ handleFormOpen={handleFormOpen}
110
+ />
111
+ </ResponsiveWrapper>
112
+ );
113
+ }
93
114
  };
94
115
 
95
116
  export default FormsList;
@@ -7,7 +7,7 @@ import FormsList, { type FormsListProps } from './forms-list.component';
7
7
  jest.mock('lodash-es/debounce', () => jest.fn((fn) => fn));
8
8
 
9
9
  const defaultProps: FormsListProps & { reset: () => void } = {
10
- forms: [],
10
+ completedForms: [],
11
11
  handleFormOpen: jest.fn(),
12
12
  reset() {
13
13
  this.completedForms = [];
@@ -25,7 +25,7 @@ beforeEach(async () => {
25
25
  it('renders a list of forms fetched from the server', async () => {
26
26
  const user = userEvent.setup();
27
27
 
28
- renderFormsList({ forms: forms.map((form) => ({ form, associatedEncounters: [] })) });
28
+ renderFormsList({ completedForms: forms.map((form) => ({ form, associatedEncounters: [] })) });
29
29
 
30
30
  const searchbox = screen.getByRole('searchbox');
31
31
  expect(searchbox).toBeInTheDocument();
@@ -71,7 +71,7 @@ const FormsTable = ({ tableHeaders, tableRows, isTablet, handleSearch, handleFor
71
71
  <Link
72
72
  style={{ cursor: 'pointer' }}
73
73
  onClick={() => {
74
- handleFormOpen(tableRows[i].form, null);
74
+ handleFormOpen(tableRows[i].form, '');
75
75
  }}
76
76
  role="presentation"
77
77
  className={styles.formName}