@openmrs/esm-openconceptlab-app 3.0.1-pre.5 → 3.0.1-pre.55
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.
- package/.turbo/turbo-build.log +33 -36
- package/dist/130.js +2 -0
- package/dist/130.js.LICENSE.txt +5 -0
- package/dist/130.js.map +1 -0
- package/dist/316.js +1 -1
- package/dist/316.js.map +1 -0
- package/dist/392.js +1 -0
- package/dist/392.js.map +1 -0
- package/dist/442.js +2 -0
- package/dist/442.js.LICENSE.txt +5 -0
- package/dist/442.js.map +1 -0
- package/dist/553.js +1 -0
- package/dist/553.js.map +1 -0
- package/dist/574.js +1 -1
- package/dist/784.js +1 -1
- package/dist/784.js.map +1 -0
- package/dist/842.js +1 -0
- package/dist/842.js.map +1 -0
- package/dist/853.js +1 -0
- package/dist/853.js.map +1 -0
- package/dist/912.js +2 -0
- package/dist/912.js.LICENSE.txt +9 -0
- package/dist/912.js.map +1 -0
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-openconceptlab-app.js +1 -1
- package/dist/openmrs-esm-openconceptlab-app.js.buildmanifest.json +144 -55
- package/dist/openmrs-esm-openconceptlab-app.js.map +1 -0
- package/dist/routes.json +1 -0
- package/package.json +3 -3
- package/src/admin-ocl-card-link.component.tsx +23 -0
- package/src/declarations.d.tsx +2 -0
- package/src/import/import.component.tsx +38 -21
- package/src/import/import.resource.ts +1 -1
- package/src/import/import.test.tsx +13 -15
- package/src/index.ts +9 -51
- package/src/previous-imports/import-overview/import-items.component.tsx +124 -0
- package/src/previous-imports/import-overview/import-items.resource.ts +10 -0
- package/src/previous-imports/import-overview/import-items.scss +24 -0
- package/src/previous-imports/import-overview/import-items.test.tsx +82 -0
- package/src/previous-imports/import-overview/import-overview.component.tsx +65 -0
- package/src/previous-imports/import-overview/import-overview.scss +11 -0
- package/src/previous-imports/import-overview/import-overview.test.tsx +82 -0
- package/src/previous-imports/previous-imports.component.tsx +136 -0
- package/src/previous-imports/previous-imports.resource.ts +17 -0
- package/src/previous-imports/previous-imports.scss +39 -0
- package/src/previous-imports/previous-imports.test.tsx +76 -0
- package/src/root.component.tsx +12 -9
- package/src/root.test.tsx +4 -4
- package/src/routes.json +20 -0
- package/src/subscription/subscription.component.tsx +58 -24
- package/src/subscription/subscription.resource.ts +1 -1
- package/src/subscription/subscription.test.tsx +22 -20
- package/src/types.ts +10 -0
- package/src/utils.ts +27 -0
- package/translations/en.json +44 -26
- package/tsconfig.json +1 -19
- package/webpack.config.js +1 -1
- package/dist/15.js +0 -1
- package/dist/538.js +0 -2
- package/dist/538.js.LICENSE.txt +0 -20
- package/dist/709.js +0 -1
- package/dist/858.js +0 -2
- package/dist/858.js.LICENSE.txt +0 -1
- package/dist/862.js +0 -1
- package/dist/openmrs-esm-openconceptlab-app.old +0 -1
- package/locales/en/translation.json +0 -35
- package/locales/fr/translation.json +0 -35
- /package/src/import/{import.component.scss → import.scss} +0 -0
- /package/src/{root.component.scss → root.scss} +0 -0
- /package/src/subscription/{subscription.component.scss → subscription.scss} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Button,
|
|
4
4
|
ButtonSkeleton,
|
|
@@ -8,17 +8,20 @@ import {
|
|
|
8
8
|
FileUploaderSkeleton,
|
|
9
9
|
Form,
|
|
10
10
|
Grid,
|
|
11
|
+
InlineLoading,
|
|
12
|
+
Stack,
|
|
11
13
|
SkeletonText,
|
|
12
14
|
} from '@carbon/react';
|
|
13
|
-
import React, { useCallback, useEffect, useState } from 'react';
|
|
14
15
|
import { useTranslation } from 'react-i18next';
|
|
15
|
-
import
|
|
16
|
+
import { showNotification } from '@openmrs/esm-framework';
|
|
16
17
|
import { startImportWithFile, startImportWithSubscription, useSubscription } from './import.resource';
|
|
18
|
+
import styles from './import.scss';
|
|
17
19
|
|
|
18
20
|
const Import: React.FC = () => {
|
|
19
21
|
const { t } = useTranslation();
|
|
20
22
|
const [isSubscriptionAvailable, setIsSubscriptionAvailable] = useState(false);
|
|
21
23
|
const [file, setFile] = useState<File>();
|
|
24
|
+
const [isFileUploading, setIsFileUploading] = useState(false);
|
|
22
25
|
|
|
23
26
|
const { data: subscription, isLoading, isError } = useSubscription();
|
|
24
27
|
|
|
@@ -34,7 +37,7 @@ const Import: React.FC = () => {
|
|
|
34
37
|
if (fileToUpload.type !== 'application/zip') {
|
|
35
38
|
showNotification({
|
|
36
39
|
kind: 'error',
|
|
37
|
-
description: t('fileFormatError'),
|
|
40
|
+
description: t('fileFormatError', 'Only .zip files are allowed'),
|
|
38
41
|
});
|
|
39
42
|
} else {
|
|
40
43
|
setFile(fileToUpload);
|
|
@@ -55,7 +58,7 @@ const Import: React.FC = () => {
|
|
|
55
58
|
if (!isSubscriptionAvailable) {
|
|
56
59
|
showNotification({
|
|
57
60
|
kind: 'error',
|
|
58
|
-
description: t('noSubscriptionError'),
|
|
61
|
+
description: t('noSubscriptionError', 'No saved subscription'),
|
|
59
62
|
});
|
|
60
63
|
return;
|
|
61
64
|
}
|
|
@@ -66,12 +69,12 @@ const Import: React.FC = () => {
|
|
|
66
69
|
if (response.status === 201) {
|
|
67
70
|
showNotification({
|
|
68
71
|
kind: 'success',
|
|
69
|
-
description: t('importSuccess'),
|
|
72
|
+
description: t('importSuccess', 'Import started successfully'),
|
|
70
73
|
});
|
|
71
74
|
} else {
|
|
72
75
|
showNotification({
|
|
73
76
|
kind: 'error',
|
|
74
|
-
description: t('importFailed'),
|
|
77
|
+
description: t('importFailed', 'Import failed'),
|
|
75
78
|
});
|
|
76
79
|
}
|
|
77
80
|
},
|
|
@@ -80,13 +83,15 @@ const Import: React.FC = () => {
|
|
|
80
83
|
|
|
81
84
|
const handleImportWithFile = useCallback(
|
|
82
85
|
async (evt: React.FormEvent<HTMLFormElement>) => {
|
|
86
|
+
setIsFileUploading(true);
|
|
83
87
|
evt.preventDefault();
|
|
84
88
|
evt.stopPropagation();
|
|
85
89
|
|
|
86
90
|
if (!file) {
|
|
91
|
+
setIsFileUploading(false);
|
|
87
92
|
showNotification({
|
|
88
93
|
kind: 'error',
|
|
89
|
-
description: t('noFileSelected'),
|
|
94
|
+
description: t('noFileSelected', 'No file selected'),
|
|
90
95
|
});
|
|
91
96
|
return;
|
|
92
97
|
}
|
|
@@ -96,14 +101,16 @@ const Import: React.FC = () => {
|
|
|
96
101
|
|
|
97
102
|
if (response.status === 201) {
|
|
98
103
|
setFile(null);
|
|
104
|
+
setIsFileUploading(false);
|
|
99
105
|
showNotification({
|
|
100
106
|
kind: 'success',
|
|
101
|
-
description: t('importSuccess'),
|
|
107
|
+
description: t('importSuccess', 'Import started successfully'),
|
|
102
108
|
});
|
|
103
109
|
} else {
|
|
110
|
+
setIsFileUploading(false);
|
|
104
111
|
showNotification({
|
|
105
112
|
kind: 'error',
|
|
106
|
-
description: t('importFailed'),
|
|
113
|
+
description: t('importFailed', 'Import failed'),
|
|
107
114
|
});
|
|
108
115
|
}
|
|
109
116
|
},
|
|
@@ -134,7 +141,7 @@ const Import: React.FC = () => {
|
|
|
134
141
|
if (isError) {
|
|
135
142
|
showNotification({
|
|
136
143
|
kind: 'error',
|
|
137
|
-
description: t('subscriptionError'),
|
|
144
|
+
description: t('subscriptionError', 'Error occured while fetching the subscription'),
|
|
138
145
|
});
|
|
139
146
|
}
|
|
140
147
|
|
|
@@ -142,23 +149,30 @@ const Import: React.FC = () => {
|
|
|
142
149
|
<Grid className={styles.grid}>
|
|
143
150
|
<Column sm={4} md={8} lg={10}>
|
|
144
151
|
<Form onSubmit={handleImportWithSubscription}>
|
|
145
|
-
<h3 className={styles.productiveHeading03}>{t('importConcepts')}</h3>
|
|
146
|
-
<p className={styles.formText}>
|
|
152
|
+
<h3 className={styles.productiveHeading03}>{t('importConcepts', 'Import Concepts')}</h3>
|
|
153
|
+
<p className={styles.formText}>
|
|
154
|
+
{t(
|
|
155
|
+
'importInstructions',
|
|
156
|
+
'Avoid importing during data entry hours, because the operation may significantly slow down the system. You can either use the subscription or a zip file to import concepts.',
|
|
157
|
+
)}
|
|
158
|
+
</p>
|
|
147
159
|
<Button kind="primary" type="submit" disabled={!isSubscriptionAvailable}>
|
|
148
|
-
{t('importFromSubscription')}
|
|
160
|
+
{t('importFromSubscription', 'Import from Subscription')}
|
|
149
161
|
</Button>
|
|
150
162
|
</Form>
|
|
151
163
|
|
|
152
164
|
<Form className={styles.form} onSubmit={handleImportWithFile}>
|
|
153
|
-
<h3 className={styles.productiveHeading03}>{t('importFromFileHeading')}</h3>
|
|
154
|
-
<p className={styles.formText}>
|
|
165
|
+
<h3 className={styles.productiveHeading03}>{t('importFromFileHeading', 'Import from file (Offline)')}</h3>
|
|
166
|
+
<p className={styles.formText}>
|
|
167
|
+
{t('importFromFileInfo', 'You can import data without setting up a subscription by using this option.')}
|
|
168
|
+
</p>
|
|
155
169
|
{file ? (
|
|
156
170
|
<FileUploaderItem
|
|
157
171
|
key={file.name}
|
|
158
172
|
name={file.name}
|
|
159
173
|
size="default"
|
|
160
174
|
status="edit"
|
|
161
|
-
iconDescription={t('fileAdded')}
|
|
175
|
+
iconDescription={t('fileAdded', 'File Added')}
|
|
162
176
|
onDelete={handleFileUploaderItemClick}
|
|
163
177
|
style={{ backgroundColor: '#e0e0e0' }}
|
|
164
178
|
/>
|
|
@@ -166,14 +180,17 @@ const Import: React.FC = () => {
|
|
|
166
180
|
<FileUploaderDropContainer
|
|
167
181
|
accept={['application/zip']}
|
|
168
182
|
multiple
|
|
169
|
-
labelText={t('importFromFileDragInfo')}
|
|
183
|
+
labelText={t('importFromFileDragInfo', 'Drag and drop file here or click to upload')}
|
|
170
184
|
onAddFiles={onAddFiles}
|
|
171
185
|
style={{ marginBottom: '1.5rem' }}
|
|
172
186
|
/>
|
|
173
187
|
)}
|
|
174
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
188
|
+
<Stack gap={6} orientation="horizontal">
|
|
189
|
+
<Button kind="primary" type="submit">
|
|
190
|
+
{t('importFromFile', 'Import from file')}
|
|
191
|
+
</Button>
|
|
192
|
+
{isFileUploading && <InlineLoading description={t('uploading', 'Uploading...')} />}
|
|
193
|
+
</Stack>
|
|
177
194
|
</Form>
|
|
178
195
|
</Column>
|
|
179
196
|
</Grid>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { openmrsFetch } from '@openmrs/esm-framework';
|
|
2
2
|
import useSWR from 'swr';
|
|
3
|
-
import { Import, Subscription } from '../types';
|
|
3
|
+
import type { Import, Subscription } from '../types';
|
|
4
4
|
|
|
5
5
|
export function useSubscription() {
|
|
6
6
|
const { data, error, isValidating } = useSWR<{ data: { results: Subscription[] } }, Error>(
|
|
@@ -44,14 +44,12 @@ describe(`Import component`, () => {
|
|
|
44
44
|
renderImportComponent();
|
|
45
45
|
await waitForLoadingToFinish();
|
|
46
46
|
|
|
47
|
-
expect(screen.getByText('
|
|
48
|
-
expect(screen.getByText('
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
expect(screen.getByText('
|
|
52
|
-
expect(screen.
|
|
53
|
-
expect(screen.getByText('importFromFile')).toBeEnabled();
|
|
54
|
-
expect(screen.queryByText('fileAdded')).not.toBeInTheDocument();
|
|
47
|
+
expect(screen.getByText('Import Concepts')).toBeVisible();
|
|
48
|
+
expect(screen.getByText('Import from Subscription')).toBeVisible();
|
|
49
|
+
|
|
50
|
+
expect(screen.getByText('Import from file (Offline)')).toBeVisible();
|
|
51
|
+
expect(screen.getByText('Import from file')).toBeEnabled();
|
|
52
|
+
expect(screen.queryByText('File Added')).not.toBeInTheDocument();
|
|
55
53
|
});
|
|
56
54
|
|
|
57
55
|
it(`renders correctly when there is no subscription`, async () => {
|
|
@@ -59,8 +57,8 @@ describe(`Import component`, () => {
|
|
|
59
57
|
renderImportComponent();
|
|
60
58
|
await waitForLoadingToFinish();
|
|
61
59
|
|
|
62
|
-
expect(screen.getByText('
|
|
63
|
-
expect(screen.getByText('
|
|
60
|
+
expect(screen.getByText('Import from Subscription')).toBeDisabled();
|
|
61
|
+
expect(screen.getByText('Import from file')).toBeEnabled();
|
|
64
62
|
});
|
|
65
63
|
|
|
66
64
|
it(`renders correctly when when a subscription exists`, async () => {
|
|
@@ -68,8 +66,8 @@ describe(`Import component`, () => {
|
|
|
68
66
|
renderImportComponent();
|
|
69
67
|
await waitForLoadingToFinish();
|
|
70
68
|
|
|
71
|
-
expect(screen.getByText('
|
|
72
|
-
expect(screen.getByText('
|
|
69
|
+
await waitFor(() => expect(screen.getByText('Import from Subscription')).toBeEnabled(), { timeout: 2000 });
|
|
70
|
+
expect(screen.getByText('Import from file')).toBeEnabled();
|
|
73
71
|
});
|
|
74
72
|
|
|
75
73
|
it(`allows starting an import using the subscription`, async () => {
|
|
@@ -79,14 +77,14 @@ describe(`Import component`, () => {
|
|
|
79
77
|
|
|
80
78
|
mockStartImportWithSubscription.mockReturnValueOnce({ status: 201 });
|
|
81
79
|
|
|
82
|
-
await waitFor(() => userEvent.click(screen.getByText('
|
|
80
|
+
await waitFor(() => userEvent.click(screen.getByText('Import from Subscription')));
|
|
83
81
|
|
|
84
82
|
expect(mockStartImportWithSubscription).toHaveBeenCalledWith(new AbortController());
|
|
85
83
|
expect(mockStartImportWithSubscription).toHaveBeenCalledTimes(1);
|
|
86
84
|
|
|
87
85
|
expect(mockShowNotification).toHaveBeenCalledWith(
|
|
88
86
|
expect.objectContaining({
|
|
89
|
-
description: '
|
|
87
|
+
description: 'Import started successfully',
|
|
90
88
|
kind: 'success',
|
|
91
89
|
}),
|
|
92
90
|
);
|
|
@@ -100,6 +98,6 @@ function renderImportComponent() {
|
|
|
100
98
|
|
|
101
99
|
function waitForLoadingToFinish() {
|
|
102
100
|
return waitFor(() => {
|
|
103
|
-
expect(screen.getByText('
|
|
101
|
+
expect(screen.getByText('Import Concepts')).toBeVisible, { timeout: 2000 };
|
|
104
102
|
});
|
|
105
103
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,61 +1,19 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This is the entrypoint file of the application. It communicates the
|
|
3
|
-
* important features of this microfrontend to the app shell. It
|
|
4
|
-
* connects the app shell to the React application(s) that make up this
|
|
5
|
-
* microfrontend.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { getAsyncLifecycle, defineConfigSchema } from '@openmrs/esm-framework';
|
|
9
2
|
import { configSchema } from './config-schema';
|
|
10
3
|
|
|
11
|
-
|
|
12
|
-
* This tells the app shell how to obtain translation files: that they
|
|
13
|
-
* are JSON files in the directory `../translations` (which you should
|
|
14
|
-
* see in the directory structure).
|
|
15
|
-
*/
|
|
16
|
-
const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
|
|
4
|
+
const moduleName = '@openmrs/esm-openconceptlab-app';
|
|
17
5
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* installed. The keys are the part of the module name after
|
|
22
|
-
* `openmrs-module-`; e.g., `openmrs-module-fhir2` becomes `fhir2`.
|
|
23
|
-
*/
|
|
24
|
-
const backendDependencies = {
|
|
25
|
-
openconceptlab: '^1.2.0',
|
|
26
|
-
'webservices.rest': '^2.2.0',
|
|
6
|
+
const options = {
|
|
7
|
+
featureName: 'openconceptlab',
|
|
8
|
+
moduleName,
|
|
27
9
|
};
|
|
28
10
|
|
|
29
|
-
|
|
30
|
-
* This function performs any setup that should happen at microfrontend
|
|
31
|
-
* load-time (such as defining the config schema) and then returns an
|
|
32
|
-
* object which describes how the React application(s) should be
|
|
33
|
-
* rendered.
|
|
34
|
-
*
|
|
35
|
-
* In this example, our return object contains a single page definition.
|
|
36
|
-
* It tells the app shell that the default export of `greeter.tsx`
|
|
37
|
-
* should be rendered when the route matches `hello`. The full route
|
|
38
|
-
* will be `openmrsSpaBase() + 'hello'`, which is usually
|
|
39
|
-
* `/openmrs/spa/hello`.
|
|
40
|
-
*/
|
|
41
|
-
function setupOpenMRS() {
|
|
42
|
-
const moduleName = '@openmrs/esm-openconceptlab-app';
|
|
43
|
-
|
|
44
|
-
const options = {
|
|
45
|
-
featureName: 'openconceptlab',
|
|
46
|
-
moduleName,
|
|
47
|
-
};
|
|
11
|
+
export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
|
|
48
12
|
|
|
13
|
+
export function startupApp() {
|
|
49
14
|
defineConfigSchema(moduleName, configSchema);
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
pages: [
|
|
53
|
-
{
|
|
54
|
-
load: getAsyncLifecycle(() => import('./root.component'), options),
|
|
55
|
-
route: 'ocl',
|
|
56
|
-
},
|
|
57
|
-
],
|
|
58
|
-
};
|
|
59
15
|
}
|
|
60
16
|
|
|
61
|
-
export
|
|
17
|
+
export const root = getAsyncLifecycle(() => import('./root.component'), options);
|
|
18
|
+
|
|
19
|
+
export const adminOclCardLink = getAsyncLifecycle(() => import('./admin-ocl-card-link.component'), options);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { showNotification, usePagination } from '@openmrs/esm-framework';
|
|
2
|
+
import {
|
|
3
|
+
DataTableSkeleton,
|
|
4
|
+
Link,
|
|
5
|
+
Pagination,
|
|
6
|
+
PaginationSkeleton,
|
|
7
|
+
Table,
|
|
8
|
+
TableBody,
|
|
9
|
+
TableCell,
|
|
10
|
+
TableHead,
|
|
11
|
+
TableHeader,
|
|
12
|
+
TableRow,
|
|
13
|
+
} from '@carbon/react';
|
|
14
|
+
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
|
15
|
+
import { useTranslation } from 'react-i18next';
|
|
16
|
+
import { getImportDetails } from './import-items.resource';
|
|
17
|
+
import type { ImportItem } from '../../types';
|
|
18
|
+
import styles from './import-items.scss';
|
|
19
|
+
|
|
20
|
+
interface ImportItemsProps {
|
|
21
|
+
importUuid: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ImportItems: React.FC<ImportItemsProps> = ({ importUuid }) => {
|
|
25
|
+
const { t } = useTranslation();
|
|
26
|
+
const [isLoading, setIsLoading] = useState<Boolean>();
|
|
27
|
+
const [pageSize, setPageSize] = useState(5);
|
|
28
|
+
|
|
29
|
+
const [selectedImportItems, setSelectedImportItems] = useState<ImportItem[]>([]);
|
|
30
|
+
const { results, currentPage, goTo } = usePagination(selectedImportItems, pageSize);
|
|
31
|
+
|
|
32
|
+
const handleImportDetails = useCallback(
|
|
33
|
+
async (uuid: string) => {
|
|
34
|
+
setIsLoading(true);
|
|
35
|
+
const abortController = new AbortController();
|
|
36
|
+
|
|
37
|
+
const response = await getImportDetails(uuid, abortController);
|
|
38
|
+
|
|
39
|
+
if (response.ok) {
|
|
40
|
+
setSelectedImportItems(response.data.results);
|
|
41
|
+
} else {
|
|
42
|
+
showNotification({
|
|
43
|
+
title: t('importItemsFetchError', 'Error occured while fetching the import items'),
|
|
44
|
+
kind: 'error',
|
|
45
|
+
critical: true,
|
|
46
|
+
description: JSON.stringify(response.data),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
return () => abortController.abort();
|
|
51
|
+
},
|
|
52
|
+
[t],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
handleImportDetails(importUuid);
|
|
57
|
+
}, [handleImportDetails, importUuid]);
|
|
58
|
+
|
|
59
|
+
if (isLoading) {
|
|
60
|
+
return (
|
|
61
|
+
<div>
|
|
62
|
+
<DataTableSkeleton showHeader={false} showToolbar={false} columnCount={2} />
|
|
63
|
+
<PaginationSkeleton />
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const headerData = [
|
|
69
|
+
{
|
|
70
|
+
header: t('conceptOrMapping', 'Concept/Mapping'),
|
|
71
|
+
key: 'uuid',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
header: t('message', 'Message'),
|
|
75
|
+
key: 'errorMessage',
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
!isLoading && (
|
|
81
|
+
<div>
|
|
82
|
+
<Table size="sm" className={styles.tableBordered}>
|
|
83
|
+
<TableHead>
|
|
84
|
+
<TableRow className={styles.tableHeaderRow}>
|
|
85
|
+
{headerData.map((header, i) => (
|
|
86
|
+
<TableHeader key={i} isSortable={true}>
|
|
87
|
+
{header.header}
|
|
88
|
+
</TableHeader>
|
|
89
|
+
))}
|
|
90
|
+
</TableRow>
|
|
91
|
+
</TableHead>
|
|
92
|
+
<TableBody>
|
|
93
|
+
{results?.map((row) => (
|
|
94
|
+
<Fragment key={row.uuid}>
|
|
95
|
+
<TableRow className={styles.tableDataRow}>
|
|
96
|
+
<TableCell>
|
|
97
|
+
<Link href={row.versionUrl}>
|
|
98
|
+
{row.type} {row.uuid}
|
|
99
|
+
</Link>
|
|
100
|
+
</TableCell>
|
|
101
|
+
<TableCell>{row.errorMessage}</TableCell>
|
|
102
|
+
</TableRow>
|
|
103
|
+
</Fragment>
|
|
104
|
+
))}
|
|
105
|
+
</TableBody>
|
|
106
|
+
</Table>
|
|
107
|
+
<Pagination
|
|
108
|
+
className={styles.pagination}
|
|
109
|
+
size="sm"
|
|
110
|
+
page={currentPage}
|
|
111
|
+
pageSize={pageSize}
|
|
112
|
+
pageSizes={[5, 10, 20, 50, 100]}
|
|
113
|
+
totalItems={selectedImportItems?.length}
|
|
114
|
+
onChange={({ page, pageSize }) => {
|
|
115
|
+
goTo(page);
|
|
116
|
+
setPageSize(pageSize);
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export default ImportItems;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { openmrsFetch } from '@openmrs/esm-framework';
|
|
2
|
+
import type { ImportItem } from '../../types';
|
|
3
|
+
|
|
4
|
+
export async function getImportDetails(importUuid: string, abortController?: AbortController) {
|
|
5
|
+
const url = `/ws/rest/v1/openconceptlab/import/${importUuid}/item?state=ERROR&v=full`;
|
|
6
|
+
return openmrsFetch<{ results: ImportItem[] }>(url, {
|
|
7
|
+
method: 'GET',
|
|
8
|
+
signal: abortController?.signal,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
@use "@carbon/styles/scss/spacing";
|
|
2
|
+
@use "@carbon/styles/scss/type";
|
|
3
|
+
@import "~@openmrs/esm-styleguide/src/vars";
|
|
4
|
+
|
|
5
|
+
.pagination {
|
|
6
|
+
@include type.type-style('body-short-01');
|
|
7
|
+
background-color: $ui-02;
|
|
8
|
+
color: $text-02;
|
|
9
|
+
border: 1px solid $ui-03;
|
|
10
|
+
display: flex;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.tableBordered {
|
|
14
|
+
border: 1px solid $ui-03;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.tableDataRow > td {
|
|
18
|
+
padding-left: 1rem !important;
|
|
19
|
+
background-color: $ui-02 !important;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.tableHeaderRow > th {
|
|
23
|
+
background-color: $ui-03 !important;
|
|
24
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import { usePagination } from '@openmrs/esm-framework';
|
|
4
|
+
import { renderWithSwr } from '../../../../../tools/test-helpers';
|
|
5
|
+
import { mockImportItems, mockPreviousImports } from '../../../../../__mocks__/openconceptlab.mock';
|
|
6
|
+
import { getImportDetails } from './import-items.resource';
|
|
7
|
+
import ImportItems from './import-items.component';
|
|
8
|
+
|
|
9
|
+
const testProps = {
|
|
10
|
+
importUuid: mockPreviousImports[1].uuid,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const mockGetImportDetails = getImportDetails as jest.Mock;
|
|
14
|
+
const mockUsePagination = usePagination as jest.Mock;
|
|
15
|
+
|
|
16
|
+
jest.mock('./import-items.resource', () => {
|
|
17
|
+
const originalModule = jest.requireActual('./import-items.resource');
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
...originalModule,
|
|
21
|
+
getImportDetails: jest.fn(),
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
jest.mock('@openmrs/esm-framework', () => {
|
|
26
|
+
const originalModule = jest.requireActual('@openmrs/esm-framework');
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
...originalModule,
|
|
30
|
+
usePagination: jest.fn(),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe(`Import Items component`, () => {
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
mockGetImportDetails.mockReset();
|
|
37
|
+
mockUsePagination.mockReset();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it(`renders the table`, async () => {
|
|
41
|
+
mockGetImportDetails.mockReturnValue({ status: 200, ok: true, data: mockImportItems });
|
|
42
|
+
mockUsePagination.mockReturnValue({
|
|
43
|
+
currentPage: 1,
|
|
44
|
+
goTo: () => {},
|
|
45
|
+
results: mockImportItems,
|
|
46
|
+
});
|
|
47
|
+
renderImportItemsComponent();
|
|
48
|
+
await waitForLoadingToFinish();
|
|
49
|
+
|
|
50
|
+
expect(screen.getByText('Concept/Mapping')).toBeVisible();
|
|
51
|
+
expect(screen.getByText('Message')).toBeVisible();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it(`renders the import items correctly`, async () => {
|
|
55
|
+
mockGetImportDetails.mockReturnValue({ status: 200, ok: true, data: mockImportItems });
|
|
56
|
+
mockUsePagination.mockReturnValue({
|
|
57
|
+
currentPage: 1,
|
|
58
|
+
goTo: () => {},
|
|
59
|
+
results: mockImportItems,
|
|
60
|
+
});
|
|
61
|
+
renderImportItemsComponent();
|
|
62
|
+
await waitForLoadingToFinish();
|
|
63
|
+
|
|
64
|
+
expect(screen.getByText('Concept/Mapping')).toBeVisible();
|
|
65
|
+
expect(screen.getByText('Message')).toBeVisible();
|
|
66
|
+
|
|
67
|
+
mockImportItems.slice(5).forEach((importItem) => {
|
|
68
|
+
expect(screen.getByText(importItem.type + ' ' + importItem.uuid)).toBeVisible();
|
|
69
|
+
expect(screen.getByText(importItem.errorMessage)).toBeVisible();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function renderImportItemsComponent() {
|
|
75
|
+
renderWithSwr(<ImportItems {...testProps} />);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function waitForLoadingToFinish() {
|
|
79
|
+
return waitFor(() => {
|
|
80
|
+
expect(screen.getByText('Concept/Mapping')).toBeVisible(), { timeout: 2000 };
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { formatDatetime } from '@openmrs/esm-framework';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import type { Import } from '../../types';
|
|
5
|
+
import ImportItems from './import-items.component';
|
|
6
|
+
import styles from './import-overview.scss';
|
|
7
|
+
|
|
8
|
+
interface ImportOverviewProps {
|
|
9
|
+
selectedImportObject: Import;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ImportOverview: React.FC<ImportOverviewProps> = ({ selectedImportObject }) => {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div>
|
|
17
|
+
<span className={styles.heading01}>{t('startedOn', 'Started on')} </span>
|
|
18
|
+
<span className="body">{formatDatetime(new Date(selectedImportObject.localDateStarted))}</span>
|
|
19
|
+
<br />
|
|
20
|
+
|
|
21
|
+
<span className={styles.heading01}>{t('completedOn', 'Completed on')} </span>
|
|
22
|
+
<span className="body">{formatDatetime(new Date(selectedImportObject.localDateStopped))}</span>
|
|
23
|
+
<br />
|
|
24
|
+
|
|
25
|
+
<span className={styles.heading01}>{t('duration', 'Duration')} </span>
|
|
26
|
+
<span className="body">{selectedImportObject.importTime}</span>
|
|
27
|
+
<br />
|
|
28
|
+
<br />
|
|
29
|
+
|
|
30
|
+
<span className={styles.heading01}>{t('result', 'Result:')} </span>
|
|
31
|
+
<br />
|
|
32
|
+
|
|
33
|
+
<div className={`body ${styles.indentedContent}`}>
|
|
34
|
+
<span>
|
|
35
|
+
{selectedImportObject.upToDateItemsCount} {t('conceptsUpToDate', 'concepts up to date')}
|
|
36
|
+
</span>
|
|
37
|
+
<br />
|
|
38
|
+
<span>
|
|
39
|
+
{selectedImportObject.addedItemsCount} {t('conceptsAdded', 'concepts added')}
|
|
40
|
+
</span>
|
|
41
|
+
<br />
|
|
42
|
+
<span>
|
|
43
|
+
{selectedImportObject.updatedItemsCount} {t('conceptsUpdated', 'concepts updated')}
|
|
44
|
+
</span>
|
|
45
|
+
<br />
|
|
46
|
+
<span>
|
|
47
|
+
{selectedImportObject.retiredItemsCount} {t('conceptsRetired', 'concepts retired')}
|
|
48
|
+
</span>
|
|
49
|
+
<br />
|
|
50
|
+
<span>
|
|
51
|
+
{selectedImportObject.errorItemsCount} {t('errorsFound', 'errors found')}
|
|
52
|
+
</span>
|
|
53
|
+
<br />
|
|
54
|
+
<span>
|
|
55
|
+
{selectedImportObject.ignoredErrorsCount} {t('errorsIgnored', 'errors ignored')}
|
|
56
|
+
</span>
|
|
57
|
+
<br />
|
|
58
|
+
</div>
|
|
59
|
+
<br />
|
|
60
|
+
{selectedImportObject.errorItemsCount != 0 && <ImportItems importUuid={selectedImportObject.uuid} />}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default ImportOverview;
|