@openmrs/esm-form-builder-app 3.4.2-pre.3421 → 3.4.2-pre.3423

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 (40) hide show
  1. package/dist/1379.js +1 -1
  2. package/dist/1379.js.map +1 -1
  3. package/dist/1637.js +1 -1
  4. package/dist/1719.js +1 -1
  5. package/dist/2324.js +1 -1
  6. package/dist/2324.js.map +1 -1
  7. package/dist/3074.js +1 -1
  8. package/dist/3896.js +1 -1
  9. package/dist/4192.js +1 -1
  10. package/dist/4300.js +1 -1
  11. package/dist/5328.js +1 -1
  12. package/dist/7140.js +1 -1
  13. package/dist/7146.js +1 -1
  14. package/dist/7252.js +1 -1
  15. package/dist/7252.js.map +1 -1
  16. package/dist/8091.js +1 -1
  17. package/dist/8295.js +1 -1
  18. package/dist/8776.js +1 -1
  19. package/dist/{9441.js → 8935.js} +2 -2
  20. package/dist/8935.js.map +1 -0
  21. package/dist/9544.js +1 -1
  22. package/dist/9672.js +1 -1
  23. package/dist/9852.js +1 -1
  24. package/dist/main.js +1 -1
  25. package/dist/openmrs-esm-form-builder-app.js +1 -1
  26. package/dist/openmrs-esm-form-builder-app.js.buildmanifest.json +47 -47
  27. package/dist/routes.json +1 -1
  28. package/package.json +1 -1
  29. package/src/components/action-buttons/action-buttons.component.tsx +1 -1
  30. package/src/components/audit-details/audit-details.component.tsx +41 -7
  31. package/src/components/dashboard/dashboard.component.tsx +60 -27
  32. package/src/components/dashboard/dashboard.scss +6 -0
  33. package/src/components/form-editor/form-editor.component.tsx +39 -8
  34. package/src/components/form-editor/form-editor.scss +15 -1
  35. package/src/components/interactive-builder/interactive-builder.component.tsx +6 -4
  36. package/src/components/interactive-builder/modals/add-form-reference/add-form-reference.modal.tsx +2 -2
  37. package/src/components/interactive-builder/modals/question/question-form/common/concept-search/concept-search.component.tsx +1 -1
  38. package/src/components/translation-builder/translation-builder.component.tsx +2 -2
  39. package/dist/9441.js.map +0 -1
  40. /package/dist/{9441.js.LICENSE.txt → 8935.js.LICENSE.txt} +0 -0
@@ -66,19 +66,20 @@ interface FormsListProps {
66
66
  t: TFunction;
67
67
  }
68
68
 
69
- function CustomTag({ condition }: { condition?: boolean }) {
69
+ function CustomTag({ condition, invertTone = false }: { condition?: boolean; invertTone?: boolean }) {
70
70
  const { t } = useTranslation();
71
+ const positiveTrueType = invertTone ? 'red' : 'green';
71
72
 
72
73
  if (condition) {
73
74
  return (
74
- <Tag type="green" size="md" title="Clear Filter" data-testid="yes-tag">
75
+ <Tag type={positiveTrueType} size="md" title="Clear Filter" data-testid="yes-tag">
75
76
  {t('yes', 'Yes')}
76
77
  </Tag>
77
78
  );
78
79
  }
79
80
 
80
81
  return (
81
- <Tag type="red" size="md" title="Clear Filter" data-testid="no-tag">
82
+ <Tag type="cool-gray" size="md" title="Clear Filter" data-testid="no-tag">
82
83
  {t('no', 'No')}
83
84
  </Tag>
84
85
  );
@@ -237,7 +238,7 @@ function ActionButtons({ form, mutate, responsiveSize, t }: ActionButtonsProps)
237
238
  };
238
239
 
239
240
  return (
240
- <>
241
+ <div className={styles.actionButtons}>
241
242
  {formResources.length == 0 || !form?.resources[0] ? (
242
243
  <ImportButton />
243
244
  ) : (
@@ -247,7 +248,7 @@ function ActionButtons({ form, mutate, responsiveSize, t }: ActionButtonsProps)
247
248
  </>
248
249
  )}
249
250
  {form.retired ? <RestoreButton /> : <DeleteButton />}
250
- </>
251
+ </div>
251
252
  );
252
253
  }
253
254
 
