@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
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import { formatDatetime } from '@openmrs/esm-framework';
|
|
4
|
+
import { renderWithSwr } from '../../../../../tools/test-helpers';
|
|
5
|
+
import { mockPreviousImports } from '../../../../../__mocks__/openconceptlab.mock';
|
|
6
|
+
import ImportOverview from './import-overview.component';
|
|
7
|
+
|
|
8
|
+
describe(`Import Overview component`, () => {
|
|
9
|
+
it(`renders without dying`, () => {
|
|
10
|
+
renderImportOverviewComponent(mockPreviousImports[0]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it(`renders the fields`, async () => {
|
|
14
|
+
renderImportOverviewComponent(mockPreviousImports[0]);
|
|
15
|
+
await waitForLoadingToFinish();
|
|
16
|
+
|
|
17
|
+
expect(screen.getByText('Started on')).toBeVisible();
|
|
18
|
+
expect(screen.getByText('Completed on')).toBeVisible();
|
|
19
|
+
expect(screen.getByText('Duration')).toBeVisible();
|
|
20
|
+
expect(screen.getByText('Result:')).toBeVisible();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it(`renders the import details when no errored items`, async () => {
|
|
24
|
+
const selectedImport = mockPreviousImports[0];
|
|
25
|
+
|
|
26
|
+
renderImportOverviewComponent(selectedImport);
|
|
27
|
+
await waitForLoadingToFinish();
|
|
28
|
+
|
|
29
|
+
const startedTimeText = formatDatetime(selectedImport.localDateStarted);
|
|
30
|
+
const stoppedTimeText = formatDatetime(selectedImport.localDateStopped);
|
|
31
|
+
const upToDateItemsText = selectedImport.upToDateItemsCount + ' concepts up to date';
|
|
32
|
+
const addedItemsText = selectedImport.addedItemsCount + ' concepts added';
|
|
33
|
+
const updatedItemsText = selectedImport.updatedItemsCount + ' concepts updated';
|
|
34
|
+
const retiredItemsText = selectedImport.retiredItemsCount + ' concepts retired';
|
|
35
|
+
const errorItemsText = selectedImport.errorItemsCount + ' errors found';
|
|
36
|
+
const ignoredErrorsText = selectedImport.ignoredErrorsCount + ' errors ignored';
|
|
37
|
+
|
|
38
|
+
expect(screen.getByText(startedTimeText)).toBeVisible();
|
|
39
|
+
expect(screen.getByText(stoppedTimeText)).toBeVisible();
|
|
40
|
+
expect(screen.getByText(upToDateItemsText)).toBeVisible();
|
|
41
|
+
expect(screen.getByText(addedItemsText)).toBeVisible();
|
|
42
|
+
expect(screen.getByText(updatedItemsText)).toBeVisible();
|
|
43
|
+
expect(screen.getByText(retiredItemsText)).toBeVisible();
|
|
44
|
+
expect(screen.getByText(errorItemsText)).toBeVisible();
|
|
45
|
+
expect(screen.getByText(ignoredErrorsText)).toBeVisible();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it(`renders the import details when there are errored items`, async () => {
|
|
49
|
+
const selectedImport = mockPreviousImports[1];
|
|
50
|
+
|
|
51
|
+
renderImportOverviewComponent(selectedImport);
|
|
52
|
+
await waitForLoadingToFinish();
|
|
53
|
+
|
|
54
|
+
const startedTimeText = formatDatetime(selectedImport.localDateStarted);
|
|
55
|
+
const stoppedTimeText = formatDatetime(selectedImport.localDateStopped);
|
|
56
|
+
const upToDateItemsText = selectedImport.upToDateItemsCount + ' concepts up to date';
|
|
57
|
+
const addedItemsText = selectedImport.addedItemsCount + ' concepts added';
|
|
58
|
+
const updatedItemsText = selectedImport.updatedItemsCount + ' concepts updated';
|
|
59
|
+
const retiredItemsText = selectedImport.retiredItemsCount + ' concepts retired';
|
|
60
|
+
const errorItemsText = selectedImport.errorItemsCount + ' errors found';
|
|
61
|
+
const ignoredErrorsText = selectedImport.ignoredErrorsCount + ' errors ignored';
|
|
62
|
+
|
|
63
|
+
expect(screen.getByText(startedTimeText)).toBeVisible();
|
|
64
|
+
expect(screen.getByText(stoppedTimeText)).toBeVisible();
|
|
65
|
+
expect(screen.getByText(upToDateItemsText)).toBeVisible();
|
|
66
|
+
expect(screen.getByText(addedItemsText)).toBeVisible();
|
|
67
|
+
expect(screen.getByText(updatedItemsText)).toBeVisible();
|
|
68
|
+
expect(screen.getByText(retiredItemsText)).toBeVisible();
|
|
69
|
+
expect(screen.getByText(errorItemsText)).toBeVisible();
|
|
70
|
+
expect(screen.getByText(ignoredErrorsText)).toBeVisible();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function renderImportOverviewComponent(importObject) {
|
|
75
|
+
renderWithSwr(<ImportOverview selectedImportObject={importObject} />);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function waitForLoadingToFinish() {
|
|
79
|
+
return waitFor(() => {
|
|
80
|
+
expect(screen.getByText('Started on')).toBeVisible(), { timeout: 2000 };
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import React, { Fragment, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Column,
|
|
4
|
+
DataTable,
|
|
5
|
+
DataTableSkeleton,
|
|
6
|
+
Pagination,
|
|
7
|
+
PaginationSkeleton,
|
|
8
|
+
Grid,
|
|
9
|
+
SkeletonText,
|
|
10
|
+
Table,
|
|
11
|
+
TableBody,
|
|
12
|
+
TableCell,
|
|
13
|
+
TableExpandedRow,
|
|
14
|
+
TableExpandHeader,
|
|
15
|
+
TableExpandRow,
|
|
16
|
+
TableHead,
|
|
17
|
+
TableHeader,
|
|
18
|
+
TableRow,
|
|
19
|
+
} from '@carbon/react';
|
|
20
|
+
import { useTranslation } from 'react-i18next';
|
|
21
|
+
import { formatDatetime, showNotification, usePagination } from '@openmrs/esm-framework';
|
|
22
|
+
import { usePreviousImports } from './previous-imports.resource';
|
|
23
|
+
import type { Import } from '../types';
|
|
24
|
+
import ImportOverview from './import-overview/import-overview.component';
|
|
25
|
+
import styles from './previous-imports.scss';
|
|
26
|
+
|
|
27
|
+
const PreviousImports: React.FC = () => {
|
|
28
|
+
const { t } = useTranslation();
|
|
29
|
+
const [pageSize, setPageSize] = useState(10);
|
|
30
|
+
|
|
31
|
+
const { data: prevImports, isLoading, isError } = usePreviousImports();
|
|
32
|
+
const { results, currentPage, goTo } = usePagination(prevImports, pageSize);
|
|
33
|
+
|
|
34
|
+
if (isLoading) {
|
|
35
|
+
return (
|
|
36
|
+
<Grid className={styles.grid}>
|
|
37
|
+
<Column sm={4} md={8} lg={10}>
|
|
38
|
+
<SkeletonText className={styles.productiveHeading03} />
|
|
39
|
+
<DataTableSkeleton showHeader={false} showToolbar={false} rowCount={10} columnCount={3} />
|
|
40
|
+
<PaginationSkeleton />
|
|
41
|
+
</Column>
|
|
42
|
+
</Grid>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (isError) {
|
|
47
|
+
showNotification({
|
|
48
|
+
kind: 'error',
|
|
49
|
+
description: t('previousImportsFetchError', 'Error occured while fetching the imports'),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const headerData = [
|
|
54
|
+
{
|
|
55
|
+
header: t('dateAndTime', 'Date and Time'),
|
|
56
|
+
key: 'localDateStarted',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
header: t('duration', 'Duration'),
|
|
60
|
+
key: 'importTime',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
header: t('status', 'Status'),
|
|
64
|
+
key: 'status',
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const rowData = results?.map((prevImport) => {
|
|
69
|
+
return {
|
|
70
|
+
id: prevImport.uuid,
|
|
71
|
+
localDateStarted: formatDatetime(new Date(prevImport.localDateStarted)),
|
|
72
|
+
importTime: prevImport.importTime,
|
|
73
|
+
status: prevImport.status,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
!isLoading &&
|
|
79
|
+
!isError && (
|
|
80
|
+
<Grid className={styles.grid}>
|
|
81
|
+
<Column sm={4} md={8} lg={10}>
|
|
82
|
+
<h3 className={styles.productiveHeading03}>{t('previousImports', 'Previous Imports')}</h3>
|
|
83
|
+
|
|
84
|
+
<DataTable rows={rowData} headers={headerData} size="sm">
|
|
85
|
+
{({ rows, headers, getHeaderProps, getRowProps, getTableProps }) => (
|
|
86
|
+
<Table {...getTableProps()} className={styles.tableBordered}>
|
|
87
|
+
<TableHead>
|
|
88
|
+
<TableRow>
|
|
89
|
+
<TableExpandHeader />
|
|
90
|
+
{headers.map((header, i) => (
|
|
91
|
+
<TableHeader key={i} {...getHeaderProps({ header })}>
|
|
92
|
+
{header.header}
|
|
93
|
+
</TableHeader>
|
|
94
|
+
))}
|
|
95
|
+
</TableRow>
|
|
96
|
+
</TableHead>
|
|
97
|
+
<TableBody>
|
|
98
|
+
{rows.map((row) => (
|
|
99
|
+
<Fragment key={row.id}>
|
|
100
|
+
<TableExpandRow {...getRowProps({ row })} className={styles.tableRow}>
|
|
101
|
+
{row.cells.map((cell) => (
|
|
102
|
+
<TableCell key={cell.id}>{cell.value}</TableCell>
|
|
103
|
+
))}
|
|
104
|
+
</TableExpandRow>
|
|
105
|
+
{row.isExpanded && (
|
|
106
|
+
<TableExpandedRow colSpan={headers.length + 1} className={styles.tableExpandedRow}>
|
|
107
|
+
<ImportOverview
|
|
108
|
+
selectedImportObject={prevImports.find((importItem: Import) => importItem.uuid === row.id)}
|
|
109
|
+
/>
|
|
110
|
+
</TableExpandedRow>
|
|
111
|
+
)}
|
|
112
|
+
</Fragment>
|
|
113
|
+
))}
|
|
114
|
+
</TableBody>
|
|
115
|
+
</Table>
|
|
116
|
+
)}
|
|
117
|
+
</DataTable>
|
|
118
|
+
<Pagination
|
|
119
|
+
className={styles.pagination}
|
|
120
|
+
size="sm"
|
|
121
|
+
page={currentPage}
|
|
122
|
+
pageSize={pageSize}
|
|
123
|
+
pageSizes={[10, 20, 50, 100]}
|
|
124
|
+
totalItems={prevImports.length}
|
|
125
|
+
onChange={({ page, pageSize }) => {
|
|
126
|
+
goTo(page);
|
|
127
|
+
setPageSize(pageSize);
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
</Column>
|
|
131
|
+
</Grid>
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export default PreviousImports;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { openmrsFetch } from '@openmrs/esm-framework';
|
|
2
|
+
import useSWR from 'swr';
|
|
3
|
+
import type { Import } from '../types';
|
|
4
|
+
|
|
5
|
+
export function usePreviousImports() {
|
|
6
|
+
const { data, error, isValidating } = useSWR<{ data: { results: Import[] } }, Error>(
|
|
7
|
+
'/ws/rest/v1/openconceptlab/import?v=full',
|
|
8
|
+
openmrsFetch,
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
data: data?.data?.results,
|
|
13
|
+
isLoading: !data && !error,
|
|
14
|
+
isError: error,
|
|
15
|
+
isValidating,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
@use "@carbon/styles/scss/spacing";
|
|
2
|
+
@use "@carbon/styles/scss/type";
|
|
3
|
+
@import "~@openmrs/esm-styleguide/src/vars";
|
|
4
|
+
|
|
5
|
+
.grid {
|
|
6
|
+
margin-left: 0;
|
|
7
|
+
margin-right: 0;
|
|
8
|
+
padding-left: 0;
|
|
9
|
+
padding-right: 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.productiveHeading03 {
|
|
13
|
+
@include type.type-style('productive-heading-03');
|
|
14
|
+
margin-bottom: spacing.$spacing-03;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.pagination {
|
|
18
|
+
@include type.type-style('body-short-01');
|
|
19
|
+
background-color: $ui-02;
|
|
20
|
+
color: $text-02;
|
|
21
|
+
border: 1px solid $ui-03;
|
|
22
|
+
display: flex;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.tableBordered {
|
|
26
|
+
border: 1px solid $ui-03;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.tableRow > td {
|
|
30
|
+
background-color: $ui-02;
|
|
31
|
+
border-bottom: 1px solid $ui-03 !important;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.tableExpandedRow > td {
|
|
35
|
+
padding-left: spacing.$spacing-09 !important;
|
|
36
|
+
padding-right: spacing.$spacing-09;
|
|
37
|
+
padding-top: spacing.$spacing-05;
|
|
38
|
+
padding-bottom: spacing.$spacing-05;
|
|
39
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import { formatDatetime, openmrsFetch, usePagination } from '@openmrs/esm-framework';
|
|
4
|
+
import { renderWithSwr } from '../../../../tools/test-helpers';
|
|
5
|
+
import { mockPreviousImports } from '../../../../__mocks__/openconceptlab.mock';
|
|
6
|
+
import PreviousImports from './previous-imports.component';
|
|
7
|
+
|
|
8
|
+
const mockOpenmrsFetch = openmrsFetch as jest.Mock;
|
|
9
|
+
const mockUsePagination = usePagination as jest.Mock;
|
|
10
|
+
|
|
11
|
+
jest.mock('@openmrs/esm-framework', () => {
|
|
12
|
+
const originalModule = jest.requireActual('@openmrs/esm-framework');
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
...originalModule,
|
|
16
|
+
usePagination: jest.fn(),
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe(`Previous Imports component`, () => {
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
mockUsePagination.mockReset();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it(`renders without dying`, () => {
|
|
26
|
+
mockUsePagination.mockReturnValue({
|
|
27
|
+
currentPage: 1,
|
|
28
|
+
goTo: () => {},
|
|
29
|
+
results: [],
|
|
30
|
+
});
|
|
31
|
+
renderPreviousImportsComponent();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it(`renders the table`, async () => {
|
|
35
|
+
mockOpenmrsFetch.mockReturnValueOnce({ data: { results: [] } });
|
|
36
|
+
mockUsePagination.mockReturnValue({
|
|
37
|
+
currentPage: 1,
|
|
38
|
+
goTo: () => {},
|
|
39
|
+
results: [],
|
|
40
|
+
});
|
|
41
|
+
renderPreviousImportsComponent();
|
|
42
|
+
await waitForLoadingToFinish();
|
|
43
|
+
|
|
44
|
+
expect(screen.getByText('Previous Imports')).toBeVisible();
|
|
45
|
+
expect(screen.getByText('Date and Time')).toBeVisible();
|
|
46
|
+
expect(screen.getByText('Duration')).toBeVisible();
|
|
47
|
+
expect(screen.getByText('Status')).toBeVisible();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it(`renders the previous imports correctly`, async () => {
|
|
51
|
+
mockOpenmrsFetch.mockReturnValueOnce({ data: { results: mockPreviousImports } });
|
|
52
|
+
mockUsePagination.mockReturnValue({
|
|
53
|
+
currentPage: 1,
|
|
54
|
+
goTo: () => {},
|
|
55
|
+
results: mockPreviousImports,
|
|
56
|
+
});
|
|
57
|
+
renderPreviousImportsComponent();
|
|
58
|
+
await waitForLoadingToFinish();
|
|
59
|
+
|
|
60
|
+
mockPreviousImports.forEach((item) => {
|
|
61
|
+
expect(screen.getByText(formatDatetime(item.localDateStarted))).toBeVisible();
|
|
62
|
+
expect(screen.getByText(item.importTime)).toBeVisible();
|
|
63
|
+
expect(screen.getByText(item.status)).toBeVisible();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function renderPreviousImportsComponent() {
|
|
69
|
+
renderWithSwr(<PreviousImports />);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function waitForLoadingToFinish() {
|
|
73
|
+
return waitFor(() => {
|
|
74
|
+
expect(screen.getByText('Previous Imports')).toBeVisible(), { timeout: 2000 };
|
|
75
|
+
});
|
|
76
|
+
}
|
package/src/root.component.tsx
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import { Tab, Tabs, TabList, TabPanels, TabPanel } from '@carbon/react';
|
|
2
1
|
import React from 'react';
|
|
2
|
+
import { Tab, Tabs, TabList, TabPanels, TabPanel } from '@carbon/react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { SWRConfig } from 'swr';
|
|
5
|
-
import Subscription from './subscription/subscription.component';
|
|
6
|
-
import styles from './root.component.scss';
|
|
7
5
|
import Import from './import/import.component';
|
|
6
|
+
import PreviousImports from './previous-imports/previous-imports.component';
|
|
7
|
+
import Subscription from './subscription/subscription.component';
|
|
8
|
+
import styles from './root.scss';
|
|
8
9
|
|
|
9
10
|
const Root: React.FC = () => {
|
|
10
11
|
const { t } = useTranslation();
|
|
11
12
|
return (
|
|
12
13
|
<SWRConfig>
|
|
13
14
|
<main className={`omrs-main-content ${styles.main}`}>
|
|
14
|
-
<h3 className={styles.moduleHeader}>{t('moduleTitle')}</h3>
|
|
15
|
+
<h3 className={styles.moduleHeader}>{t('moduleTitle', 'OCL Subscription Module')}</h3>
|
|
15
16
|
<Tabs>
|
|
16
|
-
<TabList className={styles.tabList} contained={true}>
|
|
17
|
-
<Tab>{t('subscription')} </Tab>
|
|
18
|
-
<Tab>{t('import')} </Tab>
|
|
19
|
-
<Tab>{t('previousImports')} </Tab>
|
|
17
|
+
<TabList aria-label="OCL tabs" className={styles.tabList} contained={true}>
|
|
18
|
+
<Tab>{t('subscription', 'Subscription')} </Tab>
|
|
19
|
+
<Tab>{t('import', 'Import')} </Tab>
|
|
20
|
+
<Tab>{t('previousImports', 'Previous Imports')} </Tab>
|
|
20
21
|
</TabList>
|
|
21
22
|
<TabPanels>
|
|
22
23
|
<TabPanel className={styles.tabPanel}>
|
|
@@ -25,7 +26,9 @@ const Root: React.FC = () => {
|
|
|
25
26
|
<TabPanel className={styles.tabPanel}>
|
|
26
27
|
<Import />
|
|
27
28
|
</TabPanel>
|
|
28
|
-
<TabPanel className={styles.tabPanel}
|
|
29
|
+
<TabPanel className={styles.tabPanel}>
|
|
30
|
+
<PreviousImports />
|
|
31
|
+
</TabPanel>
|
|
29
32
|
</TabPanels>
|
|
30
33
|
</Tabs>
|
|
31
34
|
</main>
|
package/src/root.test.tsx
CHANGED
|
@@ -10,10 +10,10 @@ describe(`Root component`, () => {
|
|
|
10
10
|
|
|
11
11
|
it(`renders the title and tab containers`, () => {
|
|
12
12
|
renderOclSubscriptionModule();
|
|
13
|
-
expect(screen.getByText('
|
|
14
|
-
expect(screen.getByRole('tab', { name: '
|
|
15
|
-
expect(screen.getByRole('tab', { name: '
|
|
16
|
-
expect(screen.getByRole('tab', { name: '
|
|
13
|
+
expect(screen.getByText('OCL Subscription Module')).toBeInTheDocument();
|
|
14
|
+
expect(screen.getByRole('tab', { name: 'Subscription' })).toBeInTheDocument();
|
|
15
|
+
expect(screen.getByRole('tab', { name: 'Import' })).toBeInTheDocument();
|
|
16
|
+
expect(screen.getByRole('tab', { name: 'Previous Imports' })).toBeInTheDocument();
|
|
17
17
|
});
|
|
18
18
|
});
|
|
19
19
|
|
package/src/routes.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.openmrs.org/routes.schema.json",
|
|
3
|
+
"backendDependencies": {
|
|
4
|
+
"openconceptlab": ">=1.2.0",
|
|
5
|
+
"webservices.rest": "^2.2.0"
|
|
6
|
+
},
|
|
7
|
+
"extensions": [
|
|
8
|
+
{
|
|
9
|
+
"name": "admin-ocl-card-link",
|
|
10
|
+
"slot": "system-admin-page-card-link-slot",
|
|
11
|
+
"component": "adminOclCardLink"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"pages": [
|
|
15
|
+
{
|
|
16
|
+
"component": "root",
|
|
17
|
+
"route": "ocl"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { useSWRConfig } from 'swr';
|
|
2
3
|
import {
|
|
3
4
|
Button,
|
|
4
5
|
ButtonSkeleton,
|
|
@@ -13,11 +14,11 @@ import {
|
|
|
13
14
|
TextInput,
|
|
14
15
|
TextInputSkeleton,
|
|
15
16
|
} from '@carbon/react';
|
|
16
|
-
import React, { useCallback, useEffect, useState } from 'react';
|
|
17
17
|
import { useTranslation } from 'react-i18next';
|
|
18
|
+
import { showNotification } from '@openmrs/esm-framework';
|
|
18
19
|
import { deleteSubscription, updateSubscription, useSubscription } from './subscription.resource';
|
|
19
|
-
import
|
|
20
|
-
import
|
|
20
|
+
import { isVersionDefinedInUrl } from '../utils';
|
|
21
|
+
import styles from './subscription.scss';
|
|
21
22
|
|
|
22
23
|
const Subscription: React.FC = () => {
|
|
23
24
|
const { t } = useTranslation();
|
|
@@ -26,6 +27,7 @@ const Subscription: React.FC = () => {
|
|
|
26
27
|
const [token, setToken] = useState('');
|
|
27
28
|
const [isSubscribedToSnapshot, setIsSubscribedToSnapshot] = useState(false);
|
|
28
29
|
const [validationType, setValidationType] = useState<'NONE' | 'FULL'>('FULL');
|
|
30
|
+
const [isSnapshotOptionDisabled, setIsSnapshotOptionDisabled] = useState(false);
|
|
29
31
|
|
|
30
32
|
const { data: subscription, isLoading, isError } = useSubscription();
|
|
31
33
|
|
|
@@ -35,22 +37,29 @@ const Subscription: React.FC = () => {
|
|
|
35
37
|
setToken(subscription?.token || '');
|
|
36
38
|
setIsSubscribedToSnapshot(subscription?.subscribedToSnapshot || false);
|
|
37
39
|
setValidationType(subscription?.validationType || 'FULL');
|
|
40
|
+
setIsSnapshotOptionDisabled(subscription ? isVersionDefinedInUrl(subscription.url) : false);
|
|
38
41
|
}
|
|
39
42
|
}, [isLoading, isError, subscription]);
|
|
40
43
|
|
|
41
44
|
const handleChangeSubscriptionUrl = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
|
42
45
|
setSubscriptionUrl(event.target.value);
|
|
46
|
+
if (isVersionDefinedInUrl(event.target.value)) {
|
|
47
|
+
setIsSnapshotOptionDisabled(true);
|
|
48
|
+
setIsSubscribedToSnapshot(false);
|
|
49
|
+
} else {
|
|
50
|
+
setIsSnapshotOptionDisabled(false);
|
|
51
|
+
}
|
|
43
52
|
}, []);
|
|
44
53
|
|
|
45
54
|
const handleChangeToken = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
|
46
55
|
setToken(event.target.value);
|
|
47
56
|
}, []);
|
|
48
57
|
|
|
49
|
-
const handleChangeValidationType = useCallback((checked
|
|
58
|
+
const handleChangeValidationType = useCallback((event, { checked, id }) => {
|
|
50
59
|
setValidationType(checked ? 'NONE' : 'FULL');
|
|
51
60
|
}, []);
|
|
52
61
|
|
|
53
|
-
const handleChangeSubscriptionType = useCallback((checked
|
|
62
|
+
const handleChangeSubscriptionType = useCallback((event, { checked, id }) => {
|
|
54
63
|
setIsSubscribedToSnapshot(checked);
|
|
55
64
|
}, []);
|
|
56
65
|
|
|
@@ -59,6 +68,17 @@ const Subscription: React.FC = () => {
|
|
|
59
68
|
evt.preventDefault();
|
|
60
69
|
evt.stopPropagation();
|
|
61
70
|
|
|
71
|
+
if (isSnapshotOptionDisabled && isSubscribedToSnapshot) {
|
|
72
|
+
showNotification({
|
|
73
|
+
kind: 'error',
|
|
74
|
+
description: t(
|
|
75
|
+
'snapshotDisabledError',
|
|
76
|
+
"You cannot subscribe to a SNAPSHOT if you've provided the collection version",
|
|
77
|
+
),
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
62
82
|
const abortController = new AbortController();
|
|
63
83
|
|
|
64
84
|
const updatedSubscription = {
|
|
@@ -76,11 +96,14 @@ const Subscription: React.FC = () => {
|
|
|
76
96
|
if (response.ok) {
|
|
77
97
|
showNotification({
|
|
78
98
|
kind: 'success',
|
|
79
|
-
description: t(
|
|
99
|
+
description: t(
|
|
100
|
+
response.status === 201 ? 'subscriptionCreated' : 'subscriptionUpdated',
|
|
101
|
+
response.status === 201 ? 'Subscription created successfully' : 'Subscription updated successfully',
|
|
102
|
+
),
|
|
80
103
|
});
|
|
81
104
|
} else {
|
|
82
105
|
showNotification({
|
|
83
|
-
title: t('errorSavingSubscription'),
|
|
106
|
+
title: t('errorSavingSubscription', 'Error occured while saving the subscription'),
|
|
84
107
|
kind: 'error',
|
|
85
108
|
critical: true,
|
|
86
109
|
description: JSON.stringify(response.data),
|
|
@@ -89,7 +112,7 @@ const Subscription: React.FC = () => {
|
|
|
89
112
|
|
|
90
113
|
return () => abortController.abort();
|
|
91
114
|
},
|
|
92
|
-
[subscriptionUrl, token, validationType, isSubscribedToSnapshot,
|
|
115
|
+
[subscriptionUrl, token, validationType, isSubscribedToSnapshot, isSnapshotOptionDisabled, subscription, t, mutate],
|
|
93
116
|
);
|
|
94
117
|
|
|
95
118
|
const handleCancel = useCallback(() => {
|
|
@@ -97,10 +120,11 @@ const Subscription: React.FC = () => {
|
|
|
97
120
|
setToken(subscription?.token || '');
|
|
98
121
|
setIsSubscribedToSnapshot(subscription?.subscribedToSnapshot || false);
|
|
99
122
|
setValidationType(subscription?.validationType || 'FULL');
|
|
123
|
+
setIsSnapshotOptionDisabled(subscription ? isVersionDefinedInUrl(subscription.url) : false);
|
|
100
124
|
|
|
101
125
|
showNotification({
|
|
102
126
|
kind: 'info',
|
|
103
|
-
description: t('cancelledChanges'),
|
|
127
|
+
description: t('cancelledChanges', 'Cancelled changes successfully'),
|
|
104
128
|
});
|
|
105
129
|
}, [subscription, t]);
|
|
106
130
|
|
|
@@ -118,13 +142,14 @@ const Subscription: React.FC = () => {
|
|
|
118
142
|
setToken('');
|
|
119
143
|
setIsSubscribedToSnapshot(false);
|
|
120
144
|
setValidationType('FULL');
|
|
145
|
+
setIsSnapshotOptionDisabled(false);
|
|
121
146
|
showNotification({
|
|
122
147
|
kind: 'success',
|
|
123
|
-
description: t('subscriptionDeleted'),
|
|
148
|
+
description: t('subscriptionDeleted', 'Successfully unsubscribed'),
|
|
124
149
|
});
|
|
125
150
|
} else {
|
|
126
151
|
showNotification({
|
|
127
|
-
title: t('errorDeletingSubscription'),
|
|
152
|
+
title: t('errorDeletingSubscription', 'Error occured while deleting the subscription'),
|
|
128
153
|
kind: 'error',
|
|
129
154
|
critical: true,
|
|
130
155
|
description: JSON.stringify(response.data),
|
|
@@ -166,7 +191,7 @@ const Subscription: React.FC = () => {
|
|
|
166
191
|
if (isError) {
|
|
167
192
|
showNotification({
|
|
168
193
|
kind: 'error',
|
|
169
|
-
description: t('subscriptionError'),
|
|
194
|
+
description: t('subscriptionError', 'Error occured while fetching the subscription'),
|
|
170
195
|
});
|
|
171
196
|
}
|
|
172
197
|
|
|
@@ -174,12 +199,12 @@ const Subscription: React.FC = () => {
|
|
|
174
199
|
<Grid className={styles.grid}>
|
|
175
200
|
<Column sm={4} md={8} lg={10}>
|
|
176
201
|
<Form onSubmit={handleSubmit}>
|
|
177
|
-
<h3 className={styles.productiveHeading03}>{t('setupSubscription')}</h3>
|
|
202
|
+
<h3 className={styles.productiveHeading03}>{t('setupSubscription', 'Setup Subscription')}</h3>
|
|
178
203
|
<Stack gap={5}>
|
|
179
204
|
<TextInput
|
|
180
205
|
id="subscriptionUrl"
|
|
181
206
|
type="url"
|
|
182
|
-
labelText={t('subscriptionUrl')}
|
|
207
|
+
labelText={t('subscriptionUrl', 'Subscription URL')}
|
|
183
208
|
placeholder="https://api.openconceptlab.org/orgs/organization-name/collections/dictionary-name"
|
|
184
209
|
value={subscriptionUrl}
|
|
185
210
|
onChange={handleChangeSubscriptionUrl}
|
|
@@ -190,39 +215,48 @@ const Subscription: React.FC = () => {
|
|
|
190
215
|
id="apiToken"
|
|
191
216
|
type="password"
|
|
192
217
|
placeholder="••••••••••••••••••••••••••••••••••••••••••••••••"
|
|
193
|
-
labelText={t('apiToken')}
|
|
218
|
+
labelText={t('apiToken', 'Token')}
|
|
194
219
|
value={token}
|
|
195
220
|
onChange={handleChangeToken}
|
|
196
221
|
light={true}
|
|
197
222
|
required
|
|
198
223
|
/>
|
|
199
|
-
<FormGroup legendText={t('advancedOptions')} className={styles.formGroup}>
|
|
224
|
+
<FormGroup legendText={t('advancedOptions', 'Advanced Options')} className={styles.formGroup}>
|
|
200
225
|
<Checkbox
|
|
201
226
|
checked={isSubscribedToSnapshot}
|
|
202
227
|
onChange={handleChangeSubscriptionType}
|
|
203
|
-
labelText={t('subscribeToSnapshotText')}
|
|
228
|
+
labelText={t('subscribeToSnapshotText', 'Subscribe to SNAPSHOT versions (not recommended)')}
|
|
204
229
|
id="isSubscribedToSnapshot"
|
|
230
|
+
disabled={isSnapshotOptionDisabled}
|
|
205
231
|
/>
|
|
206
232
|
<Checkbox
|
|
207
233
|
checked={validationType === 'NONE'}
|
|
208
234
|
onChange={handleChangeValidationType}
|
|
209
|
-
labelText={t(
|
|
235
|
+
labelText={t(
|
|
236
|
+
'disableValidationText',
|
|
237
|
+
'Disable validation (should be used with care for well curated collections or sources)',
|
|
238
|
+
)}
|
|
210
239
|
id="isValidationDisabled"
|
|
211
240
|
/>
|
|
212
241
|
</FormGroup>
|
|
213
242
|
</Stack>
|
|
214
243
|
<Button kind="secondary" onClick={handleCancel}>
|
|
215
|
-
{t('cancelButton')}
|
|
244
|
+
{t('cancelButton', 'Cancel changes')}
|
|
216
245
|
</Button>
|
|
217
246
|
<Button kind="primary" type="submit">
|
|
218
|
-
{t('subscribeButton')}
|
|
247
|
+
{t('subscribeButton', 'Save changes')}
|
|
219
248
|
</Button>
|
|
220
249
|
</Form>
|
|
221
250
|
<Form onSubmit={handleUnsubscribe} className={styles.unsubscribeForm}>
|
|
222
|
-
<h3 className={styles.productiveHeading03}>{t('unsubscribe')}</h3>
|
|
223
|
-
<p className={styles.unsubscribeText}>
|
|
251
|
+
<h3 className={styles.productiveHeading03}>{t('unsubscribe', 'Unsubscribe')}</h3>
|
|
252
|
+
<p className={styles.unsubscribeText}>
|
|
253
|
+
{t(
|
|
254
|
+
'unsubscribeInfo',
|
|
255
|
+
'If you unsubscribe, no concepts will be deleted nor changed. All information about subscription will be deleted from your system.',
|
|
256
|
+
)}
|
|
257
|
+
</p>
|
|
224
258
|
<Button kind="danger" type="submit" disabled={!subscription}>
|
|
225
|
-
{t('unsubscribeButton')}
|
|
259
|
+
{t('unsubscribeButton', 'Unsubscribe')}
|
|
226
260
|
</Button>
|
|
227
261
|
</Form>
|
|
228
262
|
</Column>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { openmrsFetch } from '@openmrs/esm-framework';
|
|
2
2
|
import useSWR from 'swr';
|
|
3
|
-
import { Subscription } from '../types';
|
|
4
3
|
import isNil from 'lodash-es/isNil';
|
|
4
|
+
import type { Subscription } from '../types';
|
|
5
5
|
|
|
6
6
|
export function useSubscription() {
|
|
7
7
|
const { data, error, isValidating } = useSWR<{ data: { results: Subscription[] } }, Error>(
|