@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,347 @@
|
|
|
1
|
+
import classnames from 'classnames';
|
|
2
|
+
import {
|
|
3
|
+
Stack,
|
|
4
|
+
Column,
|
|
5
|
+
Form,
|
|
6
|
+
TextInput,
|
|
7
|
+
Row,
|
|
8
|
+
ButtonSet,
|
|
9
|
+
Button,
|
|
10
|
+
AccordionItem,
|
|
11
|
+
TextArea,
|
|
12
|
+
SelectSkeleton,
|
|
13
|
+
Select,
|
|
14
|
+
SelectItem,
|
|
15
|
+
NumberInput,
|
|
16
|
+
Checkbox,
|
|
17
|
+
InlineLoading,
|
|
18
|
+
Accordion,
|
|
19
|
+
} from '@carbon/react';
|
|
20
|
+
import React, { useEffect } from 'react';
|
|
21
|
+
import { useTranslation } from 'react-i18next';
|
|
22
|
+
import { ResponsiveWrapper, closeWorkspace, showSnackbar, useLayoutType } from '@openmrs/esm-framework';
|
|
23
|
+
import { useProviderConfigTemplates } from '../../hooks/useProviderConfigTemplates';
|
|
24
|
+
import { z } from 'zod';
|
|
25
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
26
|
+
import { Controller, useForm } from 'react-hook-form';
|
|
27
|
+
import { type Prop, type ProviderConfiguration } from '../../types';
|
|
28
|
+
import { saveConfig } from '../../api/providers.resource';
|
|
29
|
+
import { useProviderConfigurations } from '../../hooks/useProviderConfigurations';
|
|
30
|
+
import styles from './provider-config-form.scss';
|
|
31
|
+
|
|
32
|
+
interface AddProviderConfigProps {
|
|
33
|
+
providerName?: string;
|
|
34
|
+
templateName?: string;
|
|
35
|
+
maxRetries?: number;
|
|
36
|
+
splitHeader?: string;
|
|
37
|
+
splitFooter?: string;
|
|
38
|
+
excludeLastFooter?: boolean;
|
|
39
|
+
autoScript?: string;
|
|
40
|
+
props?: Array<Prop>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const addProviderConfigFormSchema = z.object({
|
|
44
|
+
providerName: z.string(),
|
|
45
|
+
templateName: z.string(),
|
|
46
|
+
maxRetries: z.number(),
|
|
47
|
+
splitHeader: z.string(),
|
|
48
|
+
splitFooter: z.string(),
|
|
49
|
+
excludeLastFooter: z.boolean(),
|
|
50
|
+
autoScript: z.string().optional(),
|
|
51
|
+
dynamicFields: z.record(z.string(), z.string()),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
type AddProviderConfigFormData = z.infer<typeof addProviderConfigFormSchema>;
|
|
55
|
+
|
|
56
|
+
const AddProviderConfigForm: React.FC<AddProviderConfigProps> = ({
|
|
57
|
+
providerName,
|
|
58
|
+
templateName,
|
|
59
|
+
excludeLastFooter,
|
|
60
|
+
splitFooter,
|
|
61
|
+
splitHeader,
|
|
62
|
+
maxRetries,
|
|
63
|
+
autoScript,
|
|
64
|
+
props,
|
|
65
|
+
}) => {
|
|
66
|
+
const { t } = useTranslation();
|
|
67
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
68
|
+
const { mutateConfigs, providerConfigurations } = useProviderConfigurations();
|
|
69
|
+
const { templates, isLoadingTemplates } = useProviderConfigTemplates();
|
|
70
|
+
|
|
71
|
+
const {
|
|
72
|
+
handleSubmit,
|
|
73
|
+
control,
|
|
74
|
+
watch,
|
|
75
|
+
setValue,
|
|
76
|
+
formState: { isSubmitting, isDirty },
|
|
77
|
+
} = useForm<AddProviderConfigFormData>({
|
|
78
|
+
mode: 'all',
|
|
79
|
+
resolver: zodResolver(addProviderConfigFormSchema),
|
|
80
|
+
defaultValues: {
|
|
81
|
+
providerName: providerName ? providerName : undefined,
|
|
82
|
+
templateName: templateName ? templateName : undefined,
|
|
83
|
+
maxRetries: maxRetries ? maxRetries : 0,
|
|
84
|
+
splitHeader: splitHeader ? splitHeader : undefined,
|
|
85
|
+
splitFooter: splitFooter ? splitFooter : undefined,
|
|
86
|
+
excludeLastFooter: excludeLastFooter ? excludeLastFooter : false,
|
|
87
|
+
autoScript: autoScript ? autoScript : undefined,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const onSubmit = async (data: AddProviderConfigFormData) => {
|
|
92
|
+
if (!isDirty) {
|
|
93
|
+
return closeForm();
|
|
94
|
+
}
|
|
95
|
+
const props = Object.entries(data.dynamicFields).map(([key, value]) => ({ name: key, value: value }));
|
|
96
|
+
const { providerName, dynamicFields, ...rest } = data;
|
|
97
|
+
const payload = {
|
|
98
|
+
...rest,
|
|
99
|
+
props,
|
|
100
|
+
name: providerName,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
await saveConfig([...providerConfigurations, payload as ProviderConfiguration])
|
|
104
|
+
.then(() => {
|
|
105
|
+
showSnackbar({
|
|
106
|
+
title: t('configSaved', 'Configuration saved'),
|
|
107
|
+
kind: 'success',
|
|
108
|
+
});
|
|
109
|
+
mutateConfigs();
|
|
110
|
+
closeForm();
|
|
111
|
+
})
|
|
112
|
+
.catch(() =>
|
|
113
|
+
showSnackbar({
|
|
114
|
+
title: t('configNotSaved', 'Configuration not saved'),
|
|
115
|
+
kind: 'error',
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const selectedTemplate = templates?.[watch('templateName')] ?? null;
|
|
121
|
+
|
|
122
|
+
const closeForm = () => closeWorkspace('add-provider-config-form');
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (props) {
|
|
126
|
+
props.forEach(({ name, value }) => {
|
|
127
|
+
setValue(`dynamicFields.${name}`, value);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}, [props, setValue]);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<Form data-testid="add-provider-form" className={styles.form} onSubmit={handleSubmit(onSubmit)}>
|
|
134
|
+
<Stack gap={2} className={styles.formContainer}>
|
|
135
|
+
<Row className={styles.row}>
|
|
136
|
+
<Column sm={1}>
|
|
137
|
+
<span className={styles.columnLabel}>{t('providerName', ' Provider Name')}</span>
|
|
138
|
+
</Column>
|
|
139
|
+
<Column sm={3}>
|
|
140
|
+
<ResponsiveWrapper>
|
|
141
|
+
<Controller
|
|
142
|
+
name="providerName"
|
|
143
|
+
control={control}
|
|
144
|
+
render={({ field, fieldState: { error } }) => (
|
|
145
|
+
<TextInput
|
|
146
|
+
{...field}
|
|
147
|
+
data-testid="provider-name"
|
|
148
|
+
invalid={!!error?.message}
|
|
149
|
+
invalidText={error?.message}
|
|
150
|
+
labelText={t('nameOfProvider', 'Name of the provider')}
|
|
151
|
+
placeholder={t('namePlaceHolder', 'Enter provider name e.g Twilio')}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
/>
|
|
155
|
+
</ResponsiveWrapper>
|
|
156
|
+
</Column>
|
|
157
|
+
</Row>
|
|
158
|
+
<Row className={styles.row}>
|
|
159
|
+
<Column sm={1}>
|
|
160
|
+
<span className={styles.columnLabel}>{t('template', 'Template')}</span>
|
|
161
|
+
</Column>
|
|
162
|
+
<Column sm={3}>
|
|
163
|
+
<ResponsiveWrapper>
|
|
164
|
+
{isLoadingTemplates ? (
|
|
165
|
+
<SelectSkeleton />
|
|
166
|
+
) : (
|
|
167
|
+
<Controller
|
|
168
|
+
name="templateName"
|
|
169
|
+
control={control}
|
|
170
|
+
render={({ field, fieldState: { error } }) => (
|
|
171
|
+
<Select
|
|
172
|
+
{...field}
|
|
173
|
+
data-testid="select-template"
|
|
174
|
+
labelText={t('selectTemplate', 'Select a template')}
|
|
175
|
+
invalid={!!error?.message}
|
|
176
|
+
invalidText={error?.message}
|
|
177
|
+
>
|
|
178
|
+
<SelectItem value={''} text={t('chooseTemplate', 'Choose a template')} />
|
|
179
|
+
{Object.keys(templates).map((value) => (
|
|
180
|
+
<SelectItem value={value} text={value} />
|
|
181
|
+
))}
|
|
182
|
+
</Select>
|
|
183
|
+
)}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
</ResponsiveWrapper>
|
|
187
|
+
</Column>
|
|
188
|
+
</Row>
|
|
189
|
+
<Row className={styles.row}>
|
|
190
|
+
<Column sm={1}>
|
|
191
|
+
<span className={styles.columnLabel}>{t('maximumRetries', 'Maximum retries')}</span>
|
|
192
|
+
</Column>
|
|
193
|
+
<Column sm={3}>
|
|
194
|
+
<ResponsiveWrapper>
|
|
195
|
+
<Controller
|
|
196
|
+
name="maxRetries"
|
|
197
|
+
control={control}
|
|
198
|
+
render={({ field, fieldState: { error } }) => (
|
|
199
|
+
<NumberInput
|
|
200
|
+
{...field}
|
|
201
|
+
data-testid="numberOfRetries"
|
|
202
|
+
invalid={!!error?.message}
|
|
203
|
+
invalidText={error?.message}
|
|
204
|
+
onChange={(_, { value }) => field.onChange(Number(value))}
|
|
205
|
+
labelText={t('numberOfRetries', 'Number of retries')}
|
|
206
|
+
/>
|
|
207
|
+
)}
|
|
208
|
+
/>
|
|
209
|
+
</ResponsiveWrapper>
|
|
210
|
+
</Column>
|
|
211
|
+
</Row>
|
|
212
|
+
<Row className={styles.row}>
|
|
213
|
+
<Column sm={1}>
|
|
214
|
+
<span className={styles.columnLabel}>{t('headerSplitMessage', 'Header split message')}</span>
|
|
215
|
+
</Column>
|
|
216
|
+
<Column sm={3}>
|
|
217
|
+
<ResponsiveWrapper>
|
|
218
|
+
<Controller
|
|
219
|
+
name="splitHeader"
|
|
220
|
+
control={control}
|
|
221
|
+
render={({ field, fieldState: { error } }) => (
|
|
222
|
+
<TextInput
|
|
223
|
+
data-testid="split-header"
|
|
224
|
+
invalid={!!error?.message}
|
|
225
|
+
invalidText={error?.message}
|
|
226
|
+
{...field}
|
|
227
|
+
labelText={t('headerSplitText', 'Header split text')}
|
|
228
|
+
/>
|
|
229
|
+
)}
|
|
230
|
+
/>
|
|
231
|
+
</ResponsiveWrapper>
|
|
232
|
+
</Column>
|
|
233
|
+
</Row>
|
|
234
|
+
<Row className={styles.row}>
|
|
235
|
+
<Column sm={1}>
|
|
236
|
+
<span className={styles.columnLabel}>{t('footerSplitMessage', 'Footer split message')}</span>
|
|
237
|
+
</Column>
|
|
238
|
+
<Column sm={3}>
|
|
239
|
+
<ResponsiveWrapper>
|
|
240
|
+
<Controller
|
|
241
|
+
name="splitFooter"
|
|
242
|
+
control={control}
|
|
243
|
+
render={({ field, fieldState: { error } }) => (
|
|
244
|
+
<TextInput
|
|
245
|
+
{...field}
|
|
246
|
+
data-testid="split-footer"
|
|
247
|
+
invalid={!!error?.message}
|
|
248
|
+
invalidText={error?.message}
|
|
249
|
+
labelText={t('footerSplitText', 'Footers split text')}
|
|
250
|
+
/>
|
|
251
|
+
)}
|
|
252
|
+
/>
|
|
253
|
+
</ResponsiveWrapper>
|
|
254
|
+
</Column>
|
|
255
|
+
</Row>
|
|
256
|
+
<Row className={styles.row}>
|
|
257
|
+
<Column sm={3}>
|
|
258
|
+
<ResponsiveWrapper>
|
|
259
|
+
<Controller
|
|
260
|
+
name="excludeLastFooter"
|
|
261
|
+
control={control}
|
|
262
|
+
render={({ field }) => (
|
|
263
|
+
<Checkbox
|
|
264
|
+
data-testid="exclude-last-footer"
|
|
265
|
+
id={field.name}
|
|
266
|
+
name={field.name}
|
|
267
|
+
checked={field.value}
|
|
268
|
+
onChange={(_, { checked }) => setValue('excludeLastFooter', checked)}
|
|
269
|
+
onBlur={field.onBlur}
|
|
270
|
+
className={styles.checkBox}
|
|
271
|
+
labelText={t('excludeLastFooter', 'Exclude footer from last split message')}
|
|
272
|
+
/>
|
|
273
|
+
)}
|
|
274
|
+
/>
|
|
275
|
+
</ResponsiveWrapper>
|
|
276
|
+
</Column>
|
|
277
|
+
</Row>
|
|
278
|
+
<Row className={styles.row}>
|
|
279
|
+
<Accordion>
|
|
280
|
+
<AccordionItem title={t('advanced', 'Advanced')}>
|
|
281
|
+
<Column sm={1}>
|
|
282
|
+
<span className={styles.columnLabel}>
|
|
283
|
+
{t('automaticResponseScript', 'Automatic response script (Optional)')}
|
|
284
|
+
</span>
|
|
285
|
+
</Column>
|
|
286
|
+
<Column sm={3}>
|
|
287
|
+
<ResponsiveWrapper>
|
|
288
|
+
<Controller
|
|
289
|
+
name="autoScript"
|
|
290
|
+
control={control}
|
|
291
|
+
render={({ field, fieldState: { error } }) => (
|
|
292
|
+
<TextArea
|
|
293
|
+
{...field}
|
|
294
|
+
data-testid="auto-script"
|
|
295
|
+
invalid={!!error?.message}
|
|
296
|
+
invalidText={error?.message}
|
|
297
|
+
labelText={t('writeYourScript', 'Write your script')}
|
|
298
|
+
placeholder={t('enterAutomaticScript', 'Enter your script')}
|
|
299
|
+
/>
|
|
300
|
+
)}
|
|
301
|
+
/>
|
|
302
|
+
</ResponsiveWrapper>
|
|
303
|
+
</Column>
|
|
304
|
+
</AccordionItem>
|
|
305
|
+
</Accordion>
|
|
306
|
+
</Row>
|
|
307
|
+
{(() => {
|
|
308
|
+
if (selectedTemplate) {
|
|
309
|
+
return selectedTemplate.configurables.map((dataField, index) => (
|
|
310
|
+
<Row className={styles.dynamicRow} key={index}>
|
|
311
|
+
<ResponsiveWrapper>
|
|
312
|
+
<Controller
|
|
313
|
+
name={`dynamicFields.${dataField}`}
|
|
314
|
+
control={control}
|
|
315
|
+
render={({ field, fieldState: { error } }) => (
|
|
316
|
+
<TextInput
|
|
317
|
+
{...field}
|
|
318
|
+
invalid={!!error?.message}
|
|
319
|
+
invalidText={error?.message}
|
|
320
|
+
labelText={dataField}
|
|
321
|
+
data-testid={dataField}
|
|
322
|
+
/>
|
|
323
|
+
)}
|
|
324
|
+
/>
|
|
325
|
+
</ResponsiveWrapper>
|
|
326
|
+
</Row>
|
|
327
|
+
));
|
|
328
|
+
}
|
|
329
|
+
})()}
|
|
330
|
+
</Stack>
|
|
331
|
+
<ButtonSet className={classnames({ [styles.tablet]: isTablet, [styles.desktop]: !isTablet })}>
|
|
332
|
+
<Button className={styles.button} kind="secondary" onClick={closeForm}>
|
|
333
|
+
{t('discard', 'Discard')}
|
|
334
|
+
</Button>
|
|
335
|
+
<Button className={styles.button} kind="primary" disabled={isSubmitting} type="submit">
|
|
336
|
+
{isSubmitting ? (
|
|
337
|
+
<InlineLoading description={t('sending', 'Sending') + '...'} />
|
|
338
|
+
) : (
|
|
339
|
+
<span>{t('saveAndClose', 'Save and close')}</span>
|
|
340
|
+
)}
|
|
341
|
+
</Button>
|
|
342
|
+
</ButtonSet>
|
|
343
|
+
</Form>
|
|
344
|
+
);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
export default AddProviderConfigForm;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export const EmptyDataIllustration = ({ width = '64', height = '64' }) => {
|
|
4
|
+
return (
|
|
5
|
+
<svg width={width} height={height} viewBox="0 0 64 64">
|
|
6
|
+
<title>Empty data illustration</title>
|
|
7
|
+
<g fill="none" fillRule="evenodd">
|
|
8
|
+
<path
|
|
9
|
+
d="M38.133 13.186H21.947c-.768.001-1.39.623-1.39 1.391V50.55l-.186.057-3.97 1.216a.743.743 0 01-.927-.493L3.664 12.751a.742.742 0 01.492-.926l6.118-1.874 17.738-5.43 6.119-1.873a.741.741 0 01.926.492L38.076 13l.057.186z"
|
|
10
|
+
fill="#F4F4F4"
|
|
11
|
+
/>
|
|
12
|
+
<path
|
|
13
|
+
d="M41.664 13L38.026 1.117A1.576 1.576 0 0036.056.07l-8.601 2.633-17.737 5.43-8.603 2.634a1.578 1.578 0 00-1.046 1.97l12.436 40.616a1.58 1.58 0 001.969 1.046l5.897-1.805.185-.057v-.194l-.185.057-5.952 1.822a1.393 1.393 0 01-1.737-.923L.247 12.682a1.39 1.39 0 01.923-1.738L9.772 8.31 27.51 2.881 36.112.247a1.393 1.393 0 011.737.923L41.47 13l.057.186h.193l-.057-.185z"
|
|
14
|
+
fill="#8D8D8D"
|
|
15
|
+
/>
|
|
16
|
+
<path
|
|
17
|
+
d="M11.378 11.855a.836.836 0 01-.798-.59L9.385 7.361a.835.835 0 01.554-1.042l16.318-4.996a.836.836 0 011.042.554l1.195 3.902a.836.836 0 01-.554 1.043l-16.318 4.995a.831.831 0 01-.244.037z"
|
|
18
|
+
fill="#C6C6C6"
|
|
19
|
+
/>
|
|
20
|
+
<circle fill="#C6C6C6" cx={17.636} cy={2.314} r={1.855} />
|
|
21
|
+
<circle fill="#FFF" fillRule="nonzero" cx={17.636} cy={2.314} r={1.175} />
|
|
22
|
+
<path
|
|
23
|
+
d="M55.893 53.995H24.544a.79.79 0 01-.788-.789V15.644a.79.79 0 01.788-.788h31.349a.79.79 0 01.788.788v37.562a.79.79 0 01-.788.789z"
|
|
24
|
+
fill="#F4F4F4"
|
|
25
|
+
/>
|
|
26
|
+
<path
|
|
27
|
+
d="M41.47 13H21.948a1.579 1.579 0 00-1.576 1.577V52.4l.185-.057V14.577c.001-.768.623-1.39 1.391-1.39h19.581L41.471 13zm17.02 0H21.947a1.579 1.579 0 00-1.576 1.577v42.478c0 .87.706 1.576 1.576 1.577H58.49a1.579 1.579 0 001.576-1.577V14.577a1.579 1.579 0 00-1.576-1.576zm1.39 44.055c0 .768-.622 1.39-1.39 1.392H21.947c-.768-.001-1.39-.624-1.39-1.392V14.577c0-.768.622-1.39 1.39-1.39H58.49c.768 0 1.39.622 1.39 1.39v42.478z"
|
|
28
|
+
fill="#8D8D8D"
|
|
29
|
+
/>
|
|
30
|
+
<path
|
|
31
|
+
d="M48.751 17.082H31.686a.836.836 0 01-.835-.835v-4.081c0-.46.374-.834.835-.835H48.75c.461 0 .834.374.835.835v4.08c0 .462-.374.835-.835.836z"
|
|
32
|
+
fill="#C6C6C6"
|
|
33
|
+
/>
|
|
34
|
+
<circle fill="#C6C6C6" cx={40.218} cy={9.755} r={1.855} />
|
|
35
|
+
<circle fill="#FFF" fillRule="nonzero" cx={40.218} cy={9.755} r={1.13} />
|
|
36
|
+
</g>
|
|
37
|
+
</svg>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Layer, Link, Tile } from '@carbon/react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { EmptyDataIllustration } from './empty-data-illustration.component';
|
|
5
|
+
import { useLayoutType } from '@openmrs/esm-framework';
|
|
6
|
+
import styles from './empty-state.scss';
|
|
7
|
+
import { Button } from '@carbon/react';
|
|
8
|
+
|
|
9
|
+
export interface EmptyStateProps {
|
|
10
|
+
displayText: string;
|
|
11
|
+
headerTitle: string;
|
|
12
|
+
launchForm?(): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const EmptyState: React.FC<EmptyStateProps> = (props) => {
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Layer>
|
|
21
|
+
<Tile className={styles.tile}>
|
|
22
|
+
<div className={isTablet ? styles.tabletHeading : styles.desktopHeading}>
|
|
23
|
+
<h4>{props.headerTitle}</h4>
|
|
24
|
+
</div>
|
|
25
|
+
<EmptyDataIllustration />
|
|
26
|
+
<p className={styles.content}>
|
|
27
|
+
{t('emptyTableStateText', 'There are no {{displayText}} to display', {
|
|
28
|
+
displayText: props.displayText.toLowerCase(),
|
|
29
|
+
})}
|
|
30
|
+
</p>
|
|
31
|
+
<p className={styles.action}>
|
|
32
|
+
{props.launchForm && (
|
|
33
|
+
<Button onClick={() => props.launchForm()} kind="ghost" size={isTablet ? 'lg' : 'sm'}>
|
|
34
|
+
{t('record', 'Record')} {props.displayText.toLowerCase()}
|
|
35
|
+
</Button>
|
|
36
|
+
)}
|
|
37
|
+
</p>
|
|
38
|
+
</Tile>
|
|
39
|
+
</Layer>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
@use '@carbon/styles/scss/type';
|
|
3
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
4
|
+
|
|
5
|
+
.action {
|
|
6
|
+
margin-bottom: spacing.$spacing-03;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.content {
|
|
10
|
+
@include type.type-style("heading-compact-01");
|
|
11
|
+
color: $text-02;
|
|
12
|
+
margin-top: spacing.$spacing-05;
|
|
13
|
+
margin-bottom: spacing.$spacing-03;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.desktopHeading {
|
|
17
|
+
h4 {
|
|
18
|
+
@include type.type-style('heading-compact-02');
|
|
19
|
+
color: $text-02;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.tabletHeading {
|
|
24
|
+
h4 {
|
|
25
|
+
@include type.type-style('heading-03');
|
|
26
|
+
color: $text-02;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.desktopHeading, .tabletHeading {
|
|
31
|
+
text-align: left;
|
|
32
|
+
text-transform: capitalize;
|
|
33
|
+
margin-bottom: spacing.$spacing-05;
|
|
34
|
+
|
|
35
|
+
h4:after {
|
|
36
|
+
content: "";
|
|
37
|
+
display: block;
|
|
38
|
+
width: 2rem;
|
|
39
|
+
padding-top: 0.188rem;
|
|
40
|
+
border-bottom: 0.375rem solid var(--brand-03);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.heading:after {
|
|
45
|
+
content: "";
|
|
46
|
+
display: block;
|
|
47
|
+
width: 2rem;
|
|
48
|
+
padding-top: 0.188rem;
|
|
49
|
+
border-bottom: 0.375rem solid var(--brand-03);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.tile {
|
|
53
|
+
text-align: center;
|
|
54
|
+
border: 1px solid $ui-03;
|
|
55
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
FileUploaderDropContainer,
|
|
4
|
+
InlineNotification,
|
|
5
|
+
ModalHeader,
|
|
6
|
+
ModalBody,
|
|
7
|
+
ModalFooter,
|
|
8
|
+
Button,
|
|
9
|
+
Form,
|
|
10
|
+
Stack,
|
|
11
|
+
TextArea,
|
|
12
|
+
} from '@carbon/react';
|
|
13
|
+
import { useTranslation } from 'react-i18next';
|
|
14
|
+
import { DocumentUnknown } from '@carbon/react/icons';
|
|
15
|
+
import { uploadConfigTemplate } from '../../api/providers.resource';
|
|
16
|
+
import { showSnackbar } from '@openmrs/esm-framework';
|
|
17
|
+
import styles from './config-upload.scss';
|
|
18
|
+
|
|
19
|
+
interface ErrorNotification {
|
|
20
|
+
title: string;
|
|
21
|
+
subtitle: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ConfigUploadModalProps {
|
|
25
|
+
closeModal: () => {};
|
|
26
|
+
mutateTemplates: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ConfigUploadModal: React.FC<ConfigUploadModalProps> = ({ closeModal, mutateTemplates }) => {
|
|
30
|
+
const { t } = useTranslation();
|
|
31
|
+
|
|
32
|
+
// Maximm config file size. Hard coding since we don't expect the file size to go beyond
|
|
33
|
+
// 1MB. This value should be updated to match the actual maximum file size once needed in the future.
|
|
34
|
+
const maxFileSize = 1;
|
|
35
|
+
|
|
36
|
+
const [errorNotification, setErrorNotification] = useState<ErrorNotification>(null);
|
|
37
|
+
const [fileToUpload, setFileToUpload] = useState<File>(null);
|
|
38
|
+
|
|
39
|
+
const [fileContent, setFileContent] = useState<string | null>(null);
|
|
40
|
+
|
|
41
|
+
const upload = useCallback(
|
|
42
|
+
(files: Array<File>) => {
|
|
43
|
+
const config = files[0];
|
|
44
|
+
|
|
45
|
+
if (config.size > maxFileSize * 1024 * 1024) {
|
|
46
|
+
return setErrorNotification({
|
|
47
|
+
title: t('fileSizeLimitExceededText', 'File size limit exceeded'),
|
|
48
|
+
subtitle: `The file "${config.name}" ${t(
|
|
49
|
+
'fileSizeLimitExceeded',
|
|
50
|
+
'exceeds the size limit of',
|
|
51
|
+
)} ${maxFileSize} MB.`,
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
setFileToUpload(config);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
[maxFileSize, t],
|
|
58
|
+
);
|
|
59
|
+
const onCancel = useCallback(() => {
|
|
60
|
+
setFileToUpload(null);
|
|
61
|
+
setFileContent(null);
|
|
62
|
+
closeModal();
|
|
63
|
+
}, [closeModal]);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const reader = new FileReader();
|
|
67
|
+
reader.onload = (event) => {
|
|
68
|
+
const result = event.target?.result as string;
|
|
69
|
+
try {
|
|
70
|
+
const json = JSON.parse(result);
|
|
71
|
+
setFileContent(JSON.stringify(json, null, 2));
|
|
72
|
+
} catch (e) {
|
|
73
|
+
setFileContent('Invalid JSON file');
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
if (fileToUpload) {
|
|
77
|
+
reader.readAsText(fileToUpload);
|
|
78
|
+
}
|
|
79
|
+
}, [fileToUpload]);
|
|
80
|
+
|
|
81
|
+
const handleSubmit = useCallback(
|
|
82
|
+
(e) => {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
if (fileToUpload) {
|
|
85
|
+
return uploadConfigTemplate(fileToUpload)
|
|
86
|
+
.then(() => {
|
|
87
|
+
mutateTemplates?.();
|
|
88
|
+
showSnackbar({
|
|
89
|
+
title: t('uploadSuccess', 'Upload successful'),
|
|
90
|
+
kind: 'success',
|
|
91
|
+
}),
|
|
92
|
+
closeModal();
|
|
93
|
+
})
|
|
94
|
+
.catch(() =>
|
|
95
|
+
showSnackbar({
|
|
96
|
+
title: t('uploadNotSuccess', 'Upload unsuccessful'),
|
|
97
|
+
kind: 'error',
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
[closeModal, fileToUpload, mutateTemplates, t],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<Form onSubmit={handleSubmit}>
|
|
107
|
+
<ModalHeader closeModal={onCancel} title={t('uploadConfig', 'Upload configuration template')} />
|
|
108
|
+
<ModalBody className={styles.modalBody}>
|
|
109
|
+
{fileToUpload ? (
|
|
110
|
+
<>
|
|
111
|
+
<div className={styles.filePlaceholder}>
|
|
112
|
+
<DocumentUnknown size={16} />
|
|
113
|
+
</div>
|
|
114
|
+
<div className={styles.imageDetails}>
|
|
115
|
+
<Stack gap={5}>
|
|
116
|
+
<div className="cds--label">{t('fileName', 'File name')}</div>
|
|
117
|
+
<h6>{fileToUpload.name}</h6>
|
|
118
|
+
{fileContent && (
|
|
119
|
+
<div className={styles.jsonPreviesw}>
|
|
120
|
+
<TextArea labelText={t('fileContent', 'File Content')} value={fileContent} readOnly rows={10} />
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</Stack>
|
|
124
|
+
</div>
|
|
125
|
+
</>
|
|
126
|
+
) : (
|
|
127
|
+
<div className="cds--file__container">
|
|
128
|
+
{errorNotification && (
|
|
129
|
+
<div className={styles.errorContainer}>
|
|
130
|
+
<InlineNotification
|
|
131
|
+
aria-label="Upload error notification"
|
|
132
|
+
kind="error"
|
|
133
|
+
onClose={() => setErrorNotification(null)}
|
|
134
|
+
subtitle={errorNotification.subtitle}
|
|
135
|
+
title={errorNotification.title}
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
<p className="cds--label-description">
|
|
140
|
+
{t('fileUploadSizeConstraints', 'Size limit is {{fileSize}}MB', {
|
|
141
|
+
fileSize: maxFileSize,
|
|
142
|
+
})}
|
|
143
|
+
.
|
|
144
|
+
</p>
|
|
145
|
+
<div className={styles.uploadFile}>
|
|
146
|
+
<FileUploaderDropContainer
|
|
147
|
+
accept={['.json']}
|
|
148
|
+
labelText={t('fileSizeInstructions', 'Drag and drop files here or click to upload')}
|
|
149
|
+
tabIndex={0}
|
|
150
|
+
onAddFiles={(evt, { addedFiles }) => {
|
|
151
|
+
upload(addedFiles);
|
|
152
|
+
}}
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
</ModalBody>
|
|
158
|
+
<ModalFooter>
|
|
159
|
+
<Button kind="secondary" size="lg" onClick={onCancel}>
|
|
160
|
+
{t('cancel', 'Cancel')}
|
|
161
|
+
</Button>
|
|
162
|
+
<Button type="submit" size="lg" disabled={!fileToUpload}>
|
|
163
|
+
{t('uploadConfig', 'Upload config')}
|
|
164
|
+
</Button>
|
|
165
|
+
</ModalFooter>
|
|
166
|
+
</Form>
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export default ConfigUploadModal;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
@use '@carbon/styles/scss/type';
|
|
3
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
4
|
+
|
|
5
|
+
.errorContainer {
|
|
6
|
+
margin: spacing.$spacing-05 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.uploadFile {
|
|
10
|
+
color: $ui-02;
|
|
11
|
+
width: 100%;
|
|
12
|
+
height: 25rem;
|
|
13
|
+
|
|
14
|
+
div > button {
|
|
15
|
+
max-width: 100%;
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-flow: column;
|
|
18
|
+
justify-content: center;
|
|
19
|
+
align-items: center;
|
|
20
|
+
height: 25rem;
|
|
21
|
+
position: relative;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
label {
|
|
25
|
+
max-width: unset;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
label > div {
|
|
29
|
+
height: 25rem;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
align-items: center;
|
|
32
|
+
}
|
|
33
|
+
}
|