@@ -286,22 +287,27 @@ function FormsList({ forms, isValidating, mutate, t }: FormsListProps) {
286
287
  {
287
288
  header: t('name', 'Name'),
288
289
  key: 'name',
290
+ isSortable: true,
289
291
  },
290
292
  {
291
293
  header: t('version', 'Version'),
292
294
  key: 'version',
295
+ isSortable: true,
293
296
  },
294
297
  {
295
298
  header: t('published', 'Published'),
296
299
  key: 'published',
300
+ isSortable: true,
297
301
  },
298
302
  {
299
303
  header: t('retired', 'Retired'),
300
304
  key: 'retired',
305
+ isSortable: true,
301
306
  },
302
307
  {
303
308
  header: t('schemaActions', 'Schema actions'),
304
309
  key: 'actions',
310
+ isSortable: false,
305
311
  },
306
312
  ];
307
313
 
@@ -318,23 +324,47 @@ function FormsList({ forms, isValidating, mutate, t }: FormsListProps) {
318
324
  const { paginated, goTo, results, currentPage } = usePagination(searchResults, pageSize);
319
325
 
320
326
  const tableRows = results?.map((form: TypedForm) => ({
321
- ...form,
322
327
  id: form?.uuid,
323
- name: (
324
- <ConfigurableLink
325
- className={styles.link}
326
- to={editSchemaUrl}
327
- templateParams={{ formUuid: form?.uuid }}
328
- onMouseEnter={() => void preload(`${restBaseUrl}/form/${form?.uuid}?v=full`, openmrsFetch)}
329
- >
330
- {form.name}
331
- </ConfigurableLink>
332
- ),
333
- published: <CustomTag condition={form.published} />,
334
- retired: <CustomTag condition={form.retired} />,
335
- actions: <ActionButtons form={form} mutate={mutate} responsiveSize={responsiveSize} t={t} />,
328
+ name: form.name,
329
+ version: form.version,
330
+ published: form.published,
331
+ retired: form.retired,
332
+ actions: form.uuid,
336
333
  }));
337
334
 
335
+ const formsByUuid = useMemo(() => new Map(results?.map((form: TypedForm) => [form.uuid, form]) ?? []), [results]);
336
+
337
+ const renderCell = (cell: { id: string; value: unknown; info: { header: string } }) => {
338
+ const form = formsByUuid.get(String(cell.value)) ?? formsByUuid.get(cell.id.split(':')[0]);
339
+ switch (cell.info.header) {
340
+ case 'name': {
341
+ const rowForm = formsByUuid.get(cell.id.split(':')[0]);
342
+ return (
343
+ <ConfigurableLink
344
+ className={styles.link}
345
+ to={editSchemaUrl}
346
+ templateParams={{ formUuid: rowForm?.uuid }}
347
+ onMouseEnter={() => rowForm && void preload(`${restBaseUrl}/form/${rowForm.uuid}?v=full`, openmrsFetch)}
348
+ >
349
+ {String(cell.value)}
350
+ </ConfigurableLink>
351
+ );
352
+ }
353
+ case 'published':
354
+ return <CustomTag condition={Boolean(cell.value)} />;
355
+ case 'retired':
356
+ return <CustomTag condition={Boolean(cell.value)} invertTone />;
357
+ case 'actions': {
358
+ const actionForm = formsByUuid.get(String(cell.value));
359
+ return actionForm ? (
360
+ <ActionButtons form={actionForm} mutate={mutate} responsiveSize={responsiveSize} t={t} />
361
+ ) : null;
362
+ }
363
+ default:
364
+ return String(cell.value);
365
+ }
366
+ };
367
+
338
368
  const handleFilter = ({ selectedItem }: { selectedItem: string }) => setFilter(selectedItem);
339
369
 
340
370
  const handleSearch = useCallback(
@@ -365,7 +395,7 @@ function FormsList({ forms, isValidating, mutate, t }: FormsListProps) {
365
395
  <span>{isValidating ? <InlineLoading /> : null}</span>
366
396
  </div>
367
397
  </div>
368
- <DataTable rows={tableRows} headers={tableHeaders} size={isTablet ? 'lg' : 'sm'} useZebraStyles>
398
+ <DataTable rows={tableRows} headers={tableHeaders} size={isTablet ? 'lg' : 'sm'} useZebraStyles isSortable>
369
399
  {({ rows, headers, getTableProps, getHeaderProps, getRowProps }) => (
370
400
  <>
371
401
  <TableContainer className={styles.tableContainer} data-testid="forms-table">
@@ -381,7 +411,6 @@ function FormsList({ forms, isValidating, mutate, t }: FormsListProps) {
381
411
  <Button
382
412
  kind="primary"
383
413
  iconDescription={t('createNewForm', 'Create a new form')}
384
- renderIcon={() => <Add size={16} />}
385
414
  size={responsiveSize}
386
415
  onClick={() =>
387
416
  navigate({
@@ -389,6 +418,7 @@ function FormsList({ forms, isValidating, mutate, t }: FormsListProps) {
389
418
  })
390
419
  }
391
420
  >
421
+ <Add size={16} style={{ marginRight: '0.5rem' }} aria-hidden />
392
422
  {t('createNewForm', 'Create a new form')}
393
423
  </Button>
394
424
  </TableToolbarContent>
@@ -397,16 +427,19 @@ function FormsList({ forms, isValidating, mutate, t }: FormsListProps) {
397
427
  <Table {...getTableProps()} className={styles.table}>
398
428
  <TableHead>
399
429
  <TableRow>
400
- {headers.map((header) => (
401
- <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
402
- ))}
430
+ {headers.map((header) => {
431
+ const sortable = (header as { isSortable?: boolean }).isSortable;
432
+ return (
433
+ <TableHeader {...getHeaderProps({ header, isSortable: sortable })}>{header.header}</TableHeader>
434
+ );
435
+ })}
403
436
  </TableRow>
404
437
  </TableHead>
405
438
  <TableBody>
406
439
  {rows.map((row) => (
407
- <TableRow key="row.id" {...getRowProps({ row })} data-testid={`form-row-${row.id}`}>
440
+ <TableRow key={row.id} {...getRowProps({ row })} data-testid={`form-row-${row.id}`}>
408
441
  {row.cells.map((cell) => (
409
- <TableCell key={cell.id}>{cell.value}</TableCell>
442
+ <TableCell key={cell.id}>{renderCell(cell)}</TableCell>
410
443
  ))}
411
444
  </TableRow>
412
445
  ))}
@@ -472,7 +505,7 @@ const Dashboard: React.FC = () => {
472
505
  lowContrast
473
506
  title={t(
474
507
  'schemaSaveWarningMessage',
475
- "The dev3 server is ephemeral at best and can't be relied upon to save your schemas permanently. To avoid losing your work, please save your schemas to your local machine. Alternatively, upload your schema to the distro repo to have it persisted across server resets.",
508
+ "This is a demo server and can't be relied upon to save your schemas permanently. To avoid losing your work, please save your schemas to your local machine. Alternatively, upload your schema to the distro repo to have it persisted across server resets.",
476
509
  )}
477
510
  />
478
511
  )}
@@ -132,3 +132,9 @@
132
132
  .warningMessage {
133
133
  margin: layout.$spacing-05 0;
134
134
  }
135
+
136
+ .actionButtons {
137
+ display: flex;
138
+ align-items: center;
139
+ gap: layout.$spacing-03;
140
+ }
@@ -16,8 +16,9 @@ import {
16
16
  TabPanel,
17
17
  TabPanels,
18
18
  Tabs,
19
+ Tag,
19
20
  } from '@carbon/react';
20
- import { ArrowLeft, Maximize, Minimize, Download } from '@carbon/react/icons';
21
+ import { ArrowLeft, Maximize, Minimize, Download, Renew } from '@carbon/react/icons';
21
22
  import { useParams } from 'react-router-dom';
22
23
  import { useTranslation } from 'react-i18next';
23
24
  import { type TFunction } from 'i18next';
@@ -95,6 +96,21 @@ const FormEditorContent: React.FC<TranslationFnProps> = ({ t }) => {
95
96
 
96
97
  const isLoadingFormOrSchema = Boolean(formUuid) && (isLoadingClobdata || isLoadingForm);
97
98
 
99
+ const savedSchemaString = useMemo(() => (clobdata ? JSON.stringify(clobdata, null, 2) : ''), [clobdata]);
100
+ const hasSchemaContent =
101
+ Boolean(stringifiedSchema) && stringifiedSchema !== 'undefined' && stringifiedSchema !== 'null';
102
+ const isDirty = hasSchemaContent && stringifiedSchema !== savedSchemaString;
103
+
104
+ useEffect(() => {
105
+ if (!isDirty) return;
106
+ const handler = (event: BeforeUnloadEvent) => {
107
+ event.preventDefault();
108
+ event.returnValue = '';
109
+ };
110
+ window.addEventListener('beforeunload', handler);
111
+ return () => window.removeEventListener('beforeunload', handler);
112
+ }, [isDirty]);
113
+
98
114
  const langCodeForPreview = useMemo(
99
115
  () => (shouldMergeTranslation ? renderLangCode : null),
100
116
  [shouldMergeTranslation, renderLangCode],
@@ -353,7 +369,14 @@ const FormEditorContent: React.FC<TranslationFnProps> = ({ t }) => {
353
369
  {isLoadingFormOrSchema ? (
354
370
  <InlineLoading description={t('loadingSchema', 'Loading schema') + '...'} />
355
371
  ) : (
356
- <h1 className={styles.formName}>{form?.name}</h1>
372
+ <h1 className={styles.formName}>
373
+ {form?.name}
374
+ {isDirty && (
375
+ <Tag className={styles.dirtyIndicator} type="warm-gray" size="sm">
376
+ {t('unsavedChanges', 'Unsaved changes')}
377
+ </Tag>
378
+ )}
379
+ </h1>
357
380
  )}
358
381
  </div>
359
382
  <div>
@@ -378,7 +401,7 @@ const FormEditorContent: React.FC<TranslationFnProps> = ({ t }) => {
378
401
  ) : null}
379
402
  {isNewSchema && !schema ? (
380
403
  <Button kind="ghost" onClick={inputDummySchema}>
381
- {t('inputDummySchema', 'Input dummy schema')}
404
+ {t('inputDummySchema', 'Load sample schema')}
382
405
  </Button>
383
406
  ) : null}
384
407
  <Dropdown
@@ -395,12 +418,18 @@ const FormEditorContent: React.FC<TranslationFnProps> = ({ t }) => {
395
418
  className={styles.dropdown}
396
419
  />
397
420
 
398
- <Button kind="ghost" onClick={handleRenderSchemaChanges} disabled={!!invalidJsonErrorMessage}>
399
- <span>{t('renderChanges', 'Render changes')}</span>
421
+ <Button
422
+ kind="tertiary"
423
+ size="sm"
424
+ renderIcon={Renew}
425
+ onClick={handleRenderSchemaChanges}
426
+ disabled={!!invalidJsonErrorMessage}
427
+ >
428
+ {t('renderChanges', 'Render changes')}
400
429
  </Button>
401
430
  </div>
402
431
  {schema ? (
403
- <>
432
+ <div className={styles.schemaIconActions}>
404
433
  <IconButton
405
434
  enterDelayMs={defaultEnterDelayInMs}
406
435
  kind="ghost"
@@ -429,7 +458,7 @@ const FormEditorContent: React.FC<TranslationFnProps> = ({ t }) => {
429
458
  <Download />
430
459
  </IconButton>
431
460
  </a>
432
- </>
461
+ </div>
433
462
  ) : null}
434
463
  </div>
435
464
  {formError ? (
@@ -518,10 +547,12 @@ function BackButton({ t }: TranslationFnProps) {
518
547
 
519
548
  function FormEditor() {
520
549
  const { t } = useTranslation();
550
+ const { formUuid } = useParams<{ formUuid: string }>();
551
+ const isNewForm = !formUuid;
521
552
 
522
553
  return (
523
554
  <>
524
- <Header title={t('schemaEditor', 'Schema editor')} />
555
+ <Header title={isNewForm ? t('createNewForm', 'Create a new form') : t('editForm', 'Edit form')} />
525
556
  <BackButton t={t} />
526
557
  <FormEditorContent t={t} />
527
558
  </>
@@ -56,6 +56,11 @@
56
56
  @include type.type-style('heading-03');
57
57
  }
58
58
 
59
+ .dirtyIndicator {
60
+ margin-left: layout.$spacing-03;
61
+ vertical-align: middle;
62
+ }
63
+
59
64
  .editorContainer {
60
65
  padding: 1rem;
61
66
 
@@ -70,6 +75,7 @@
70
75
  display: flex;
71
76
  margin-right: 1rem;
72
77
  align-items: center;
78
+ gap: layout.$spacing-05;
73
79
  }
74
80
 
75
81
  .tabHeading {
@@ -112,10 +118,18 @@ button {
112
118
  .topBtns {
113
119
  display: flex;
114
120
  align-items: center;
121
+ gap: layout.$spacing-05;
122
+ }
123
+
124
+ .schemaIconActions {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: layout.$spacing-02;
115
128
  }
116
129
 
117
130
  .dropdown {
118
- grid-gap: 0 !important;
131
+ gap: layout.$spacing-03 !important;
132
+ column-gap: layout.$spacing-03 !important;
119
133
 
120
134
  :global(.cds--label) {
121
135
  white-space: nowrap;
@@ -392,7 +392,8 @@ const InteractiveBuilder: React.FC<InteractiveBuilderProps> = ({
392
392
  </p>
393
393
  </div>
394
394
  <Button
395
- kind="ghost"
395
+ kind="tertiary"
396
+ size="sm"
396
397
  renderIcon={Add}
397
398
  onClick={launchAddPageModal}
398
399
  iconDescription={t('addPage', 'Add Page')}
@@ -420,7 +421,7 @@ const InteractiveBuilder: React.FC<InteractiveBuilderProps> = ({
420
421
  )}
421
422
  </p>
422
423
 
423
- <Button onClick={launchNewFormModal} className={styles.startButton} kind="ghost">
424
+ <Button onClick={launchNewFormModal} className={styles.startButton} kind="primary">
424
425
  {t('startBuilding', 'Start building')}
425
426
  </Button>
426
427
  </div>
@@ -481,7 +482,7 @@ const InteractiveBuilder: React.FC<InteractiveBuilderProps> = ({
481
482
  <IconButton
482
483
  enterDelayMs={300}
483
484
  kind="ghost"
484
- label={t('editSection', 'Edit Section')}
485
+ label={t('editSection', 'Edit section')}
485
486
  onClick={() =>
486
487
  section.reference
487
488
  ? launchAddFormReferenceModal(pageIndex, 'edit', sectionIndex)
@@ -565,7 +566,8 @@ const InteractiveBuilder: React.FC<InteractiveBuilderProps> = ({
565
566
 
566
567
  <Button
567
568
  className={styles.addQuestionButton}
568
- kind="ghost"
569
+ kind="tertiary"
570
+ size="sm"
569
571
  renderIcon={Add}
570
572
  onClick={() => {
571
573
  launchAddQuestionModal(pageIndex, sectionIndex);
@@ -178,7 +178,7 @@ const AddFormReferenceModal: React.FC<AddFormReferenceModalProps> = ({
178
178
  <ModalBody className={styles.modalBody}>
179
179
  <Stack gap={4}>
180
180
  {isLoading ? (
181
- <InlineLoading description={t('loading', 'Loading...')} />
181
+ <InlineLoading description={t('loading', 'Loading') + '...'} />
182
182
  ) : error ? (
183
183
  <InlineNotification>{t('errorLoadingForms', 'Error loading forms')}</InlineNotification>
184
184
  ) : forms.length === 0 ? (
@@ -200,7 +200,7 @@ const AddFormReferenceModal: React.FC<AddFormReferenceModalProps> = ({
200
200
  </FormGroup>
201
201
  ) : null}
202
202
  {isLoadingClobdata ? (
203
- <InlineLoading description={t('loading', 'Loading...')} />
203
+ <InlineLoading description={t('loading', 'Loading') + '...'} />
204
204
  ) : clobdataError ? (
205
205
  <InlineNotification>{t('errorLoadingForm', 'Error loading form')}</InlineNotification>
206
206
  ) : pages && pages.length > 0 && !mode ? (
@@ -102,7 +102,7 @@ const ConceptSearch: React.FC<ConceptSearchProps> = ({
102
102
  )}
103
103
  <div className={styles.searchContainer}>
104
104
  {isLoadingConcept ? (
105
- <InlineLoading className={styles.loader} description={t('loading', 'Loading...')} />
105
+ <InlineLoading className={styles.loader} description={t('loading', 'Loading') + '...'} />
106
106
  ) : (
107
107
  <Search
108
108
  id="conceptLookup"
@@ -195,11 +195,11 @@ const TranslationBuilder: React.FC<TranslationBuilderProps> = ({ formSchema, onU
195
195
  subtitle: t('translationsUploadedSuccessfully', `Translation file uploaded successfully.`),
196
196
  });
197
197
  } catch (err: any) {
198
- setError(t('translationFileUploadFail', 'Failed to upload translation file.'));
198
+ setError(t('translationFileUploadFail', 'Failed to upload translation file'));
199
199
  showSnackbar({
200
200
  title: t('uploadFailed', 'Upload Failed'),
201
201
  kind: 'error',
202
- subtitle: t('translationFileUploadFail', `Failed to upload translation file`),
202
+ subtitle: t('translationFileUploadFail', 'Failed to upload translation file'),
203
203
  });
204
204
  console.error(err);
205
205
  } finally {