@openmrs/esm-form-builder-app 2.2.2-pre.651 → 2.2.2-pre.658

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.
@@ -1,19 +1,32 @@
1
1
  import React from 'react';
2
- import { screen, waitFor } from '@testing-library/react';
2
+ import { screen } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
- import { navigate, openmrsFetch, usePagination } from '@openmrs/esm-framework';
4
+ import { type FetchResponse, navigate, openmrsFetch, usePagination } from '@openmrs/esm-framework';
5
5
  import { renderWithSwr, waitForLoadingToFinish } from '../../test-helpers';
6
6
  import { deleteForm } from '../../forms.resource';
7
7
  import Dashboard from './dashboard.component';
8
8
 
9
- const mockedOpenmrsFetch = openmrsFetch as jest.Mock;
10
- const mockedDeleteForm = deleteForm as jest.Mock;
11
- global.window.URL.createObjectURL = jest.fn();
12
-
13
- jest.mock('../../forms.resource', () => ({
14
- deleteForm: jest.fn(),
15
- }));
16
- const mockUsePagination = usePagination as jest.Mock;
9
+ type OpenmrsFetchResponse = Promise<
10
+ FetchResponse<{
11
+ results: unknown[];
12
+ }>
13
+ >;
14
+
15
+ type PaginationData = {
16
+ currentPage: number;
17
+ goTo: (page: number) => void;
18
+ results: unknown[];
19
+ totalPages: number;
20
+ paginated: boolean;
21
+ showNextButton: boolean;
22
+ showPreviousButton: boolean;
23
+ goToNext: () => void;
24
+ goToPrevious: () => void;
25
+ };
26
+
27
+ const mockedOpenmrsFetch = jest.mocked(openmrsFetch);
28
+ const mockedDeleteForm = jest.mocked(deleteForm);
29
+ const mockUsePagination = jest.mocked(usePagination);
17
30
 
