@kenyaemr/esm-admin-app 5.4.4-pre.297 → 5.4.4-pre.3

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 (124) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/100.js +1 -0
  3. package/dist/100.js.map +1 -0
  4. package/dist/1442.js +1 -0
  5. package/dist/1442.js.map +1 -0
  6. package/dist/1462.js +1 -1
  7. package/dist/1972.js +1 -1
  8. package/dist/2016.js +1 -1
  9. package/dist/2080.js +1 -1
  10. package/dist/2080.js.map +1 -1
  11. package/dist/2096.js +1 -1
  12. package/dist/2096.js.map +1 -1
  13. package/dist/2270.js +1 -1
  14. package/dist/2270.js.map +1 -1
  15. package/dist/2294.js +1 -1
  16. package/dist/2294.js.map +1 -1
  17. package/dist/2467.js +1 -0
  18. package/dist/2467.js.map +1 -0
  19. package/dist/257.js +1 -1
  20. package/dist/257.js.map +1 -1
  21. package/dist/2948.js +1 -1
  22. package/dist/3380.js +1 -0
  23. package/dist/3380.js.map +1 -0
  24. package/dist/3548.js +1 -1
  25. package/dist/3816.js +1 -1
  26. package/dist/4337.js +1 -1
  27. package/dist/4687.js +1 -0
  28. package/dist/4687.js.map +1 -0
  29. package/dist/4858.js +1 -1
  30. package/dist/487.js +1 -1
  31. package/dist/5297.js +1 -0
  32. package/dist/5297.js.map +1 -0
  33. package/dist/5910.js +1 -0
  34. package/dist/5910.js.map +1 -0
  35. package/dist/5940.js +1 -1
  36. package/dist/5940.js.map +1 -1
  37. package/dist/6253.js +1 -0
  38. package/dist/6253.js.map +1 -0
  39. package/dist/6455.js +1 -0
  40. package/dist/6455.js.map +1 -0
  41. package/dist/6456.js +1 -1
  42. package/dist/6925.js +1 -0
  43. package/dist/6925.js.map +1 -0
  44. package/dist/7607.js +1 -1
  45. package/dist/7737.js +1 -1
  46. package/dist/7739.js +1 -1
  47. package/dist/7866.js +1 -1
  48. package/dist/7866.js.map +1 -1
  49. package/dist/8206.js +1 -0
  50. package/dist/8206.js.map +1 -0
  51. package/dist/8376.js +1 -1
  52. package/dist/846.js +17 -0
  53. package/dist/846.js.map +1 -0
  54. package/dist/{4047.js → 8487.js} +1 -1
  55. package/dist/8487.js.map +1 -0
  56. package/dist/8528.js +1 -0
  57. package/dist/8528.js.map +1 -0
  58. package/dist/8727.js +1 -1
  59. package/dist/9182.js +1 -1
  60. package/dist/9446.js +1 -1
  61. package/dist/9585.js +1 -0
  62. package/dist/9585.js.map +1 -0
  63. package/dist/9647.js +1 -0
  64. package/dist/9647.js.map +1 -0
  65. package/dist/kenyaemr-esm-admin-app.js +3 -3
  66. package/dist/kenyaemr-esm-admin-app.js.buildmanifest.json +329 -306
  67. package/dist/kenyaemr-esm-admin-app.js.map +1 -1
  68. package/dist/main.js +3 -3
  69. package/dist/main.js.map +1 -1
  70. package/dist/routes.json +1 -1
  71. package/package.json +1 -1
  72. package/src/components/confirm-modal/confirmation-operation.test.tsx +8 -19
  73. package/src/components/dashboard/dashboard.component.tsx +4 -1
  74. package/src/components/empty-state/empty-state-log.test.tsx +3 -4
  75. package/src/components/global-property/dashboard/global-property-dashboard.component.tsx +23 -0
  76. package/src/components/global-property/dashboard/global-property-dashboard.scss +6 -0
  77. package/src/components/global-property/hooks/useGlobalProperty.ts +64 -0
  78. package/src/components/global-property/index.ts +14 -0
  79. package/src/components/global-property/modal/delete-global-property-modal.component.tsx +71 -0
  80. package/src/components/global-property/modal/delete-global-property-modal.test.tsx +131 -0
  81. package/src/components/global-property/table/global-property-table.component.tsx +249 -0
  82. package/src/components/global-property/table/global-property-table.scss +34 -0
  83. package/src/components/global-property/table/global-property-table.test.tsx +198 -0
  84. package/src/components/global-property/workspace/global-property-form-schema.ts +32 -0
  85. package/src/components/global-property/workspace/global-property.workspace.scss +40 -0
  86. package/src/components/global-property/workspace/global-property.workspace.test.tsx +172 -0
  87. package/src/components/global-property/workspace/global-property.workspace.tsx +260 -0
  88. package/src/components/logs-table/operation-log-table.component.tsx +87 -75
  89. package/src/components/logs-table/operation-log.test.tsx +134 -28
  90. package/src/index.ts +9 -1
  91. package/src/root.component.tsx +2 -0
  92. package/src/routes.json +20 -1
  93. package/src/types/index.ts +11 -2
  94. package/translations/am.json +39 -2
  95. package/translations/en.json +62 -24
  96. package/translations/fr.json +167 -128
  97. package/translations/sw.json +311 -275
  98. package/dist/12.js +0 -17
  99. package/dist/12.js.map +0 -1
  100. package/dist/1201.js +0 -1
  101. package/dist/1201.js.map +0 -1
  102. package/dist/2402.js +0 -1
  103. package/dist/2402.js.map +0 -1
  104. package/dist/3366.js +0 -1
  105. package/dist/3366.js.map +0 -1
  106. package/dist/3852.js +0 -1
  107. package/dist/3852.js.map +0 -1
  108. package/dist/4047.js.map +0 -1
  109. package/dist/552.js +0 -1
  110. package/dist/552.js.map +0 -1
  111. package/dist/6092.js +0 -1
  112. package/dist/6092.js.map +0 -1
  113. package/dist/6399.js +0 -1
  114. package/dist/6399.js.map +0 -1
  115. package/dist/6676.js +0 -1
  116. package/dist/6676.js.map +0 -1
  117. package/dist/6976.js +0 -1
  118. package/dist/6976.js.map +0 -1
  119. package/dist/7916.js +0 -1
  120. package/dist/7916.js.map +0 -1
  121. package/dist/9406.js +0 -1
  122. package/dist/9406.js.map +0 -1
  123. package/dist/9711.js +0 -1
  124. package/dist/9711.js.map +0 -1
