@webitel/ui-sdk 26.8.55 → 26.8.57
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/dist/clients-B8MJJx6E.js +39958 -0
- package/dist/{components-DNSY1_s4.js → components-BWyW5EEY.js} +3 -3
- package/dist/{engineCallFileType.zod-C-t93kQg.js → engineCallFileType-D7QVFOac.js} +1 -1
- package/dist/{generateInstance-C9d7J_TX.js → genClient-HHiOUj56.js} +969 -755
- package/dist/ui-sdk.js +2 -2
- package/dist/ui-sdk.umd.cjs +2088 -1976
- package/dist/{wt-call-media-action-Bn2bGCWs.js → wt-call-media-action-Bkyppnsm.js} +1 -1
- package/dist/{wt-chat-emoji-B13JscVZ.js → wt-chat-emoji-Cj3B8oR5.js} +1 -1
- package/dist/{wt-display-chip-items-Cf8L5Upd.js → wt-display-chip-items-Cvm5rOYr.js} +1 -1
- package/dist/{wt-screen-recordings-action-Dj6ITuhE.js → wt-screen-recordings-action-DI1qlUi1.js} +1 -1
- package/dist/{wt-send-message-popup-CO92UYST.js → wt-send-message-popup-OYvzhdyL.js} +2 -2
- package/dist/{wt-type-extension-value-input-FHQ4bN4j.js → wt-type-extension-value-input-BfUPxO-s.js} +2 -2
- package/dist/{wt-vidstack-player-DcItshB2.js → wt-vidstack-player-DjQEPOTu.js} +1 -1
- package/package.json +6 -2
- package/src/api/clients/caseSources/caseSources.ts +2 -151
- package/src/api/clients/quickReplies/quickReplies.ts +2 -157
- package/src/modules/QueryFilters/mixins/__tests__/enumFilterMixin.spec.js +60 -0
- package/src/modules/QueryFilters/mixins/enumFilterMixin.js +21 -6
- package/src/modules/UploadCsvPopup/components/__tests__/wt-upload-csv-popup.spec.js +32 -0
- package/src/modules/UploadCsvPopup/components/wt-upload-csv-popup.vue +15 -26
- package/src/modules/UploadCsvPopup/composable/__tests__/useUploadCsv.spec.js +393 -0
- package/src/modules/UploadCsvPopup/composable/useUploadCsv.ts +36 -19
- package/src/modules/UploadCsvPopup/index.ts +11 -0
- package/src/modules/UploadCsvPopup/scripts/__tests__/normalizeCSVData.spec.js +125 -0
- package/src/modules/UploadCsvPopup/scripts/__tests__/parseCSV.spec.js +63 -0
- package/src/modules/UploadCsvPopup/scripts/normalizeCSVData.ts +2 -0
- package/src/modules/UploadCsvPopup/scripts/parseCSV.ts +0 -23
- package/src/modules/UserNotifications/api/UserNotifications.ts +1 -1
- package/types/.tsbuildinfo +1 -1
- package/types/api/clients/caseSources/caseSources.d.ts +1 -16
- package/types/api/clients/quickReplies/quickReplies.d.ts +1 -16
- package/types/api/defaults/getDefaultInstance/getDefaultInstance.d.ts +1 -1
- package/types/modules/UploadCsvPopup/components/__tests__/wt-upload-csv-popup.spec.d.ts +1 -0
- package/types/modules/UploadCsvPopup/components/wt-upload-csv-popup.vue.d.ts +5 -5
- package/types/modules/UploadCsvPopup/composable/__tests__/useUploadCsv.spec.d.ts +1 -0
- package/types/modules/UploadCsvPopup/composable/useUploadCsv.d.ts +7 -3
- package/types/modules/UploadCsvPopup/index.d.ts +5 -0
- package/types/modules/UploadCsvPopup/scripts/__tests__/normalizeCSVData.spec.d.ts +1 -0
- package/types/modules/UploadCsvPopup/scripts/__tests__/parseCSV.spec.d.ts +1 -0
- package/types/modules/UploadCsvPopup/scripts/normalizeCSVData.d.ts +2 -0
- package/dist/clients-Ck0KwDIR.js +0 -39275
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
import HandlingCSVMode from '../../types/WtUploadCSVHandlingMode.enum';
|
|
5
|
+
import useUploadCsv from '../useUploadCsv';
|
|
6
|
+
|
|
7
|
+
const Component = {
|
|
8
|
+
props: {
|
|
9
|
+
file: {
|
|
10
|
+
default: null,
|
|
11
|
+
},
|
|
12
|
+
mappingFields: {
|
|
13
|
+
type: Array,
|
|
14
|
+
default: () => [],
|
|
15
|
+
},
|
|
16
|
+
addBulkItems: {
|
|
17
|
+
type: Function,
|
|
18
|
+
default: undefined,
|
|
19
|
+
},
|
|
20
|
+
handlingMode: {
|
|
21
|
+
type: String,
|
|
22
|
+
default: HandlingCSVMode.PROCESS,
|
|
23
|
+
},
|
|
24
|
+
fileUploadHandler: {
|
|
25
|
+
type: Function,
|
|
26
|
+
default: undefined,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
emits: [
|
|
30
|
+
'changeMappingFields',
|
|
31
|
+
'save',
|
|
32
|
+
'close',
|
|
33
|
+
],
|
|
34
|
+
setup(props, { emit }) {
|
|
35
|
+
const skipHeaders = ref(true);
|
|
36
|
+
const separator = ref(',');
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
skipHeaders,
|
|
40
|
+
separator,
|
|
41
|
+
...useUploadCsv({
|
|
42
|
+
props,
|
|
43
|
+
emit,
|
|
44
|
+
skipHeaders,
|
|
45
|
+
separator,
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
render: () => null,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const makeFile = (content) => {
|
|
53
|
+
const blob = new Blob(
|
|
54
|
+
[
|
|
55
|
+
content,
|
|
56
|
+
],
|
|
57
|
+
{
|
|
58
|
+
type: 'text/csv',
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return new File(
|
|
63
|
+
[
|
|
64
|
+
blob,
|
|
65
|
+
],
|
|
66
|
+
'test.csv',
|
|
67
|
+
{
|
|
68
|
+
type: 'text/csv',
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// the composable reads the file through a watcher, so give FileReader a tick
|
|
74
|
+
const mountWithFile = async (csv, props = {}) => {
|
|
75
|
+
const wrapper = shallowMount(Component, {
|
|
76
|
+
props,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await wrapper.setProps({
|
|
80
|
+
file: makeFile(csv),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await new Promise((resolve) => {
|
|
84
|
+
setTimeout(resolve, 100);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return wrapper;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
describe('useUploadCsv', () => {
|
|
91
|
+
it('mounts a component with the composable', () => {
|
|
92
|
+
const wrapper = shallowMount(Component, {
|
|
93
|
+
props: {
|
|
94
|
+
file: {},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(wrapper.exists()).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('parses and saves simple csv', async () => {
|
|
102
|
+
const csv = `
|
|
103
|
+
col1,col2
|
|
104
|
+
John,30
|
|
105
|
+
Jane,25
|
|
106
|
+
`.replaceAll(/ +?/g, ''); // replace all whitespaces, but not newlines
|
|
107
|
+
|
|
108
|
+
const saveCallback = vi.fn();
|
|
109
|
+
|
|
110
|
+
const wrapper = await mountWithFile(csv, {
|
|
111
|
+
addBulkItems: saveCallback,
|
|
112
|
+
mappingFields: [
|
|
113
|
+
{
|
|
114
|
+
name: 'name',
|
|
115
|
+
csv: 'col1',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'age',
|
|
119
|
+
csv: 'col2',
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await wrapper.vm.processCSV();
|
|
125
|
+
|
|
126
|
+
expect(saveCallback).toHaveBeenCalledWith([
|
|
127
|
+
{
|
|
128
|
+
name: 'John',
|
|
129
|
+
age: '30',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'Jane',
|
|
133
|
+
age: '25',
|
|
134
|
+
},
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('parsing of csv with empty required throws error', async () => {
|
|
139
|
+
const csv = `
|
|
140
|
+
col1,col2
|
|
141
|
+
John,
|
|
142
|
+
Jane,25
|
|
143
|
+
`.replaceAll(/ +?/g, ''); // replace all whitespaces, but not newlines
|
|
144
|
+
|
|
145
|
+
const saveCallback = vi.fn();
|
|
146
|
+
|
|
147
|
+
const wrapper = await mountWithFile(csv, {
|
|
148
|
+
addBulkItems: saveCallback,
|
|
149
|
+
mappingFields: [
|
|
150
|
+
{
|
|
151
|
+
name: 'name',
|
|
152
|
+
csv: 'col1',
|
|
153
|
+
required: true,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'age',
|
|
157
|
+
csv: 'col2',
|
|
158
|
+
required: true,
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await expect(wrapper.vm.processCSV()).rejects.toBeTruthy();
|
|
164
|
+
expect(saveCallback).not.toHaveBeenCalled();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('parsing of csv with multiple columns selected to required field', async () => {
|
|
168
|
+
const csv = `
|
|
169
|
+
col1,col2,col3
|
|
170
|
+
John,,30
|
|
171
|
+
Jane,25,
|
|
172
|
+
`.replaceAll(/ +?/g, ''); // replace all whitespaces, but not newlines
|
|
173
|
+
|
|
174
|
+
const saveCallback = vi.fn();
|
|
175
|
+
|
|
176
|
+
const wrapper = await mountWithFile(csv, {
|
|
177
|
+
addBulkItems: saveCallback,
|
|
178
|
+
mappingFields: [
|
|
179
|
+
{
|
|
180
|
+
name: 'name',
|
|
181
|
+
csv: 'col1',
|
|
182
|
+
required: true,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'age',
|
|
186
|
+
csv: [
|
|
187
|
+
'col2',
|
|
188
|
+
'col3',
|
|
189
|
+
],
|
|
190
|
+
required: true,
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await wrapper.vm.processCSV();
|
|
196
|
+
|
|
197
|
+
expect(saveCallback).toHaveBeenCalledWith([
|
|
198
|
+
{
|
|
199
|
+
name: 'John',
|
|
200
|
+
age: [
|
|
201
|
+
'',
|
|
202
|
+
'30',
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'Jane',
|
|
207
|
+
age: [
|
|
208
|
+
'25',
|
|
209
|
+
'',
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
/*
|
|
216
|
+
WTEL-10058: guards the admin data that the parser never touched — queue
|
|
217
|
+
member variables, blacklist descriptions, device passwords. Type casting
|
|
218
|
+
lives in CRM now, so everything reaching a consumer is a string.
|
|
219
|
+
*/
|
|
220
|
+
it('keeps plain values as strings, so text columns are not coerced', async () => {
|
|
221
|
+
const csv = `
|
|
222
|
+
col1,col2
|
|
223
|
+
true,"[1,2]"
|
|
224
|
+
`.replaceAll(/ +?/g, ''); // replace all whitespaces, but not newlines
|
|
225
|
+
|
|
226
|
+
const saveCallback = vi.fn();
|
|
227
|
+
|
|
228
|
+
const wrapper = await mountWithFile(csv, {
|
|
229
|
+
addBulkItems: saveCallback,
|
|
230
|
+
mappingFields: [
|
|
231
|
+
{
|
|
232
|
+
name: 'flag',
|
|
233
|
+
csv: 'col1',
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'list',
|
|
237
|
+
csv: 'col2',
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await wrapper.vm.processCSV();
|
|
243
|
+
|
|
244
|
+
expect(saveCallback).toHaveBeenCalledWith([
|
|
245
|
+
{
|
|
246
|
+
flag: 'true',
|
|
247
|
+
list: '[1,2]',
|
|
248
|
+
},
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('uploads the whole file in the upload handling mode', async () => {
|
|
253
|
+
const csv = `
|
|
254
|
+
col1
|
|
255
|
+
John
|
|
256
|
+
`.replaceAll(/ +?/g, ''); // replace all whitespaces, but not newlines
|
|
257
|
+
|
|
258
|
+
const saveCallback = vi.fn();
|
|
259
|
+
const fileUploadHandler = vi.fn();
|
|
260
|
+
|
|
261
|
+
const wrapper = await mountWithFile(csv, {
|
|
262
|
+
handlingMode: HandlingCSVMode.UPLOAD,
|
|
263
|
+
addBulkItems: saveCallback,
|
|
264
|
+
fileUploadHandler,
|
|
265
|
+
mappingFields: [
|
|
266
|
+
{
|
|
267
|
+
name: 'name',
|
|
268
|
+
csv: 'col1',
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
await wrapper.vm.processCSV();
|
|
274
|
+
|
|
275
|
+
expect(fileUploadHandler).toHaveBeenCalled();
|
|
276
|
+
expect(saveCallback).not.toHaveBeenCalled();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
/*
|
|
280
|
+
WTEL-10058: the popup owns the separator, so a consumer that ships the raw
|
|
281
|
+
file to the backend has no other way to learn which one the user picked.
|
|
282
|
+
*/
|
|
283
|
+
it('hands the chosen separator to the file upload handler', async () => {
|
|
284
|
+
const csv = 'col1;col2\nJohn;30';
|
|
285
|
+
|
|
286
|
+
const fileUploadHandler = vi.fn();
|
|
287
|
+
|
|
288
|
+
const wrapper = await mountWithFile(csv, {
|
|
289
|
+
handlingMode: HandlingCSVMode.UPLOAD,
|
|
290
|
+
fileUploadHandler,
|
|
291
|
+
mappingFields: [
|
|
292
|
+
{
|
|
293
|
+
name: 'name',
|
|
294
|
+
csv: 'col1',
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
wrapper.vm.separator = ';';
|
|
300
|
+
await wrapper.vm.processCSV();
|
|
301
|
+
|
|
302
|
+
expect(fileUploadHandler).toHaveBeenCalledWith({
|
|
303
|
+
separator: ';',
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('emits save and closes on a successful handleSave', async () => {
|
|
308
|
+
const csv = `
|
|
309
|
+
col1
|
|
310
|
+
John
|
|
311
|
+
`.replaceAll(/ +?/g, ''); // replace all whitespaces, but not newlines
|
|
312
|
+
|
|
313
|
+
const wrapper = await mountWithFile(csv, {
|
|
314
|
+
addBulkItems: vi.fn(),
|
|
315
|
+
mappingFields: [
|
|
316
|
+
{
|
|
317
|
+
name: 'name',
|
|
318
|
+
csv: 'col1',
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
await wrapper.vm.handleSave();
|
|
324
|
+
|
|
325
|
+
expect(wrapper.emitted('save')).toHaveLength(1);
|
|
326
|
+
expect(wrapper.emitted('close')).toHaveLength(1);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('does not close the popup when saving fails', async () => {
|
|
330
|
+
const csv = `
|
|
331
|
+
col1
|
|
332
|
+
John
|
|
333
|
+
`.replaceAll(/ +?/g, ''); // replace all whitespaces, but not newlines
|
|
334
|
+
|
|
335
|
+
const wrapper = await mountWithFile(csv, {
|
|
336
|
+
addBulkItems: vi.fn(() => Promise.reject(new Error('nope'))),
|
|
337
|
+
mappingFields: [
|
|
338
|
+
{
|
|
339
|
+
name: 'name',
|
|
340
|
+
csv: 'col1',
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
await expect(wrapper.vm.processCSV()).rejects.toThrow();
|
|
346
|
+
expect(wrapper.emitted('close')).toBeUndefined();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('resets the mappings when the parse options change', async () => {
|
|
350
|
+
const csv = `
|
|
351
|
+
col1,col2
|
|
352
|
+
John,30
|
|
353
|
+
`.replaceAll(/ +?/g, ''); // replace all whitespaces, but not newlines
|
|
354
|
+
|
|
355
|
+
const wrapper = await mountWithFile(csv, {
|
|
356
|
+
mappingFields: [
|
|
357
|
+
{
|
|
358
|
+
name: 'name',
|
|
359
|
+
csv: 'col1',
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: 'tags',
|
|
363
|
+
csv: [
|
|
364
|
+
'col2',
|
|
365
|
+
],
|
|
366
|
+
multiple: true,
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
wrapper.vm.separator = ';';
|
|
372
|
+
|
|
373
|
+
// the parse options watcher is debounced by 1s
|
|
374
|
+
await new Promise((resolve) => {
|
|
375
|
+
setTimeout(resolve, 1200);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const emitted = wrapper.emitted('changeMappingFields');
|
|
379
|
+
|
|
380
|
+
expect(emitted).toBeTruthy();
|
|
381
|
+
expect(emitted.at(-1)[0]).toEqual([
|
|
382
|
+
{
|
|
383
|
+
name: 'name',
|
|
384
|
+
csv: '',
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: 'tags',
|
|
388
|
+
csv: [],
|
|
389
|
+
multiple: true,
|
|
390
|
+
},
|
|
391
|
+
]);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { computed, type Ref, ref, watch } from 'vue';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useEventBus } from '../../../composables';
|
|
4
|
+
import {
|
|
5
|
+
debounce,
|
|
6
|
+
eventBus as defaultEventBus,
|
|
7
|
+
isEmpty,
|
|
8
|
+
} from '../../../scripts';
|
|
4
9
|
import normalizeCSVData, {
|
|
5
10
|
type CsvDataRow,
|
|
6
11
|
type CsvMappingField,
|
|
@@ -17,12 +22,20 @@ import HandlingCSVMode from '../types/WtUploadCSVHandlingMode.enum';
|
|
|
17
22
|
* @param {import('vue').Ref<boolean>} params.skipHeaders
|
|
18
23
|
* @param {import('vue').Ref<string>} params.separator
|
|
19
24
|
*/
|
|
25
|
+
|
|
26
|
+
export interface UploadCsvParseSettings {
|
|
27
|
+
separator: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
export interface UseUploadCsvProps {
|
|
21
31
|
file?: File | null;
|
|
22
32
|
mappingFields?: CsvMappingField[];
|
|
23
33
|
addBulkItems?: (items: unknown[]) => unknown | Promise<unknown>;
|
|
24
|
-
handlingMode?:
|
|
25
|
-
fileUploadHandler?: (
|
|
34
|
+
handlingMode?: HandlingCSVMode;
|
|
35
|
+
fileUploadHandler?: (
|
|
36
|
+
settings: UploadCsvParseSettings,
|
|
37
|
+
) => unknown | Promise<unknown>;
|
|
38
|
+
charset?: string;
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
interface UseUploadCsvParams {
|
|
@@ -39,11 +52,12 @@ const useUploadCsv = ({
|
|
|
39
52
|
skipHeaders,
|
|
40
53
|
separator,
|
|
41
54
|
}: UseUploadCsvParams) => {
|
|
55
|
+
const eventBus = useEventBus() ?? defaultEventBus;
|
|
56
|
+
|
|
42
57
|
const isReadingFile = ref(false);
|
|
43
58
|
const isParsingCSV = ref(false);
|
|
44
59
|
const parsedFile = ref<unknown>(null);
|
|
45
60
|
const isParsingPreview = ref(false);
|
|
46
|
-
const parseErrorStackTrace = ref<unknown>('');
|
|
47
61
|
const csvPreview = ref<unknown[]>([
|
|
48
62
|
[],
|
|
49
63
|
]);
|
|
@@ -101,15 +115,21 @@ const useUploadCsv = ({
|
|
|
101
115
|
),
|
|
102
116
|
);
|
|
103
117
|
|
|
118
|
+
function notifyError(err: unknown) {
|
|
119
|
+
eventBus.$emit('notification', {
|
|
120
|
+
type: 'error',
|
|
121
|
+
text: err instanceof Error ? err.message : String(err),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
104
125
|
async function createCSVPreview(file = parsedFile.value) {
|
|
105
126
|
try {
|
|
106
|
-
parseErrorStackTrace.value = '';
|
|
107
127
|
csvPreview.value = await parseCSV(file as string, {
|
|
108
128
|
...parseCSVOptions.value,
|
|
109
129
|
toLine: 4,
|
|
110
130
|
});
|
|
111
131
|
} catch (err) {
|
|
112
|
-
|
|
132
|
+
notifyError(err);
|
|
113
133
|
csvPreview.value = [
|
|
114
134
|
[],
|
|
115
135
|
];
|
|
@@ -138,7 +158,9 @@ const useUploadCsv = ({
|
|
|
138
158
|
|
|
139
159
|
isReadingFile.value = true;
|
|
140
160
|
|
|
141
|
-
parsedFile.value = await processFile(props.file as File, {
|
|
161
|
+
parsedFile.value = await processFile(props.file as File, {
|
|
162
|
+
charset: props.charset,
|
|
163
|
+
});
|
|
142
164
|
await createCSVPreview(parsedFile.value);
|
|
143
165
|
|
|
144
166
|
isReadingFile.value = false;
|
|
@@ -165,20 +187,17 @@ const useUploadCsv = ({
|
|
|
165
187
|
isParsingCSV.value = true;
|
|
166
188
|
|
|
167
189
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
props.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (handler) {
|
|
176
|
-
await handler();
|
|
190
|
+
if (props.handlingMode === HandlingCSVMode.PROCESS) {
|
|
191
|
+
await handleCSVProcessing();
|
|
192
|
+
} else {
|
|
193
|
+
await props.fileUploadHandler?.({
|
|
194
|
+
separator: separator.value,
|
|
195
|
+
});
|
|
177
196
|
}
|
|
178
197
|
|
|
179
198
|
close();
|
|
180
199
|
} catch (err) {
|
|
181
|
-
|
|
200
|
+
notifyError(err);
|
|
182
201
|
throw err;
|
|
183
202
|
} finally {
|
|
184
203
|
isParsingCSV.value = false;
|
|
@@ -194,7 +213,6 @@ const useUploadCsv = ({
|
|
|
194
213
|
emit('close');
|
|
195
214
|
}
|
|
196
215
|
|
|
197
|
-
// watchers (те, що були в mixin.watch)
|
|
198
216
|
watch(skipHeaders, async () => {
|
|
199
217
|
await handleParseOptionsChange();
|
|
200
218
|
});
|
|
@@ -214,7 +232,6 @@ const useUploadCsv = ({
|
|
|
214
232
|
isReadingFile,
|
|
215
233
|
isParsingCSV,
|
|
216
234
|
isParsingPreview,
|
|
217
|
-
parseErrorStackTrace,
|
|
218
235
|
csvPreviewTableData,
|
|
219
236
|
csvPreviewTableHeaders,
|
|
220
237
|
filteredCsvPreviewTableHeaders,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { default as WtUploadCsvPopup } from './components/wt-upload-csv-popup.vue';
|
|
2
|
+
export type {
|
|
3
|
+
UploadCsvParseSettings,
|
|
4
|
+
UseUploadCsvProps,
|
|
5
|
+
} from './composable/useUploadCsv';
|
|
6
|
+
export { default as useUploadCsv } from './composable/useUploadCsv';
|
|
7
|
+
export type {
|
|
8
|
+
CsvDataRow,
|
|
9
|
+
CsvMappingField,
|
|
10
|
+
} from './scripts/normalizeCSVData';
|
|
11
|
+
export { HandlingCSVMode } from './types/WtUploadCSVHandlingMode.enum';
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import normalizeCSVData from '../normalizeCSVData';
|
|
2
|
+
|
|
3
|
+
describe('normalizeCSVData', () => {
|
|
4
|
+
const mappings = [
|
|
5
|
+
{
|
|
6
|
+
// webitel field name
|
|
7
|
+
name: 'name',
|
|
8
|
+
// csv column name
|
|
9
|
+
csv: 'col1',
|
|
10
|
+
// is this webitel field required?
|
|
11
|
+
required: true,
|
|
12
|
+
// ui
|
|
13
|
+
locale: '',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'phone',
|
|
17
|
+
csv: [
|
|
18
|
+
'col2',
|
|
19
|
+
'col3',
|
|
20
|
+
],
|
|
21
|
+
required: true,
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
it('normalizes simple data', () => {
|
|
26
|
+
const input = [
|
|
27
|
+
{
|
|
28
|
+
col1: 'John',
|
|
29
|
+
col2: '123',
|
|
30
|
+
col3: '456',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
col1: 'Jane',
|
|
34
|
+
col2: '789',
|
|
35
|
+
col3: '012',
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
const result = normalizeCSVData({
|
|
39
|
+
data: input,
|
|
40
|
+
mappings,
|
|
41
|
+
});
|
|
42
|
+
const output = [
|
|
43
|
+
{
|
|
44
|
+
name: 'John',
|
|
45
|
+
phone: [
|
|
46
|
+
'123',
|
|
47
|
+
'456',
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'Jane',
|
|
52
|
+
phone: [
|
|
53
|
+
'789',
|
|
54
|
+
'012',
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
expect(result).toEqual(output);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('throws an error at normalizing data with required (!) empty value', () => {
|
|
62
|
+
const input = [
|
|
63
|
+
{
|
|
64
|
+
col1: '',
|
|
65
|
+
col2: '123',
|
|
66
|
+
col3: '3424',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
col1: 'name',
|
|
70
|
+
col2: '131312',
|
|
71
|
+
col3: '012',
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
try {
|
|
75
|
+
normalizeCSVData({
|
|
76
|
+
data: input,
|
|
77
|
+
mappings,
|
|
78
|
+
});
|
|
79
|
+
} catch (err) {
|
|
80
|
+
expect(err).toBeTruthy();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
expect(false).toBe(true); // reject test if no error
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it(`returns correct fields for mapping, including empty fields,
|
|
87
|
+
if one of them is empty`, () => {
|
|
88
|
+
const input = [
|
|
89
|
+
{
|
|
90
|
+
col1: 'name',
|
|
91
|
+
col2: '123',
|
|
92
|
+
col3: '',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
col1: 'name',
|
|
96
|
+
col2: '123',
|
|
97
|
+
col3: '012',
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const result = normalizeCSVData({
|
|
102
|
+
data: input,
|
|
103
|
+
mappings,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const output = [
|
|
107
|
+
{
|
|
108
|
+
name: 'name',
|
|
109
|
+
phone: [
|
|
110
|
+
'123',
|
|
111
|
+
'',
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'name',
|
|
116
|
+
phone: [
|
|
117
|
+
'123',
|
|
118
|
+
'012',
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
expect(result).toEqual(output);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import parseCSV from '../parseCSV';
|
|
2
|
+
|
|
3
|
+
describe('parseCSV', () => {
|
|
4
|
+
it('parses a csv into rows keyed by the header', async () => {
|
|
5
|
+
const csv = 'col1,col2\nJohn,30';
|
|
6
|
+
|
|
7
|
+
const result = await parseCSV(csv, {
|
|
8
|
+
columns: true,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
expect(result).toEqual([
|
|
12
|
+
{
|
|
13
|
+
col1: 'John',
|
|
14
|
+
col2: '30',
|
|
15
|
+
},
|
|
16
|
+
]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
WTEL-10058: the parser used to coerce 'true'/'false' and JSON-looking cells
|
|
21
|
+
for every consumer. Type casting belongs to whoever knows the field types —
|
|
22
|
+
CRM does it in prepareCsvLookupRows. Everything here stays a string.
|
|
23
|
+
*/
|
|
24
|
+
it.each([
|
|
25
|
+
'true',
|
|
26
|
+
'false',
|
|
27
|
+
'[1,2]',
|
|
28
|
+
'{"a":1}',
|
|
29
|
+
'42',
|
|
30
|
+
])('keeps %s as a string', async (value) => {
|
|
31
|
+
const csv = `col1\n"${value.replaceAll('"', '""')}"`;
|
|
32
|
+
|
|
33
|
+
const result = await parseCSV(csv, {
|
|
34
|
+
columns: true,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(result[0].col1).toBe(value);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('respects a custom delimiter', async () => {
|
|
41
|
+
const csv = 'col1;col2\nJohn;30';
|
|
42
|
+
|
|
43
|
+
const result = await parseCSV(csv, {
|
|
44
|
+
columns: true,
|
|
45
|
+
delimiter: ';',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(result[0]).toEqual({
|
|
49
|
+
col1: 'John',
|
|
50
|
+
col2: '30',
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('rejects on a malformed csv', async () => {
|
|
55
|
+
const csv = 'col1,col2\n"unterminated,30\nJane,25';
|
|
56
|
+
|
|
57
|
+
await expect(
|
|
58
|
+
parseCSV(csv, {
|
|
59
|
+
columns: true,
|
|
60
|
+
}),
|
|
61
|
+
).rejects.toBeTruthy();
|
|
62
|
+
});
|
|
63
|
+
});
|