18
31
  const formsResponse = [
19
32
  {
@@ -37,6 +50,10 @@ const formsResponse = [
37
50
  },
38
51
  ];
39
52
 
53
+ jest.mock('../../forms.resource', () => ({
54
+ deleteForm: jest.fn(),
55
+ }));
56
+
40
57
  jest.mock('@openmrs/esm-framework', () => {
41
58
  const originalModule = jest.requireActual('@openmrs/esm-framework');
42
59
 
@@ -51,15 +68,17 @@ jest.mock('@openmrs/esm-framework', () => {
51
68
  };
52
69
  });
53
70
 
71
+ global.window.URL.createObjectURL = jest.fn();
72
+
54
73
  describe('Dashboard', () => {
55
74
  it('renders an empty state view if no forms are available', async () => {
56
- mockedOpenmrsFetch.mockReturnValueOnce({ data: { results: [] } });
75
+ mockedOpenmrsFetch.mockReturnValueOnce({ data: { results: [] } } as unknown as OpenmrsFetchResponse);
57
76
 
58
77
  renderDashboard();
59
78
 
60
79
  await waitForLoadingToFinish();
61
80
 
62
- expect(screen.getByRole('heading', { name: /form builder/i })).toBeInTheDocument();
81
+ expect(screen.getByText(/form builder/i)).toBeInTheDocument();
63
82
  expect(screen.getByRole('heading', { name: /forms/i })).toBeInTheDocument();
64
83
  expect(screen.getByTitle(/empty data illustration/i)).toBeInTheDocument();
65
84
  expect(screen.getByText(/there are no forms to display/i)).toBeInTheDocument();
@@ -73,7 +92,7 @@ describe('Dashboard', () => {
73
92
  data: {
74
93
  results: formsResponse,
75
94
  },
76
- });
95
+ } as unknown as OpenmrsFetchResponse);
77
96
 
78
97
  renderDashboard();
79
98
 
@@ -81,17 +100,20 @@ describe('Dashboard', () => {
81
100
 
82
101
  const searchbox = screen.getByRole('searchbox') as HTMLInputElement;
83
102
 
84
- await waitFor(() => user.type(searchbox, 'COVID'));
103
+ await user.type(searchbox, 'COVID');
85
104
 
86
105
  expect(searchbox.value).toBe('COVID');
87
106
 
88
- mockUsePagination.mockImplementation(() => ({
89
- currentPage: 1,
90
- goTo: () => {},
91
- results: formsResponse.filter((form) => form.name === searchbox.value),
92
- }));
107
+ mockUsePagination.mockImplementation(
108
+ () =>
109
+ ({
110
+ currentPage: 1,
111
+ goTo: () => {},
112
+ results: formsResponse.filter((form) => form.name === searchbox.value),
113
+ }) as unknown as PaginationData,
114
+ );
93
115
 
94
- await waitFor(() => expect(screen.queryByText(/Test Form 1/i)).not.toBeInTheDocument());
116
+ await expect(screen.queryByText(/Test Form 1/i)).not.toBeInTheDocument();
95
117
  expect(screen.getByText(/no matching forms to display/i)).toBeInTheDocument();
96
118
  });
97
119
 
@@ -102,7 +124,7 @@ describe('Dashboard', () => {
102
124
  data: {
103
125
  results: formsResponse,
104
126
  },
105
- });
127
+ } as unknown as OpenmrsFetchResponse);
106
128
 
107
129
  renderDashboard();
108
130
 
@@ -112,14 +134,17 @@ describe('Dashboard', () => {
112
134
  name: /filter by/i,
113
135
  });
114
136
 
115
- await waitFor(() => user.click(publishStatusFilter));
116
- await waitFor(() => user.click(screen.getByRole('option', { name: /unpublished/i })));
137
+ await user.click(publishStatusFilter);
138
+ await user.click(screen.getByRole('option', { name: /unpublished/i }));
117
139
 
118
- mockUsePagination.mockImplementation(() => ({
119
- currentPage: 1,
120
- goTo: () => {},
121
- results: formsResponse.filter((form) => !form.published),
122
- }));
140
+ mockUsePagination.mockImplementation(
141
+ () =>
142
+ ({
143
+ currentPage: 1,
144
+ goTo: () => {},
145
+ results: formsResponse.filter((form) => !form.published),
146
+ }) as unknown as PaginationData,
147
+ );
123
148
 
124
149
  expect(screen.queryByText(/Test Form 1/i)).not.toBeInTheDocument();
125
150
  expect(screen.getByText(/no matching forms to display/i)).toBeInTheDocument();
@@ -130,19 +155,22 @@ describe('Dashboard', () => {
130
155
  data: {
131
156
  results: formsResponse,
132
157
  },
133
- });
158
+ } as unknown as OpenmrsFetchResponse);
134
159
 
135
- mockUsePagination.mockImplementation(() => ({
136
- currentPage: 1,
137
- goTo: () => {},
138
- results: formsResponse,
139
- }));
160
+ mockUsePagination.mockImplementation(
161
+ () =>
162
+ ({
163
+ currentPage: 1,
164
+ goTo: () => {},
165
+ results: formsResponse,
166
+ }) as unknown as PaginationData,
167
+ );
140
168
 
141
169
  renderDashboard();
142
170
 
143
171
  await waitForLoadingToFinish();
144
172
 
145
- expect(screen.getByRole('heading', { name: /form builder/i })).toBeInTheDocument();
173
+ expect(screen.getByText(/form builder/i)).toBeInTheDocument();
146
174
  expect(screen.getByRole('combobox', { name: /filter by/i })).toBeInTheDocument();
147
175
  expect(screen.getByRole('button', { name: /create a new form/i })).toBeInTheDocument();
148
176
  expect(screen.getByRole('button', { name: /edit schema/i })).toBeInTheDocument();
@@ -159,13 +187,16 @@ describe('Dashboard', () => {
159
187
  data: {
160
188
  results: formsResponse,
161
189
  },
162
- });
190
+ } as unknown as OpenmrsFetchResponse);
163
191
 