@@ -0,0 +1,260 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import {
3
+ ResponsiveWrapper,
4
+ Workspace2,
5
+ showSnackbar,
6
+ useLayoutType,
7
+ type Workspace2DefinitionProps,
8
+ } from '@openmrs/esm-framework';
9
+ import { useTranslation } from 'react-i18next';
10
+ import { Controller, useForm } from 'react-hook-form';
11
+ import { Button, ButtonSet, ComboBox, Form, FormGroup, InlineLoading, Stack, TextArea, TextInput } from '@carbon/react';
12
+ import { zodResolver } from '@hookform/resolvers/zod';
13
+ import classNames from 'classnames';
14
+
15
+ import styles from './global-property.workspace.scss';
16
+ import { type SystemSettings } from '../../../types';
17
+ import { saveOrUpdateGlobalProperty } from '../hooks/useGlobalProperty';
18
+ import {
19
+ createGlobalPropertyFormSchema,
20
+ openmrsCustomDatatypes,
21
+ type GlobalPropertyFormType,
22
+ } from './global-property-form-schema';
23
+
24
+ type GlobalPropertyWorkspaceProps = {
25
+ systemSetting?: SystemSettings | null;
26
+ mutateGlobalProperty: () => void;
27
+ };
28
+
29
+ const GlobalPropertyWorkspace: React.FC<Workspace2DefinitionProps<GlobalPropertyWorkspaceProps, {}, {}>> = ({
30
+ closeWorkspace,
31
+ workspaceProps: { systemSetting, mutateGlobalProperty },
32
+ }) => {
33
+ const { t } = useTranslation();
34
+ const isTablet = useLayoutType() === 'tablet';
35
+ const isEditMode = Boolean(systemSetting?.uuid);
36
+ const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
37
+
38
+ const {
39
+ handleSubmit,
40
+ control,
41
+ formState: { isSubmitting, isDirty, errors },
42
+ } = useForm<GlobalPropertyFormType>({
43
+ resolver: zodResolver(createGlobalPropertyFormSchema(t)),
44
+ defaultValues: {
45
+ property: systemSetting?.property ?? '',
46
+ description: systemSetting?.description ?? '',
47
+ datatypeClassname: systemSetting?.datatypeClassname ?? '',
48
+ datatypeConfig: systemSetting?.datatypeConfig ?? '',
49
+ preferredHandlerClassname: systemSetting?.preferredHandlerClassname ?? '',
50
+ handlerConfig: systemSetting?.handlerConfig ?? '',
51
+ value: systemSetting?.value ?? '',
52
+ },
53
+ });
54
+
55
+ useEffect(() => {
56
+ setHasUnsavedChanges(isDirty);
57
+ }, [isDirty]);
58
+
59
+ const onSubmit = async (data: GlobalPropertyFormType) => {
60
+ const payload = {
61
+ property: data.property,
62
+ value: data.value,
63
+ description: data.description,
64
+ datatypeClassname: data.datatypeClassname,
65
+ datatypeConfig: data.datatypeConfig,
66
+ preferredHandlerClassname: data.preferredHandlerClassname,
67
+ handlerConfig: data.handlerConfig,
68
+ };
69
+ try {
70
+ await saveOrUpdateGlobalProperty(payload, systemSetting?.uuid);
71
+
72
+ showSnackbar({
73
+ title: t('success', 'Success'),
74
+ kind: 'success',
75
+ subtitle: isEditMode
76
+ ? t('gpUpdated', 'Global property {{property}} was updated successfully.', { property: data.property })
77
+ : t('gpCreated', 'Global property {{property}} was created successfully.', { property: data.property }),
78
+ });
79
+ mutateGlobalProperty();
80
+ closeWorkspace({ discardUnsavedChanges: true });
81
+ } catch (error: any) {
82
+ showSnackbar({
83
+ title: t('error', 'Error'),
84
+ kind: 'error',
85
+ subtitle: error?.message ?? t('gpSaveError', 'Error saving global property'),
86
+ });
87
+ }
88
+ };
89
+
90
+ const title = isEditMode
91
+ ? t('editGlobalProperty', 'Edit global property')
92
+ : t('addGlobalProperty', 'Add global property');
93
+
94
+ return (
95
+ <Workspace2 title={title} hasUnsavedChanges={hasUnsavedChanges}>
96
+ <Form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
97
+ <div className={styles.formContainer}>
98
+ <Stack gap={4}>
99
+ <ResponsiveWrapper>
100
+ <FormGroup legendText="">
101
+ <Controller
102
+ control={control}
103
+ name="property"
104
+ render={({ field }) => (
105
+ <TextInput
106
+ id="gpProperty"
107
+ labelText={t('property', 'Property (required)')}
108
+ placeholder={t('gpPropertyPlaceholder', 'e.g. setting.name')}
109
+ value={field.value}
110
+ onChange={field.onChange}
111
+ invalid={!!errors.property}
112
+ invalidText={errors.property?.message}
113
+ disabled={isEditMode}
114
+ />
115
+ )}
116
+ />
117
+ </FormGroup>
118
+ </ResponsiveWrapper>
119
+
120
+ <ResponsiveWrapper>
121
+ <FormGroup legendText="">
122
+ <Controller
123
+ control={control}
124
+ name="value"
125
+ render={({ field }) => (
126
+ <TextArea
127
+ id="gpValue"
128
+ labelText={t('value', 'Value (required)')}
129
+ placeholder={t('gpValuePlaceholder', 'Enter value')}
130
+ value={field.value}
131
+ onChange={field.onChange}
132
+ invalid={!!errors.value}
133
+ invalidText={errors.value?.message}
134
+ rows={3}
135
+ />
136
+ )}
137
+ />
138
+ </FormGroup>
139
+ </ResponsiveWrapper>
140
+
141
+ <ResponsiveWrapper>
142
+ <FormGroup legendText="">
143
+ <Controller
144
+ control={control}
145
+ name="description"
146
+ render={({ field }) => (
147
+ <TextArea
148
+ id="gpDescription"
149
+ labelText={t('description', 'Description')}
150
+ placeholder={t('gpDescriptionPlaceholder', 'Optional description')}
151
+ value={field.value ?? ''}
152
+ onChange={field.onChange}
153
+ rows={2}
154
+ />
155
+ )}
156
+ />
157
+ </FormGroup>
158
+ </ResponsiveWrapper>
159
+
160
+ <ResponsiveWrapper>
161
+ <FormGroup legendText="">
162
+ <Controller
163
+ control={control}
164
+ name="datatypeClassname"
165
+ render={({ field }) => (
166
+ <ComboBox
167
+ id="datatypeClassname"
168
+ invalidText={errors.datatypeClassname?.message}
169
+ items={openmrsCustomDatatypes}
170
+ onChange={function Zye() {}}
171
+ titleText={t('datatypeClassname', 'Datatype classname')}
172
+ typeahead
173
+ placeholder={t('selectDatatypeClassname', 'Select datatype classname')}
174
+ />
175
+ )}
176
+ />
177
+ </FormGroup>
178
+ </ResponsiveWrapper>
179
+
180
+ <ResponsiveWrapper>
181
+ <FormGroup legendText="">
182
+ <Controller
183
+ control={control}
184
+ name="datatypeConfig"
185
+ render={({ field }) => (
186
+ <TextInput
187
+ id="gpDatatypeConfig"
188
+ labelText={t('datatypeConfig', 'Datatype config')}
189
+ placeholder={t('gpDatatypeConfigPlaceholder', 'Optional datatype configuration')}
190
+ value={field.value ?? ''}
191
+ onChange={field.onChange}
192
+ />
193
+ )}
194
+ />
195
+ </FormGroup>
196
+ </ResponsiveWrapper>
197
+
198
+ <ResponsiveWrapper>
199
+ <FormGroup legendText="">
200
+ <Controller
201
+ control={control}
202
+ name="preferredHandlerClassname"
203
+ render={({ field }) => (
204
+ <TextInput
205
+ id="gpPreferredHandlerClassname"
206
+ labelText={t('preferredHandlerClassname', 'Preferred handler classname')}
207
+ placeholder={t('gpPreferredHandlerPlaceholder', 'Optional preferred handler classname')}
208
+ value={field.value ?? ''}
209
+ onChange={field.onChange}
210
+ />
211
+ )}
212
+ />
213
+ </FormGroup>
214
+ </ResponsiveWrapper>
215
+
216
+ <ResponsiveWrapper>
217
+ <FormGroup legendText="">
218
+ <Controller
219
+ control={control}
220
+ name="handlerConfig"
221
+ render={({ field }) => (
222
+ <TextInput
223
+ id="gpHandlerConfig"
224
+ labelText={t('handlerConfig', 'Handler config')}
225
+ placeholder={t('gpHandlerConfigPlaceholder', 'Optional handler configuration')}
226
+ value={field.value ?? ''}
227
+ onChange={field.onChange}
228
+ />
229
+ )}
230
+ />
231
+ </FormGroup>
232
+ </ResponsiveWrapper>
233
+ </Stack>
234
+ </div>
235
+
236
+ <ButtonSet
237
+ className={classNames({
238
+ [styles.tablet]: isTablet,
239
+ [styles.desktop]: !isTablet,
240
+ })}>
241
+ <Button className={styles.buttonContainer} kind="secondary" onClick={() => closeWorkspace()}>
242
+ {t('cancel', 'Cancel')}
243
+ </Button>
244
+ <Button className={styles.buttonContainer} disabled={isSubmitting || !isDirty} kind="primary" type="submit">
245
+ {isSubmitting ? (
246
+ <span className={styles.inlineLoading}>
247
+ {t('submitting', 'Submitting...')}
248
+ <InlineLoading status="active" iconDescription="Loading" />
249
+ </span>
250
+ ) : (
251
+ t('saveAndClose', 'Save & close')
252
+ )}
253
+ </Button>
254
+ </ButtonSet>
255
+ </Form>
256
+ </Workspace2>
257
+ );
258
+ };
259
+
260
+ export default GlobalPropertyWorkspace;
@@ -12,7 +12,6 @@ import {
12
12
  TableRow,
13
13
  Tag,
14
14
  DataTableSkeleton,
15
- InlineLoading,
16
15
  } from '@carbon/react';
