@openmrs/esm-sms-app 1.0.1-pre.22
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 +32 -0
- package/dist/123.js +2 -0
- package/dist/123.js.LICENSE.txt +9 -0
- package/dist/123.js.map +1 -0
- package/dist/144.js +2 -0
- package/dist/144.js.LICENSE.txt +19 -0
- package/dist/144.js.map +1 -0
- package/dist/160.js +1 -0
- package/dist/160.js.map +1 -0
- package/dist/173.js +1 -0
- package/dist/173.js.map +1 -0
- package/dist/197.js +1 -0
- package/dist/260.js +1 -0
- package/dist/260.js.map +1 -0
- package/dist/285.js +1 -0
- package/dist/285.js.map +1 -0
- package/dist/300.js +2 -0
- package/dist/300.js.LICENSE.txt +5 -0
- package/dist/300.js.map +1 -0
- package/dist/335.js +1 -0
- package/dist/356.js +1 -0
- package/dist/356.js.map +1 -0
- package/dist/372.js +1 -0
- package/dist/372.js.map +1 -0
- package/dist/378.js +1 -0
- package/dist/378.js.map +1 -0
- package/dist/41.js +2 -0
- package/dist/41.js.LICENSE.txt +9 -0
- package/dist/41.js.map +1 -0
- package/dist/449.js +1 -0
- package/dist/449.js.map +1 -0
- package/dist/465.js +1 -0
- package/dist/465.js.map +1 -0
- package/dist/470.js +2 -0
- package/dist/470.js.LICENSE.txt +9 -0
- package/dist/470.js.map +1 -0
- package/dist/495.js +1 -0
- package/dist/495.js.map +1 -0
- package/dist/55.js +1 -0
- package/dist/613.js +2 -0
- package/dist/613.js.LICENSE.txt +5 -0
- package/dist/613.js.map +1 -0
- package/dist/628.js +1 -0
- package/dist/628.js.map +1 -0
- package/dist/652.js +1 -0
- package/dist/733.js +2 -0
- package/dist/733.js.LICENSE.txt +30 -0
- package/dist/733.js.map +1 -0
- package/dist/830.js +1 -0
- package/dist/830.js.map +1 -0
- package/dist/831.js +2 -0
- package/dist/831.js.LICENSE.txt +5 -0
- package/dist/831.js.map +1 -0
- package/dist/876.js +2 -0
- package/dist/876.js.LICENSE.txt +9 -0
- package/dist/876.js.map +1 -0
- package/dist/919.js +1 -0
- package/dist/99.js +1 -0
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-sms-app.js +1 -0
- package/dist/openmrs-esm-sms-app.js.buildmanifest.json +807 -0
- package/dist/openmrs-esm-sms-app.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +3 -0
- package/package.json +55 -0
- package/src/api/providers.resource.ts +45 -0
- package/src/config-schema.ts +34 -0
- package/src/declarations.d.ts +4 -0
- package/src/hooks/useLogs.ts +28 -0
- package/src/hooks/useProviderConfigTemplates.ts +16 -0
- package/src/hooks/useProviderConfigurations.ts +32 -0
- package/src/index.ts +35 -0
- package/src/providers/add-config-form/provider-config-form.scss +118 -0
- package/src/providers/add-config-form/provider-config-form.test.tsx +252 -0
- package/src/providers/add-config-form/provider-config-form.workspace.tsx +347 -0
- package/src/providers/empty-state/empty-data-illustration.component.tsx +39 -0
- package/src/providers/empty-state/empty-state.component.tsx +41 -0
- package/src/providers/empty-state/empty-state.scss +55 -0
- package/src/providers/modals/config-upload.modal.tsx +170 -0
- package/src/providers/modals/config-upload.scss +33 -0
- package/src/providers/modals/remove-config.modal.tsx +69 -0
- package/src/providers/providers-dashboard.component.tsx +63 -0
- package/src/providers/providers-dashboard.scss +69 -0
- package/src/providers/providers-dashboard.test.tsx +63 -0
- package/src/providers/providers-overview/providers-action-menu.component.tsx +102 -0
- package/src/providers/providers-overview/providers-action-menu.scss +11 -0
- package/src/providers/providers-overview/providers-overview.component.tsx +306 -0
- package/src/providers/providers-overview/providers-overview.scss +114 -0
- package/src/providers/sms-logs-table/sms-logs-table.component.tsx +217 -0
- package/src/providers/sms-logs-table/sms-logs-table.scss +63 -0
- package/src/providers/sms-logs-table/sms-logs-table.test.tsx +103 -0
- package/src/providers/test-form/provider-config-test-form.scss +105 -0
- package/src/providers/test-form/provider-config-test-form.test.tsx +122 -0
- package/src/providers/test-form/provider-config-test-form.workspace.tsx +233 -0
- package/src/providers/utils/index.ts +57 -0
- package/src/root.scss +39 -0
- package/src/routes.json +44 -0
- package/src/sms-admin-card-link.component.tsx +24 -0
- package/src/types/index.ts +68 -0
- package/translations/am.json +4 -0
- package/translations/en.json +47 -0
- package/translations/es.json +4 -0
- package/translations/fr.json +4 -0
- package/translations/he.json +4 -0
- package/translations/km.json +4 -0
- package/tsconfig.json +5 -0
- package/webpack.config.js +1 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { screen } from '@testing-library/react';
|
|
3
|
+
import { userEvent } from '@testing-library/user-event';
|
|
4
|
+
import { renderWithSwr } from 'tools';
|
|
5
|
+
import ConfigTestForm from './provider-config-test-form.workspace';
|
|
6
|
+
import { openmrsFetch } from '@openmrs/esm-framework';
|
|
7
|
+
import { saveConfig, sendTestMessage } from '../../api/providers.resource';
|
|
8
|
+
|
|
9
|
+
jest.mock('zod', () => {
|
|
10
|
+
const originalModule = jest.requireActual('zod');
|
|
11
|
+
const mockedZod = {
|
|
12
|
+
...originalModule,
|
|
13
|
+
z: {
|
|
14
|
+
...originalModule.z,
|
|
15
|
+
schema: jest.fn(() => ({
|
|
16
|
+
safeParse: jest.fn(() => ({
|
|
17
|
+
success: true,
|
|
18
|
+
data: {},
|
|
19
|
+
})),
|
|
20
|
+
})),
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
return mockedZod;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
jest.mock('../../api/providers.resource', () => ({
|
|
27
|
+
sendTestMessage: jest.fn(),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
jest.mock('@openmrs/esm-framework', () => ({
|
|
31
|
+
openmrsFetch: jest.fn(),
|
|
32
|
+
showSnackbar: jest.fn(),
|
|
33
|
+
useLayoutType: () => 'desktop',
|
|
34
|
+
useConfig: jest.fn(() => ({ configurationPageSize: 10 })),
|
|
35
|
+
ResponsiveWrapper: ({ children }) => <div>{children}</div>,
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
jest.mock('../../hooks/useLogs', () => ({
|
|
39
|
+
useLogsRecords: jest.fn(() => ({ mutateLogs: jest.fn() })),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const mockOpenmrsFetch = openmrsFetch as jest.Mock;
|
|
43
|
+
const mockUseConfig = saveConfig as jest.Mock;
|
|
44
|
+
const mockSendTestMessage = sendTestMessage as jest.Mock;
|
|
45
|
+
|
|
46
|
+
describe('AddProviderConfigForm', () => {
|
|
47
|
+
it('Renders form fields correctly', async () => {
|
|
48
|
+
renderConfigTestForm();
|
|
49
|
+
const inputs = getFormInputs();
|
|
50
|
+
|
|
51
|
+
expect(inputs.deliveryTimeInput).toBeInTheDocument();
|
|
52
|
+
expect(inputs.recipientsInput).toBeInTheDocument();
|
|
53
|
+
expect(inputs.testMessageInput).toBeInTheDocument();
|
|
54
|
+
expect(inputs.customParamsInput).toBeInTheDocument();
|
|
55
|
+
|
|
56
|
+
const buttons = getFormButtons();
|
|
57
|
+
expect(buttons.cancelButton).toBeInTheDocument();
|
|
58
|
+
expect(buttons.cancelButton).not.toBeDisabled();
|
|
59
|
+
expect(buttons.submitButton).toBeInTheDocument();
|
|
60
|
+
expect(buttons.submitButton).not.toBeDisabled();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('sends a test message', async () => {
|
|
64
|
+
mockOpenmrsFetch.mockReturnValue(mockTestMessageResponse);
|
|
65
|
+
const user = userEvent.setup();
|
|
66
|
+
renderConfigTestForm();
|
|
67
|
+
const inputs = getFormInputs();
|
|
68
|
+
|
|
69
|
+
await fillFormInputs(user, inputs, {
|
|
70
|
+
deliveryTime: '0',
|
|
71
|
+
recipients: '1234567890',
|
|
72
|
+
testMessage: 'Hello world',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const buttons = getFormButtons();
|
|
76
|
+
await user.click(buttons.submitButton);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should show field errors when invalid data type is provided', async () => {
|
|
80
|
+
const user = userEvent.setup();
|
|
81
|
+
renderConfigTestForm();
|
|
82
|
+
|
|
83
|
+
const formbuttons = getFormButtons();
|
|
84
|
+
await user.click(formbuttons.submitButton);
|
|
85
|
+
|
|
86
|
+
expect(screen.getAllByText('Required')).toHaveLength(3);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
function renderConfigTestForm() {
|
|
91
|
+
const props = { providerName: 'test-provider' };
|
|
92
|
+
return renderWithSwr(<ConfigTestForm {...props} />);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getFormInputs() {
|
|
96
|
+
return {
|
|
97
|
+
deliveryTimeInput: screen.getByTestId('delivery-time'),
|
|
98
|
+
recipientsInput: screen.getByTestId('recipients'),
|
|
99
|
+
testMessageInput: screen.getByTestId('test-message'),
|
|
100
|
+
customParamsInput: screen.getByTestId('custom-params'),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getFormButtons() {
|
|
105
|
+
return {
|
|
106
|
+
cancelButton: screen.getByRole('button', { name: /Discard/i }),
|
|
107
|
+
submitButton: screen.getByRole('button', { name: /Send and close/i }),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function fillFormInputs(user, inputs, values) {
|
|
112
|
+
await user.click(inputs.deliveryTimeInput, values.deliveryTime);
|
|
113
|
+
await user.type(inputs.recipientsInput, values.recipients);
|
|
114
|
+
await user.type(inputs.testMessageInput, values.testMessage);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const mockTestMessageResponse = {
|
|
118
|
+
config: 'test-provider',
|
|
119
|
+
deliveryTime: '0',
|
|
120
|
+
recipients: '1234567890',
|
|
121
|
+
testMessage: 'Hello world',
|
|
122
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import classnames from 'classnames';
|
|
2
|
+
import React, { useCallback } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
RadioButton,
|
|
5
|
+
Stack,
|
|
6
|
+
Row,
|
|
7
|
+
RadioButtonGroup,
|
|
8
|
+
Column,
|
|
9
|
+
Form,
|
|
10
|
+
TextInput,
|
|
11
|
+
ButtonSet,
|
|
12
|
+
Button,
|
|
13
|
+
AccordionItem,
|
|
14
|
+
InlineLoading,
|
|
15
|
+
Accordion,
|
|
16
|
+
TextArea,
|
|
17
|
+
} from '@carbon/react';
|
|
18
|
+
import { ResponsiveWrapper, useLayoutType, showSnackbar, closeWorkspace } from '@openmrs/esm-framework';
|
|
19
|
+
import { useTranslation } from 'react-i18next';
|
|
20
|
+
import styles from './provider-config-test-form.scss';
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
import { Controller, useForm } from 'react-hook-form';
|
|
23
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
24
|
+
import { sendTestMessage } from '../../api/providers.resource';
|
|
25
|
+
import { useLogsRecords } from '../../hooks/useLogs';
|
|
26
|
+
|
|
27
|
+
const configTestFormSchema = z.object({
|
|
28
|
+
deliveryTime: z.number(),
|
|
29
|
+
recipients: z
|
|
30
|
+
.string()
|
|
31
|
+
.refine((val) => !val.includes('+'), {
|
|
32
|
+
message: 'Phone numbers should not contain the "+" sign.',
|
|
33
|
+
})
|
|
34
|
+
.refine((val) => val.split(',').every((num) => num.trim().length >= 10), {
|
|
35
|
+
message: 'Each phone number must have at least 10 digits.',
|
|
36
|
+
})
|
|
37
|
+
.refine((val) => val.split(',').every((num) => num.trim().length <= 15), {
|
|
38
|
+
message: 'Each phone number must have no more than 15 digits.',
|
|
39
|
+
})
|
|
40
|
+
.refine((val) => val.split(',').every((num) => /^[0-9\s\-()]+$/.test(num.trim())), {
|
|
41
|
+
message: 'Phone numbers can only contain digits, spaces, hyphens, and parentheses.',
|
|
42
|
+
}),
|
|
43
|
+
message: z.string(),
|
|
44
|
+
customParams: z.string().optional(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
type ConfigTestFormData = z.infer<typeof configTestFormSchema>;
|
|
48
|
+
|
|
49
|
+
interface ProviderConfigTestFormProps {
|
|
50
|
+
providerName: string;
|
|
51
|
+
}
|
|
52
|
+
const ProviderConfigTestForm: React.FC<ProviderConfigTestFormProps> = ({ providerName }) => {
|
|
53
|
+
const { t } = useTranslation();
|
|
54
|
+
const { mutateLogs } = useLogsRecords();
|
|
55
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
56
|
+
|
|
57
|
+
const {
|
|
58
|
+
handleSubmit,
|
|
59
|
+
control,
|
|
60
|
+
formState: { isSubmitting },
|
|
61
|
+
} = useForm<ConfigTestFormData>({
|
|
62
|
+
mode: 'all',
|
|
63
|
+
resolver: zodResolver(configTestFormSchema),
|
|
64
|
+
});
|
|
65
|
+
const deliveryTimes = [
|
|
66
|
+
{ key: 'immediately', labelText: 'Immediately', value: 0 },
|
|
67
|
+
{ key: 'after10secs', labelText: '10s', value: 10000 },
|
|
68
|
+
{ key: 'after1min', labelText: '1m', value: 60000 },
|
|
69
|
+
{ key: 'afterOneHour', labelText: '30s', value: 3600000 },
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const getFutureTime = (delay: number) => {
|
|
73
|
+
const currentTime = new Date();
|
|
74
|
+
const futureTime = new Date(currentTime.getTime() + delay);
|
|
75
|
+
return futureTime.toISOString();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const onSubmit = useCallback(
|
|
79
|
+
async (data: ConfigTestFormData) => {
|
|
80
|
+
const { deliveryTime, recipients, message, customParams } = data;
|
|
81
|
+
const futureTime = getFutureTime(deliveryTime);
|
|
82
|
+
const recipientsArray = recipients.split(',').map((num) => num.trim());
|
|
83
|
+
const payload = {
|
|
84
|
+
config: providerName,
|
|
85
|
+
deliveryTime: futureTime,
|
|
86
|
+
recipients: recipientsArray,
|
|
87
|
+
message,
|
|
88
|
+
customParams,
|
|
89
|
+
};
|
|
90
|
+
await sendTestMessage(payload)
|
|
91
|
+
.then(() => {
|
|
92
|
+
mutateLogs();
|
|
93
|
+
showSnackbar({
|
|
94
|
+
title: t('testMessageSent', 'Message sent'),
|
|
95
|
+
kind: 'success',
|
|
96
|
+
autoClose: true,
|
|
97
|
+
});
|
|
98
|
+
})
|
|
99
|
+
.catch((errror) =>
|
|
100
|
+
showSnackbar({
|
|
101
|
+
title: t('errorSendingMessage', 'Message not sent'),
|
|
102
|
+
subtitle: errror?.message,
|
|
103
|
+
kind: 'error',
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
[providerName, mutateLogs, t],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<Form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
|
|
112
|
+
<Stack gap={2} className={styles.formContainer}>
|
|
113
|
+
<Row className={styles.row}>
|
|
114
|
+
<Column sm={1}>
|
|
115
|
+
<span className={styles.columnLabel}>{t('deliveryTime', 'Delivery Time')}</span>
|
|
116
|
+
</Column>
|
|
117
|
+
<Column sm={3}>
|
|
118
|
+
<Controller
|
|
119
|
+
name="deliveryTime"
|
|
120
|
+
control={control}
|
|
121
|
+
render={({ field, fieldState: { error } }) => (
|
|
122
|
+
<RadioButtonGroup
|
|
123
|
+
{...field}
|
|
124
|
+
invalid={!!error?.message}
|
|
125
|
+
invalidText={error?.message}
|
|
126
|
+
className={styles.radioButtonGroup}
|
|
127
|
+
orientation="vertical"
|
|
128
|
+
data-testid="delivery-time"
|
|
129
|
+
>
|
|
130
|
+
{deliveryTimes.map((deliveryTime) => (
|
|
131
|
+
<RadioButton
|
|
132
|
+
key={deliveryTime.key}
|
|
133
|
+
labelText={deliveryTime.labelText}
|
|
134
|
+
value={deliveryTime.value}
|
|
135
|
+
id={deliveryTime.key}
|
|
136
|
+
/>
|
|
137
|
+
))}
|
|
138
|
+
</RadioButtonGroup>
|
|
139
|
+
)}
|
|
140
|
+
/>
|
|
141
|
+
</Column>
|
|
142
|
+
</Row>
|
|
143
|
+
<Row className={styles.row}>
|
|
144
|
+
<Column sm={1}>
|
|
145
|
+
<span className={styles.columnLabel}>{t('recipients', 'Recipient(s)')}</span>
|
|
146
|
+
</Column>
|
|
147
|
+
<Column sm={3}>
|
|
148
|
+
<ResponsiveWrapper>
|
|
149
|
+
<Controller
|
|
150
|
+
name="recipients"
|
|
151
|
+
control={control}
|
|
152
|
+
render={({ field, fieldState: { error } }) => (
|
|
153
|
+
<TextInput
|
|
154
|
+
{...field}
|
|
155
|
+
invalid={!!error?.message}
|
|
156
|
+
invalidText={error?.message}
|
|
157
|
+
labelText={t('multipleRecipientsNotice', 'Separate multiple numbers with comma')}
|
|
158
|
+
placeholder={t('placeHolder', 'Enter phone +123455690, +09348...')}
|
|
159
|
+
data-testid="recipients"
|
|
160
|
+
/>
|
|
161
|
+
)}
|
|
162
|
+
/>
|
|
163
|
+
</ResponsiveWrapper>
|
|
164
|
+
</Column>
|
|
165
|
+
</Row>
|
|
166
|
+
<Row className={styles.row}>
|
|
167
|
+
<Column sm={1}>
|
|
168
|
+
<span className={styles.columnLabel}>{t('message', 'Message')}</span>
|
|
169
|
+
</Column>
|
|
170
|
+
<Column sm={3}>
|
|
171
|
+
<ResponsiveWrapper>
|
|
172
|
+
<Controller
|
|
173
|
+
name="message"
|
|
174
|
+
control={control}
|
|
175
|
+
render={({ field, fieldState: { error } }) => (
|
|
176
|
+
<TextArea
|
|
177
|
+
{...field}
|
|
178
|
+
invalid={!!error?.message}
|
|
179
|
+
invalidText={error?.message}
|
|
180
|
+
labelText={t('writeTestMessage', 'Write your test message')}
|
|
181
|
+
placeholder={t('writeTestMessage', 'Write your test message')}
|
|
182
|
+
data-testid="test-message"
|
|
183
|
+
/>
|
|
184
|
+
)}
|
|
185
|
+
/>
|
|
186
|
+
</ResponsiveWrapper>
|
|
187
|
+
</Column>
|
|
188
|
+
</Row>
|
|
189
|
+
<Row className={styles.row}>
|
|
190
|
+
<ResponsiveWrapper>
|
|
191
|
+
<Accordion>
|
|
192
|
+
<AccordionItem title={t('advanced', 'Advanced')}>
|
|
193
|
+
<Column sm={1}>
|
|
194
|
+
<span className={styles.columnLabel}>{t('customParameters', 'Custom params (optional)')}</span>
|
|
195
|
+
</Column>
|
|
196
|
+
<Column sm={3}>
|
|
197
|
+
<ResponsiveWrapper>
|
|
198
|
+
<Controller
|
|
199
|
+
name="customParams"
|
|
200
|
+
control={control}
|
|
201
|
+
render={({ field }) => (
|
|
202
|
+
<TextArea
|
|
203
|
+
{...field}
|
|
204
|
+
labelText={t('mapCustomParams', 'Map custom params in key:value format')}
|
|
205
|
+
placeholder={t('enterCustomParams', 'Enter custom parameters. Use new line as separator')}
|
|
206
|
+
data-testid="custom-params"
|
|
207
|
+
/>
|
|
208
|
+
)}
|
|
209
|
+
/>
|
|
210
|
+
</ResponsiveWrapper>
|
|
211
|
+
</Column>
|
|
212
|
+
</AccordionItem>
|
|
213
|
+
</Accordion>
|
|
214
|
+
</ResponsiveWrapper>
|
|
215
|
+
</Row>
|
|
216
|
+
</Stack>
|
|
217
|
+
<ButtonSet className={classnames({ [styles.tablet]: isTablet, [styles.desktop]: !isTablet })}>
|
|
218
|
+
<Button className={styles.button} kind="secondary" onClick={() => closeWorkspace('provider-config-test-form')}>
|
|
219
|
+
{t('discard', 'Discard')}
|
|
220
|
+
</Button>
|
|
221
|
+
<Button className={styles.button} kind="primary" type="submit" disabled={isSubmitting}>
|
|
222
|
+
{isSubmitting ? (
|
|
223
|
+
<InlineLoading description={t('sending', 'Sending') + '...'} />
|
|
224
|
+
) : (
|
|
225
|
+
<span>{t('sendAndClose', 'Send and close')}</span>
|
|
226
|
+
)}
|
|
227
|
+
</Button>
|
|
228
|
+
</ButtonSet>
|
|
229
|
+
</Form>
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export default ProviderConfigTestForm;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import { type ConfigurationTableDataRow, type ConfigurationTableHeader } from '../../types';
|
|
3
|
+
|
|
4
|
+
export function readFileAsString(file: File) {
|
|
5
|
+
return new Promise<string>((resolve) => {
|
|
6
|
+
if (file) {
|
|
7
|
+
const reader = new FileReader();
|
|
8
|
+
|
|
9
|
+
reader.addEventListener('load', () => {
|
|
10
|
+
if (typeof reader.result === 'string') {
|
|
11
|
+
resolve(reader.result);
|
|
12
|
+
} else {
|
|
13
|
+
resolve('');
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
reader.addEventListener('error', () => {
|
|
18
|
+
resolve('');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
reader.readAsDataURL(file);
|
|
22
|
+
} else {
|
|
23
|
+
resolve('');
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useConfigurationsSorting(
|
|
29
|
+
tableHeaders: Array<ConfigurationTableHeader>,
|
|
30
|
+
tableRows: Array<ConfigurationTableDataRow>,
|
|
31
|
+
) {
|
|
32
|
+
const [sortParams, setSortParams] = useState<{
|
|
33
|
+
key: ConfigurationTableHeader['key'] | '';
|
|
34
|
+
sortDirection: 'ASC' | 'DESC' | 'NONE';
|
|
35
|
+
}>({ key: '', sortDirection: 'NONE' });
|
|
36
|
+
const sortRow = (cellA, cellB, { key, sortDirection }) => {
|
|
37
|
+
setSortParams({ key, sortDirection });
|
|
38
|
+
};
|
|
39
|
+
const sortedRows = useMemo(() => {
|
|
40
|
+
if (sortParams.sortDirection === 'NONE') {
|
|
41
|
+
return tableRows;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { key, sortDirection } = sortParams;
|
|
45
|
+
const tableHeader = tableHeaders.find((h) => h.key === key);
|
|
46
|
+
|
|
47
|
+
return tableRows?.slice().sort((a, b) => {
|
|
48
|
+
const sortingNum = tableHeader.sortFunc(a, b);
|
|
49
|
+
return sortDirection === 'DESC' ? sortingNum : -sortingNum;
|
|
50
|
+
});
|
|
51
|
+
}, [sortParams, tableRows, tableHeaders]);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
sortedRows,
|
|
55
|
+
sortRow,
|
|
56
|
+
};
|
|
57
|
+
}
|
package/src/root.scss
ADDED
|
@@ -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
|
+
.productiveHeading01 {
|
|
6
|
+
@include type.type-style("heading-01");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.productiveHeading02 {
|
|
10
|
+
@include type.type-style("heading-02");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.productiveHeading03 {
|
|
14
|
+
@include type.type-style("heading-03");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.productiveHeading04 {
|
|
18
|
+
@include type.type-style("heading-04");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.bodyLong01 {
|
|
22
|
+
@include type.type-style("body-01");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.bodyShort01 {
|
|
26
|
+
@include type.type-style("body-compact-01");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.bodyShort02 {
|
|
30
|
+
@include type.type-style("body-compact-02");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.text02 {
|
|
34
|
+
color: $text-02;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.label01 {
|
|
38
|
+
@include type.type-style("label-01")
|
|
39
|
+
}
|
package/src/routes.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.openmrs.org/routes.schema.json",
|
|
3
|
+
"backendDependencies": {},
|
|
4
|
+
"extensions": [
|
|
5
|
+
{
|
|
6
|
+
"name": "sms-providers-settings-admin-card-link",
|
|
7
|
+
"component": "smsAdminCardLink",
|
|
8
|
+
"slot": "system-admin-page-card-link-slot",
|
|
9
|
+
"online": true,
|
|
10
|
+
"offline": true
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"workspaces": [
|
|
14
|
+
{
|
|
15
|
+
"name": "add-provider-config-form",
|
|
16
|
+
"component": "addProviderConfigForm",
|
|
17
|
+
"title": "addProviderConfigWorkspaceTitle"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "provider-config-test-form",
|
|
21
|
+
"component": "providerConfigTestForm",
|
|
22
|
+
"title": "providerConfigTestWorkspaceTitle"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"pages": [
|
|
26
|
+
{
|
|
27
|
+
"component": "providersDashboard",
|
|
28
|
+
"route": "provider-settings",
|
|
29
|
+
"online": true,
|
|
30
|
+
"offline": true,
|
|
31
|
+
"order": 1
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"modals": [
|
|
35
|
+
{
|
|
36
|
+
"name": "config-upload-modal",
|
|
37
|
+
"component": "configUploadModal"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "remove-config-modal",
|
|
41
|
+
"component": "removeConfigModal"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Layer, ClickableTile } from '@carbon/react';
|
|
4
|
+
import { ArrowRight } from '@carbon/react/icons';
|
|
5
|
+
|
|
6
|
+
const SMSAdminCardLink: React.FC = () => {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
const header = t('smsConfiguration', 'SMS Configuration');
|
|
9
|
+
return (
|
|
10
|
+
<Layer>
|
|
11
|
+
<ClickableTile href={window.spaBase + '/provider-settings'} rel="noopener noreferrer" target="_blank">
|
|
12
|
+
<div>
|
|
13
|
+
<div className="heading">{header}</div>
|
|
14
|
+
<div className="content">{t('smsProviderSettings', 'SMS Provider Settings')}</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div className="iconWrapper">
|
|
17
|
+
<ArrowRight size={16} />
|
|
18
|
+
</div>
|
|
19
|
+
</ClickableTile>
|
|
20
|
+
</Layer>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default SMSAdminCardLink;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface ProviderConfiguration {
|
|
2
|
+
name: string;
|
|
3
|
+
maxRetries: number;
|
|
4
|
+
excludeLastFooter: boolean;
|
|
5
|
+
splitHeader: string;
|
|
6
|
+
splitFooter: string;
|
|
7
|
+
templateName: string;
|
|
8
|
+
props: Array<Prop>;
|
|
9
|
+
automaticResponseScript: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type Prop = {
|
|
13
|
+
name: string;
|
|
14
|
+
value: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface ProviderConfigurationsResponse {
|
|
18
|
+
defaultConfigName: string;
|
|
19
|
+
configs: Array<ProviderConfiguration>;
|
|
20
|
+
defaultConfig: ProviderConfiguration;
|
|
21
|
+
empty: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ConfigurationTableHeader {
|
|
25
|
+
key: string;
|
|
26
|
+
header: string;
|
|
27
|
+
isSortable: boolean;
|
|
28
|
+
sortFunc: (valueA: any, valueB: any) => any;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ConfigurationTableDataRow extends ProviderConfiguration {
|
|
32
|
+
configuration: string;
|
|
33
|
+
id: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SMSLog {
|
|
37
|
+
id: number;
|
|
38
|
+
errorMessage: string;
|
|
39
|
+
providerStatus: string;
|
|
40
|
+
openMrsId: string;
|
|
41
|
+
providerId: string;
|
|
42
|
+
deliveryStatus: string;
|
|
43
|
+
messageContent: string;
|
|
44
|
+
timestamp: string;
|
|
45
|
+
config: string;
|
|
46
|
+
smsDirection: string;
|
|
47
|
+
phoneNumber: string;
|
|
48
|
+
modificationDate: string;
|
|
49
|
+
creationDate: string;
|
|
50
|
+
modifiedBy: string;
|
|
51
|
+
creator: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface SMSLogsResponse {
|
|
55
|
+
pageIndex: number;
|
|
56
|
+
pageSize: number;
|
|
57
|
+
totalRecords: number;
|
|
58
|
+
rows: Array<SMSLog>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ServiceConfigTemplate {
|
|
62
|
+
name: string;
|
|
63
|
+
configurables: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ServiceConfigTemplates {
|
|
67
|
+
[key: string]: ServiceConfigTemplate;
|
|
68
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"add": "Add",
|
|
3
|
+
"addProviderConfigWorkspaceTitle": "Add Provider",
|
|
4
|
+
"config": "Configuration",
|
|
5
|
+
"configDetails": "Configuration details",
|
|
6
|
+
"configuration": "Configuration",
|
|
7
|
+
"configurations": "Configurations",
|
|
8
|
+
"creationDate": "Creation date",
|
|
9
|
+
"creator": "Creator",
|
|
10
|
+
"default": "Default",
|
|
11
|
+
"defaultConfigUpdated": "Default config updated",
|
|
12
|
+
"deliveryStatus": "Delivery status",
|
|
13
|
+
"edit": "Edit",
|
|
14
|
+
"editConfig": "Edit {{configName}}",
|
|
15
|
+
"emptyTableStateText": "There are no {{displayText}} to display",
|
|
16
|
+
"errorMakingDefault": "Failed to update default config",
|
|
17
|
+
"errorMessage": "Error message",
|
|
18
|
+
"fitlerLogsByNumber": "Fitler logs by phone number",
|
|
19
|
+
"importConfig": "Import config template",
|
|
20
|
+
"logs": "Logs",
|
|
21
|
+
"maxRetries": "Maximum retries",
|
|
22
|
+
"messageContent": "Message",
|
|
23
|
+
"modificationDate": "Modification date",
|
|
24
|
+
"modifiedBy": "Modified by",
|
|
25
|
+
"noConfigurationsToDisplay": "No configurations to display",
|
|
26
|
+
"noSmsLogsToDisplay": "No SMS Logs to display",
|
|
27
|
+
"openMrsId": "Openmrs ID",
|
|
28
|
+
"phoneNumber": "Phone number",
|
|
29
|
+
"providerId": "Provider",
|
|
30
|
+
"providerName": "Provider name",
|
|
31
|
+
"providers": "Providers",
|
|
32
|
+
"providerStatus": "Provider status",
|
|
33
|
+
"record": "Record",
|
|
34
|
+
"remove": "Remove",
|
|
35
|
+
"setDefault": "Set as default",
|
|
36
|
+
"smsConfiguration": "SMS Configuration",
|
|
37
|
+
"smsDirection": "SMS direction",
|
|
38
|
+
"smsLogs": "SMS Logs",
|
|
39
|
+
"smsProviderSettings": "SMS Provider Settings",
|
|
40
|
+
"splitFooter": "Footer split text",
|
|
41
|
+
"splitHeader": "Header split text",
|
|
42
|
+
"template": "Template",
|
|
43
|
+
"templateName": "Template name",
|
|
44
|
+
"test": "Test",
|
|
45
|
+
"testConfig": "Test {{ configName }}",
|
|
46
|
+
"timestamp": "Timestamp"
|
|
47
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('openmrs/default-webpack-config');
|