164
- mockUsePagination.mockImplementation(() => ({
165
- currentPage: 1,
166
- goTo: () => {},
167
- results: formsResponse,
168
- }));
192
+ mockUsePagination.mockImplementation(
193
+ () =>
194
+ ({
195
+ currentPage: 1,
196
+ goTo: () => {},
197
+ results: formsResponse,
198
+ }) as unknown as PaginationData,
199
+ );
169
200
 
170
201
  renderDashboard();
171
202
 
@@ -187,13 +218,16 @@ describe('Dashboard', () => {
187
218
  data: {
188
219
  results: formsResponse,
189
220
  },
190
- });
221
+ } as unknown as OpenmrsFetchResponse);
191
222
 
192
- mockUsePagination.mockImplementation(() => ({
193
- currentPage: 1,
194
- goTo: () => {},
195
- results: formsResponse,
196
- }));
223
+ mockUsePagination.mockImplementation(
224
+ () =>
225
+ ({
226
+ currentPage: 1,
227
+ goTo: () => {},
228
+ results: formsResponse,
229
+ }) as unknown as PaginationData,
230
+ );
197
231
 
198
232
  renderDashboard();
199
233
 
@@ -210,13 +244,16 @@ describe('Dashboard', () => {
210
244
  data: {
211
245
  results: formsResponse,
212
246
  },
213
- });
247
+ } as unknown as OpenmrsFetchResponse);
214
248
 
215
- mockUsePagination.mockImplementation(() => ({
216
- currentPage: 1,
217
- goTo: () => {},
218
- results: formsResponse,
219
- }));
249
+ mockUsePagination.mockImplementation(
250
+ () =>
251
+ ({
252
+ currentPage: 1,
253
+ goTo: () => {},
254
+ results: formsResponse,
255
+ }) as unknown as PaginationData,
256
+ );
220
257
 
221
258
  renderDashboard();
222
259
 
@@ -226,7 +263,7 @@ describe('Dashboard', () => {
226
263
  name: /edit schema/i,
227
264
  });
228
265
 
229
- await waitFor(() => user.click(editSchemaButton));
266
+ await user.click(editSchemaButton);
230
267
 
231
268
  expect(navigate).toHaveBeenCalledWith({
232
269
  to: expect.stringMatching(/form\-builder\/edit/),
@@ -240,13 +277,17 @@ describe('Dashboard', () => {
240
277
  data: {
241
278
  results: formsResponse,
242
279
  },
243
- });
280
+ } as unknown as OpenmrsFetchResponse);
281
+
282
+ mockUsePagination.mockImplementation(
283
+ () =>
284
+ ({
285
+ currentPage: 1,
286
+ goTo: () => {},
287
+ results: formsResponse,
288
+ }) as unknown as PaginationData,
289
+ );
244
290
 
245
- mockUsePagination.mockImplementation(() => ({
246
- currentPage: 1,
247
- goTo: () => {},
248
- results: formsResponse,
249
- }));
250
291
  renderDashboard();
251
292
 
252
293
  await waitForLoadingToFinish();
@@ -255,7 +296,7 @@ describe('Dashboard', () => {
255
296
  name: /download schema/i,
256
297
  });
257
298
 
258
- await waitFor(() => user.click(downloadSchemaButton));
299
+ await user.click(downloadSchemaButton);
259
300
 
260
301
  expect(window.URL.createObjectURL).toHaveBeenCalled();
261
302
  });
@@ -267,14 +308,18 @@ describe('Dashboard', () => {
267
308
  data: {
268
309
  results: formsResponse,
269
310
  },
270
- });
271
- mockedDeleteForm.mockResolvedValue({});
311
+ } as unknown as OpenmrsFetchResponse);
272
312
 