17
16
  import { CardHeader, PatientChartPagination } from '@openmrs/esm-patient-common-lib';
18
17
  import { useLayoutType, usePagination, formatDate } from '@openmrs/esm-framework';
@@ -21,13 +20,14 @@ import { ETLResponse } from '../../types';
21
20
  import EmptyState from '../empty-state/empty-state-log.components';
22
21
 
23
22
  interface LogTableProps {
24
- logData: ETLResponse[];
23
+ logData: Array<ETLResponse>;
25
24
  isLoading: boolean;
26
25
  }
27
26
 
27
+ const PAGE_SIZE = 10;
28
+
28
29
  const LogTable: React.FC<LogTableProps> = ({ logData, isLoading }) => {
29
30
  const { t } = useTranslation();
30
- const pageSize = 10;
31
31
  const isTablet = useLayoutType() === 'tablet';
32
32
 
33
33
  const headers = [
@@ -37,82 +37,94 @@ const LogTable: React.FC<LogTableProps> = ({ logData, isLoading }) => {
37
37
  { header: t('completionStatus', 'Completion status'), key: 'status' },
38
38
  ];
39
39
 
40
- const rows = logData?.map((item, index) => ({
41
- id: index.toString(),
42
- script_name: item.script_name,
43
- start_time: item?.start_time ? formatDate(new Date(item?.start_time)) : '--',
44
- stop_time: item?.stop_time ? formatDate(new Date(item?.stop_time)) : '--',
45
- status: item.status,
46
- }));
40
+ const rows =
41
+ logData?.map((item, index) => ({
42
+ id: index.toString(),
43
+ script_name: item.script_name,
44
+ start_time: item?.start_time ? formatDate(new Date(item.start_time)) : '--',
45
+ stop_time: item?.stop_time ? formatDate(new Date(item.stop_time)) : '--',
46
+ status: item.status,
47
+ })) ?? [];
48
+
49
+ const { results: paginatedData, currentPage, goTo } = usePagination(rows, PAGE_SIZE);
50
+
51
+ const renderTable = ({ rows, headers, getHeaderProps, getTableProps }: any) => (
52
+ <TableContainer className={styles.tableContainer}>
53
+ <Table {...getTableProps()}>
54
+ <TableHead>
55
+ <TableRow>
56
+ {headers.map((header: any) => (
57
+ <TableHeader
58
+ className={classNames(styles.productiveHeading01, styles.text02)}
59
+ {...getHeaderProps({ header })}>
60
+ {header.header}
61
+ </TableHeader>
62
+ ))}
63
+ </TableRow>
64
+ </TableHead>
65
+ <TableBody>
66
+ {rows.map((row: any) => (
67
+ <TableRow key={row.id}>
68
+ {row.cells.map((cell: any) => (
69
+ <TableCell key={cell.id}>
70
+ {cell.info.header === 'status' ? (
71
+ <Tag size="md" type={cell.value === 'Success' ? 'green' : 'red'}>
72
+ {cell.value}
73
+ </Tag>
74
+ ) : (
75
+ cell.value
76
+ )}
77
+ </TableCell>
78
+ ))}
79
+ </TableRow>
80
+ ))}
81
+ </TableBody>
82
+ </Table>
83
+ </TableContainer>
84
+ );
85
+
86
+ const renderContent = () => {
87
+ if (isLoading && logData.length === 0) {
88
+ return (
89
+ <DataTableSkeleton
90
+ headers={headers}
91
+ aria-label="etl table"
92
+ showToolbar={false}
93
+ showHeader={false}
94
+ rowCount={4}
95
+ zebra
96
+ columnCount={3}
97
+ className={styles.dataTableSkeleton}
98
+ />
99
+ );
100
+ }
101
+
102
+ if (logData.length === 0) {
103
+ return <EmptyState subTitle={t('noRecordsFound', 'No ETL Operation logs found')} />;
104
+ }
47
105
 
48
- const { results: paginatedData, currentPage, goTo } = usePagination(rows, pageSize);
106
+ return (
107
+ <>
108
+ <DataTable rows={paginatedData} headers={headers} isSortable size={isTablet ? 'lg' : 'sm'} useZebraStyles>
109
+ {renderTable}
110
+ </DataTable>
111
+ <PatientChartPagination
112
+ currentItems={paginatedData.length}
113
+ totalItems={rows.length}
114
+ onPageNumberChange={({ page }: { page: number }) => goTo(page)}
115
+ pageNumber={currentPage}
116
+ pageSize={PAGE_SIZE}
117
+ />
118
+ </>
119
+ );
120
+ };
49
121
 
50
122
  return (
51
123
  <div className={styles.table}>
52
- <CardHeader title={t('facilityInfo', 'Facility Info')} children={''} />
53
- <div className={styles.logTable}>
54
- {isLoading && logData.length === 0 ? (
55
- <DataTableSkeleton
56
- headers={headers}
57
- aria-label="etl table"
58
- showToolbar={false}
59
- showHeader={false}
60
- rowCount={4}
61
- zebra
62
- columnCount={3}
63
- className={styles.dataTableSkeleton}
64
- />
65
- ) : logData.length === 0 ? (
66
- <EmptyState subTitle={t('noRecordsFound', 'No ETL Operation logs found')} />
67
- ) : (
68
- <>
69
- <DataTable rows={paginatedData} headers={headers} isSortable size={isTablet ? 'lg' : 'sm'} useZebraStyles>
70
- {({ rows, headers, getHeaderProps, getTableProps }) => (
71
- <TableContainer className={styles.tableContainer}>
72
- <Table {...getTableProps()}>
73
- <TableHead>
74
- <TableRow>
75
- {headers.map((header) => (
76
- <TableHeader
77
- key={header.key}
78
- className={classNames(styles.productiveHeading01, styles.text02)}
79
- {...getHeaderProps({ header })}>
80
- {header.header}
81
- </TableHeader>
82
- ))}
83
- </TableRow>
84
- </TableHead>
85
- <TableBody>
86
- {rows.map((row) => (
87
- <TableRow key={row.id}>
88
- {row.cells.map((cell) => (
89
- <TableCell key={cell.id}>
90
- {cell.info.header === 'status' ? (
91
- <Tag size="md" type={cell.value === 'Success' ? 'green' : 'red'}>
92
- {cell.value}
93
- </Tag>
94
- ) : (
95
- cell.value
96
- )}
97
- </TableCell>
98
- ))}
99
- </TableRow>
100
- ))}
101
- </TableBody>
102
- </Table>
103
- </TableContainer>
104
- )}
105
- </DataTable>
106
- <PatientChartPagination
107
- currentItems={paginatedData.length}
108
- totalItems={rows.length}
109
- onPageNumberChange={({ page }) => goTo(page)}
110
- pageNumber={currentPage}
111
- pageSize={pageSize}
112
- />
113
- </>
114
- )}
115
- </div>
124
+ <CardHeader title={t('facilityInfo', 'Facility Info')}>
125
+ <></>
126
+ </CardHeader>
127
+ <div className={styles.logTable}>{renderContent()}</div>
116
128
  </div>
117
129
  );
118
130
  };
@@ -1,47 +1,153 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom';
4
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
4
5
  import LogTable from './operation-log-table.component';
5
6
  import { ETLResponse } from '../../types';
6
7
 
7
- describe('LogTable Component', () => {
8
- const mockLogData: ETLResponse[] = [
9
- {
10
- script_name: 'Initial Population of Tables',
11
- start_time: '2024-11-28T09:30:12',
12
- stop_time: '2024-11-28T09:33:15',
13
- status: 'Success',
14
- },
15
- {
16
- script_name: 'Initial Creation of Tables',
17
- start_time: '2024-11-28T09:30:12',
18
- stop_time: '2024-11-28T09:33:15',
19
- status: 'Failed',
8
+ vi.mock('react-i18next', () => ({
9
+ useTranslation: () => ({ t: (key: string, fallback: string) => fallback }),
10
+ }));
11
+
12
+ vi.mock('@openmrs/esm-framework', () => ({
13
+ useLayoutType: vi.fn(() => 'desktop'),
14
+ usePagination: vi.fn((data) => ({
15
+ results: data,
16
+ currentPage: 1,
17
+ goTo: vi.fn(),
18
+ })),
19
+ formatDate: vi.fn((date: Date) => date.toISOString()),
20
+ }));
21
+
22
+ vi.mock('@openmrs/esm-patient-common-lib', () => ({
23
+ CardHeader: ({ title }: { title: string }) => <div>{title}</div>,
24
+ PatientChartPagination: ({ currentItems, totalItems }: { currentItems: number; totalItems: number }) => (
25
+ <div data-testid="pagination">
26
+ {currentItems}/{totalItems}
27
+ </div>
28
+ ),
29
+ }));
30
+
31
+ vi.mock('../empty-state/empty-state-log.components', () => ({
32
+ default: ({ subTitle }: { subTitle: string }) => <div data-testid="empty-state">{subTitle}</div>,
33
+ }));
34
+
35
+ vi.mock('@carbon/react', async (importOriginal) => {
36
+ const original = await importOriginal<typeof import('@carbon/react')>();
37
+ return {
38
+ ...original,
39
+ DataTable: ({
40
+ children,
41
+ rows,
42
+ headers,
43
+ }: {
44
+ children: (props: any) => React.ReactNode;
45
+ rows: any[];
46
+ headers: any[];
47
+ }) => {
48
+ if (typeof children === 'function') {
49
+ const renderRows = rows.map((row) => ({
50
+ ...row,
51
+ cells: headers.map((h) => ({
52
+ id: `${row.id}:${h.key}`,
53
+ value: row[h.key],
54
+ info: { header: h.key },
55
+ })),
56
+ }));
57
+ return children({
58
+ rows: renderRows,
59
+ headers,
60
+ getTableProps: () => ({}),
61
+ getHeaderProps: ({ header }: any) => ({ key: header.key }),
62
+ getRowProps: () => ({}),
63
+ });
64
+ }
65
+ return children;
20
66
  },
21
- ];
67
+ };
68
+ });
22
69
 
23
- it('renders the table with correct headers and rows', () => {
24
- render(<LogTable logData={mockLogData} isLoading={false} />);
70
+ vi.mock('classnames', () => ({ default: (...args: string[]) => args.filter(Boolean).join(' ') }));
25
71
 
26
- const headers = ['Procedure', 'Start time', 'End time', 'Completion status'];
27
- headers.forEach((header) => {
28
- expect(screen.getByText(header)).toBeInTheDocument();
29
- });
72
+ vi.mock('./operation-log.scss', () => ({ default: {} }));
30
73
 
31
- const rows = screen.getAllByRole('row');
32
- expect(rows.length).toBe(3);
74
+ const mockLogData: ETLResponse[] = [
75
+ {
76
+ script_name: 'Script A',
77
+ start_time: '2024-01-01T08:00:00Z',
78
+ stop_time: '2024-01-01T09:00:00Z',
79
+ status: 'Success',
80
+ },
81
+ {
82
+ script_name: 'Script B',
83
+ start_time: '2024-01-02T10:00:00Z',
84
+ stop_time: '2024-01-02T11:00:00Z',
85
+ status: 'Failed',
86
+ },
87
+ ];
88
+
89
+ describe('LogTable', () => {
90
+ beforeEach(() => {
91
+ vi.clearAllMocks();
33
92
  });
34
93
 
35
- it('displays "No ETL Operation logs found" when logData is empty', () => {
36
- render(<LogTable logData={[]} isLoading={false} />);
94
+ it('renders the card header', () => {
95
+ render(<LogTable logData={mockLogData} isLoading={false} />);
96
+ expect(screen.getByText('Facility Info')).toBeInTheDocument();
97
+ });
98
+
99
+ it('shows skeleton while loading with no data', () => {
100
+ render(<LogTable logData={[]} isLoading={true} />);
101
+ expect(screen.getByRole('table')).toBeInTheDocument();
102
+ });
37
103
 
104
+ it('shows empty state when not loading and no data', () => {
105
+ render(<LogTable logData={[]} isLoading={false} />);
106
+ expect(screen.getByTestId('empty-state')).toBeInTheDocument();
38
107
  expect(screen.getByText('No ETL Operation logs found')).toBeInTheDocument();
39
108
  });
40
109
 
41
- it('shows skeleton loading state when isLoading is true', () => {
42
- render(<LogTable logData={[]} isLoading={true} />);
110
+ it('renders table headers when data is present', () => {
111
+ render(<LogTable logData={mockLogData} isLoading={false} />);
112
+ expect(screen.getByText('Procedure')).toBeInTheDocument();
113
+ expect(screen.getByText('Start time')).toBeInTheDocument();
114
+ expect(screen.getByText('End time')).toBeInTheDocument();
115
+ expect(screen.getByText('Completion status')).toBeInTheDocument();
116
+ });
117
+
118
+ it('renders script names in the table', () => {
119
+ render(<LogTable logData={mockLogData} isLoading={false} />);
120
+ expect(screen.getByText('Script A')).toBeInTheDocument();
121
+ expect(screen.getByText('Script B')).toBeInTheDocument();
122
+ });
123
+
124
+ it('renders Success status as a green tag', () => {
125
+ render(<LogTable logData={mockLogData} isLoading={false} />);
126
+ const successTag = screen.getByText('Success');
127
+ expect(successTag).toBeInTheDocument();
128
+ });
129
+
130
+ it('renders Failed status as a red tag', () => {
131
+ render(<LogTable logData={mockLogData} isLoading={false} />);
132
+ const failedTag = screen.getByText('Failed');
133
+ expect(failedTag).toBeInTheDocument();
134
+ });
135
+
136
+ it('renders pagination when data is present', () => {
137
+ render(<LogTable logData={mockLogData} isLoading={false} />);
138
+ expect(screen.getByTestId('pagination')).toBeInTheDocument();
139
+ });
140
+
141
+ it('renders -- for missing start_time', () => {
142
+ const dataWithMissingTime: ETLResponse[] = [
143
+ { script_name: 'Script C', start_time: '', stop_time: '', status: 'Success' },
144
+ ];
145
+ render(<LogTable logData={dataWithMissingTime} isLoading={false} />);
146
+ expect(screen.getAllByText('--').length).toBeGreaterThanOrEqual(2);
147
+ });
43
148
 
44
- const skeleton = screen.getByLabelText('etl table');
45
- expect(skeleton).toBeInTheDocument();
149
+ it('does not show empty state when data is present', () => {
150
+ render(<LogTable logData={mockLogData} isLoading={false} />);
151
+ expect(screen.queryByTestId('empty-state')).not.toBeInTheDocument();
46
152
  });
47
153
  });
package/src/index.ts CHANGED
@@ -9,7 +9,7 @@ const options = {
9
9
  };
10
10
 
11
11
  export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
12
-
12
+ export * from './components/global-property/index';
13
13
  export const root = getAsyncLifecycle(() => import('./root.component'), options);
14
14
 
15
15
  export function startupApp() {
@@ -76,6 +76,14 @@ export const searchLocationWorkspace = getAsyncLifecycle(
76
76
  () => import('./components/locations/forms/search-location/search-location.workspace'),
77
77
  options,
78
78
  );
79
+ export const globalPropertyWorkspace = getAsyncLifecycle(
80
+ () => import('./components/global-property/workspace/global-property.workspace'),
81
+ options,
82
+ );
83
+ export const deleteGlobalPropertyModal = getAsyncLifecycle(
84
+ () => import('./components/global-property/modal/delete-global-property-modal.component'),
85
+ options,
86
+ );
79
87
 
80
88
  // t('providerBanner', 'Provider banner')
81
89
  export const providerBanner = getAsyncLifecycle(
@@ -9,6 +9,7 @@ import FacilitySetup from './components/facility-setup/facility-setup.component'
9
9
  import HomeComponent from './components/locations/home/home-locations.component';
10
10
 
11
11
  import styles from './root.scss';
12
+ import GlobalPropertyDashboard from './components/global-property/dashboard/global-property-dashboard.component';
12
13
 
13
14
  const Root: React.FC = () => {
14
15
  const spaBasePath = globalThis.spaBase;
@@ -28,6 +29,7 @@ const Root: React.FC = () => {
28
29
  <Route path="/etl-administration" element={<EtlAdminDashboard />} />
29
30
  <Route path="/facility-setup" element={<FacilitySetup />} />
30
31
  <Route path="/locations" element={<HomeComponent />} />
32
+ <Route path="/global-property" element={<GlobalPropertyDashboard />} />
31
33
  </Routes>
32
34
  </main>
33
35
  </BrowserRouter>