273
- mockUsePagination.mockImplementation(() => ({
274
- currentPage: 1,
275
- goTo: () => {},
276
- results: formsResponse,
277
- }));
313
+ mockedDeleteForm.mockResolvedValue({} as FetchResponse<Record<string, never>>);
314
+
315
+ mockUsePagination.mockImplementation(
316
+ () =>
317
+ ({
318
+ currentPage: 1,
319
+ goTo: () => {},
320
+ results: formsResponse,
321
+ }) as unknown as PaginationData,
322
+ );
278
323
 
279
324
  renderDashboard();
280
325
 
@@ -283,7 +328,7 @@ describe('Dashboard', () => {
283
328
  const deleteButton = screen.getByRole('button', { name: /delete schema/i });
284
329
  expect(deleteButton).toBeInTheDocument();
285
330
 
286
- await waitFor(() => user.click(deleteButton));
331
+ await user.click(deleteButton);
287
332
 
288
333
  const modal = screen.getByRole('presentation');
289
334
  expect(modal).toBeInTheDocument();
@@ -292,7 +337,7 @@ describe('Dashboard', () => {
292
337
  expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
293
338
  expect(screen.getByRole('button', { name: /danger delete/i })).toBeInTheDocument();
294
339
 
295
- await waitFor(() => user.click(screen.getByRole('button', { name: /danger delete/i })));
340
+ await user.click(screen.getByRole('button', { name: /danger delete/i }));
296
341
  });
297
342
  });
298
343
 
@@ -17,10 +17,9 @@ import {
17
17
  TabPanels,
18
18
  TabPanel,
19
19
  } from '@carbon/react';
20
- import { Download } from '@carbon/react/icons';
20
+ import { ArrowLeft, Download } from '@carbon/react/icons';
21
21
  import { useParams } from 'react-router-dom';
22
22
  import { useTranslation } from 'react-i18next';
23
- import { ExtensionSlot } from '@openmrs/esm-framework';
24
23
  import type { OHRIFormSchema } from '@openmrs/openmrs-form-engine-lib';
25
24
  import type { Schema } from '../../types';
26
25
  import { useClobdata } from '../../hooks/useClobdata';
@@ -28,9 +27,11 @@ import { useForm } from '../../hooks/useForm';
28
27
  import ActionButtons from '../action-buttons/action-buttons.component';
29
28
  import AuditDetails from '../audit-details/audit-details.component';
30
29
  import FormRenderer from '../form-renderer/form-renderer.component';
30
+ import Header from '../header/header.component';
31
31
  import InteractiveBuilder from '../interactive-builder/interactive-builder.component';
32
32
  import SchemaEditor from '../schema-editor/schema-editor.component';
33
33
  import styles from './form-editor.scss';
34
+ import { ConfigurableLink } from '@openmrs/esm-framework';
34
35
 
35
36
  interface ErrorProps {
36
37
  error: Error;
@@ -240,9 +241,8 @@ const FormEditor: React.FC = () => {
240
241
 
241
242
  return (
242
243
  <>
243
- <div className={styles.breadcrumbsContainer}>
244
- <ExtensionSlot name="breadcrumbs-slot" />
245
- </div>
244
+ <Header title={t('schemaEditor', 'Schema editor')} />
245
+ <BackButton />
246
246
  <div className={styles.container}>
247
247
  {showDraftSchemaModal && <DraftSchemaModal />}
248
248
  <Grid className={styles.grid}>
@@ -334,4 +334,22 @@ const FormEditor: React.FC = () => {
334
334
  );
335
335
  };
336
336
 
337
+ function BackButton() {
338
+ const { t } = useTranslation();
339
+
340
+ return (
341
+ <div className={styles.backButton}>
342
+ <ConfigurableLink to={window.getOpenmrsSpaBase() + 'form-builder'}>
343
+ <Button
344
+ kind="ghost"
345
+ renderIcon={(props) => <ArrowLeft size={24} {...props} />}
346
+ iconDescription="Return to dashboard"
347
+ >
348
+ <span>{t('backToDashboard', 'Back to dashboard')}</span>
349
+ </Button>
350
+ </ConfigurableLink>
351
+ </div>
352
+ );
353
+ }
354
+
337
355
  export default FormEditor;
@@ -8,12 +8,6 @@
8
8
  flex-direction: column;
9
9
  }
10
10
 
11
- .breadcrumbsContainer {
12
- nav {
13
- background-color: colors.$white-0;
14
- }
15
- }
16
-
17
11
  .grid {
18
12
  margin-left: 0;
19
13
  margin-right: 0;
@@ -72,3 +66,30 @@
72
66
  padding: 0.75rem;
73
67
  }
74
68
 
69
+ .backButton {
70
+ margin-left: layout.$spacing-05;
71
+ padding: layout.$spacing-03 0;
72
+ max-width: fit-content;
73
+
74
+ a {
75
+ text-decoration: none;
76
+ }
77
+
78
+ button {
79
+ display: flex;
80
+ padding-left: 0 !important;
81
+
82
+ svg {
83
+ order: 1;
84
+ margin: 0 layout.$spacing-03;
85
+ }
86
+
87
+ span {
88
+ order: 2;
89
+ }
90
+ }
91
+ }
92
+
93
+ button {
94
+ padding-block-start: 0.5rem;
95
+ }
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Calendar, Location, UserFollow } from '@carbon/react/icons';
4
+ import { formatDate, useSession } from '@openmrs/esm-framework';
5
+ import Illustration from './illo.component';
6
+ import styles from './header.scss';
7
+
8
+ interface HeaderProps {
9
+ title: string;
10
+ }
11
+
12
+ const Header: React.FC<HeaderProps> = ({ title }) => {
13
+ const { t } = useTranslation();
14
+ const session = useSession();
15
+ const location = session?.sessionLocation?.display;
16
+
17
+ return (
18
+ <div className={styles.header}>
19
+ <div className={styles['left-justified-items']}>
20
+ <Illustration />
21
+ <div className={styles['page-labels']}>
22
+ <p>{t('formBuilder', 'Form builder')}</p>
23
+ <p className={styles['page-name']}>{title}</p>
24
+ </div>
25
+ </div>
26
+ <div className={styles['right-justified-items']}>
27
+ <div className={styles.userContainer}>
28
+ <p>{session?.user?.person?.display}</p>
29
+ <UserFollow size={16} className={styles.userIcon} />
30
+ </div>
31
+ <div className={styles['date-and-location']}>
32
+ <Location size={16} />
33
+ <span className={styles.value}>{location}</span>
34
+ <span className={styles.middot}>&middot;</span>
35
+ <Calendar size={16} />
36
+ <span className={styles.value}>{formatDate(new Date(), { mode: 'standard' })}</span>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ );
41
+ };
42
+
43
+ export default Header;
@@ -0,0 +1,67 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @import '~@openmrs/esm-styleguide/src/vars';
4
+
5
+ .header {
6
+ @include type.type-style('body-compact-02');
7
+ color: $text-02;
8
+ height: layout.$spacing-12;
9
+ background-color: $ui-02;
10
+ border-bottom: 1px solid $ui-03;
11
+ display: flex;
12
+ justify-content: space-between;
13
+ padding: layout.$spacing-05;
14
+ }
15
+
16
+ .left-justified-items {
17
+ display: flex;
18
+ flex-direction: row;
19
+ align-items: center;
20
+ cursor: pointer;
21
+ align-items: center;
22
+ }
23
+
24
+ .right-justified-items {
25
+ @include type.type-style('body-compact-02');
26
+ color: $text-02;
27
+ display: flex;
28
+ flex-direction: column;
29
+ justify-content: space-between;
30
+ }
31
+
32
+ .page-name {
33
+ @include type.type-style('heading-04');
34
+ }
35
+
36
+ .page-labels {
37
+ margin: layout.$spacing-03;
38
+
39
+ p:first-of-type {
40
+ margin-bottom: layout.$spacing-02;
41
+ }
42
+ }
43
+
44
+ .date-and-location {
45
+ display: flex;
46
+ justify-content: flex-end;
47
+ align-items: center;
48
+ }
49
+
50
+ .userContainer {
51
+ display: flex;
52
+ align-items: center;
53
+ justify-content: flex-end;
54
+ gap: layout.$spacing-05;
55
+ }
56
+
57
+ .value {
58
+ margin-left: layout.$spacing-02;
59
+ }
60
+
61
+ .middot {
62
+ margin: 0 layout.$spacing-03;
63
+ }
64
+
65
+ .view {
66
+ @include type.type-style('label-01');
67
+ }
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+
3
+ const Illustration: React.FC = () => {
4
+ return (
5
+ <svg
6
+ height="64"
7
+ width="64"
8
+ viewBox="0 0 32 32"
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ xmlSpace="preserve"
11
+ fillRule="evenodd"
12
+ clipRule="evenodd"
13
+ strokeLinejoin="round"
14
+ strokeMiterlimit="2"
15
+ >
16
+ <path
17
+ d="M27 31.36H8a.36.36 0 0 1-.36-.36v-1.64H6a.36.36 0 0 1-.36-.36v-1.64H5a.36.36 0 0 1-.36-.36V3A.36.36 0 0 1 5 2.64h4.64V2a.36.36 0 0 1 .36-.36h1.64V1A.36.36 0 0 1 12 .64h4a.36.36 0 0 1 .36.36v.64H18a.36.36 0 0 1 .36.36v.64H23a.36.36 0 0 1 .36.36v1.64H25a.36.36 0 0 1 .36.36v1.64H27a.36.36 0 0 1 .36.36v24a.36.36 0 0 1-.36.36Z"
18
+ fill="#d2e5e5"
19
+ />
20
+ <path d="M8.36 30.64h18.28V7.36h-1.28V29a.36.36 0 0 1-.36.36H8.36v1.28Z" fill="#8abab8" />
21
+ <path
22
+ d="M5.36 26.64h17.28V3.36h-4.28V4a.36.36 0 0 1-.36.36h-8A.36.36 0 0 1 9.64 4v-.64H5.36v23.28Z"
23
+ fill="#8abab8"
24
+ />
25
+ <path fill="#fff" d="M7.5 12.64h13v.72h-13zM7.5 8.64h13v.72h-13zM7.5 20.64h13v.72h-13zM7.5 16.64h13v.72h-13z" />
26
+ <path
27
+ d="M10.36 3.64h7.28V2.36H16a.36.36 0 0 1-.36-.36v-.64h-3.28V2a.36.36 0 0 1-.36.36h-1.64v1.28ZM6.36 28.64h18.28V5.36h-1.28V27a.36.36 0 0 1-.36.36H6.36v1.28Z"
28
+ fill="#8abab8"
29
+ />
30
+ </svg>
31
+ );
32
+ };
33
+
34
+ export default Illustration;
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { defineConfigSchema, getAsyncLifecycle, registerBreadcrumbs } from '@openmrs/esm-framework';
1
+ import { defineConfigSchema, getAsyncLifecycle } from '@openmrs/esm-framework';
2
2
  import { configSchema } from './config-schema';
3
3
 
4
4
  const moduleName = '@openmrs/esm-form-builder-app';
@@ -19,22 +19,4 @@ export const systemAdministrationFormBuilderCardLink = getAsyncLifecycle(
19
19
 
20
20
  export function startupApp() {
21
21
  defineConfigSchema(moduleName, configSchema);
22
-
23
- registerBreadcrumbs([
24
- {
25
- path: `${window.spaBase}/form-builder`,
26
- title: 'Form Builder',
27
- parent: `${window.spaBase}/home`,
28
- },
29
- {
30
- path: `${window.spaBase}/form-builder/new`,
31
- title: 'Form Editor',
32
- parent: `${window.spaBase}/form-builder`,
33
- },
34
- {
35
- path: `${window.spaBase}/form-builder/edit/:uuid`,
36
- title: 'Form Editor',
37
- parent: `${window.spaBase}/form-builder`,
38
- },
39
- ]);
40
22
  }