@wix/bex-core 2.331.0 → 2.333.0
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/cjs/assets/locale/messages_ar.json +11 -0
- package/dist/cjs/assets/locale/messages_bg.json +11 -0
- package/dist/cjs/assets/locale/messages_ca.json +11 -0
- package/dist/cjs/assets/locale/messages_cs.json +11 -0
- package/dist/cjs/assets/locale/messages_da.json +11 -0
- package/dist/cjs/assets/locale/messages_de.json +11 -0
- package/dist/cjs/assets/locale/messages_el.json +11 -0
- package/dist/cjs/assets/locale/messages_en.json +11 -0
- package/dist/cjs/assets/locale/messages_es.json +11 -0
- package/dist/cjs/assets/locale/messages_fi.json +11 -0
- package/dist/cjs/assets/locale/messages_fr.json +11 -0
- package/dist/cjs/assets/locale/messages_he.json +11 -0
- package/dist/cjs/assets/locale/messages_hi.json +11 -0
- package/dist/cjs/assets/locale/messages_hu.json +11 -0
- package/dist/cjs/assets/locale/messages_id.json +11 -0
- package/dist/cjs/assets/locale/messages_it.json +11 -0
- package/dist/cjs/assets/locale/messages_ja.json +11 -0
- package/dist/cjs/assets/locale/messages_ko.json +11 -0
- package/dist/cjs/assets/locale/messages_lt.json +11 -0
- package/dist/cjs/assets/locale/messages_ms.json +11 -0
- package/dist/cjs/assets/locale/messages_nl.json +11 -0
- package/dist/cjs/assets/locale/messages_no.json +11 -0
- package/dist/cjs/assets/locale/messages_pl.json +11 -0
- package/dist/cjs/assets/locale/messages_pt.json +11 -0
- package/dist/cjs/assets/locale/messages_ro.json +11 -0
- package/dist/cjs/assets/locale/messages_ru.json +11 -0
- package/dist/cjs/assets/locale/messages_sk.json +11 -0
- package/dist/cjs/assets/locale/messages_sl.json +11 -0
- package/dist/cjs/assets/locale/messages_sv.json +11 -0
- package/dist/cjs/assets/locale/messages_th.json +11 -0
- package/dist/cjs/assets/locale/messages_tl.json +11 -0
- package/dist/cjs/assets/locale/messages_tr.json +11 -0
- package/dist/cjs/assets/locale/messages_uk.json +11 -0
- package/dist/cjs/assets/locale/messages_vi.json +11 -0
- package/dist/cjs/assets/locale/messages_zh.json +11 -0
- package/dist/cjs/assets/locale/version_file +1 -1
- package/dist/cjs/assets/visual-e2e-locale/messages_visuale2e.json +29 -2
- package/dist/cjs/hooks/index.js +0 -6
- package/dist/cjs/hooks/index.js.map +1 -1
- package/dist/cjs/hooks/useImport.js +3 -13
- package/dist/cjs/hooks/useImport.js.map +1 -1
- package/dist/cjs/index.js +0 -7
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/services/translations.js.map +1 -1
- package/dist/cjs/state/CustomFieldModalState.js +12 -0
- package/dist/cjs/state/CustomFieldModalState.js.map +1 -1
- package/dist/cjs/state/ImportState/ImportState.js +401 -133
- package/dist/cjs/state/ImportState/ImportState.js.map +1 -1
- package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js +671 -62
- package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -1
- package/dist/cjs/state/ImportState/detectFieldType.js +23 -0
- package/dist/cjs/state/ImportState/detectFieldType.js.map +1 -1
- package/dist/cjs/state/ImportState/fieldEdit.js +49 -0
- package/dist/cjs/state/ImportState/fieldEdit.js.map +1 -1
- package/dist/cjs/state/TagsState/TagsManagementState.js +17 -44
- package/dist/cjs/state/TagsState/TagsManagementState.js.map +1 -1
- package/dist/cjs/state/TagsState/TagsSearchState.js +0 -6
- package/dist/cjs/state/TagsState/TagsSearchState.js.map +1 -1
- package/dist/cjs/testkit/createInMemorySchemaSource.js +186 -0
- package/dist/cjs/testkit/createInMemorySchemaSource.js.map +1 -0
- package/dist/cjs/testkit/hubJobImportMocks.js +18 -2
- package/dist/cjs/testkit/hubJobImportMocks.js.map +1 -1
- package/dist/cjs/testkit/index.js +6 -0
- package/dist/cjs/testkit/index.js.map +1 -1
- package/dist/cjs/types/FieldsSource.js.map +1 -1
- package/dist/cjs/types/Schema.js.map +1 -1
- package/dist/cjs/types/SchemaSource.js +2 -1
- package/dist/cjs/types/SchemaSource.js.map +1 -1
- package/dist/esm/assets/locale/messages_ar.json +11 -0
- package/dist/esm/assets/locale/messages_bg.json +11 -0
- package/dist/esm/assets/locale/messages_ca.json +11 -0
- package/dist/esm/assets/locale/messages_cs.json +11 -0
- package/dist/esm/assets/locale/messages_da.json +11 -0
- package/dist/esm/assets/locale/messages_de.json +11 -0
- package/dist/esm/assets/locale/messages_el.json +11 -0
- package/dist/esm/assets/locale/messages_en.json +11 -0
- package/dist/esm/assets/locale/messages_es.json +11 -0
- package/dist/esm/assets/locale/messages_fi.json +11 -0
- package/dist/esm/assets/locale/messages_fr.json +11 -0
- package/dist/esm/assets/locale/messages_he.json +11 -0
- package/dist/esm/assets/locale/messages_hi.json +11 -0
- package/dist/esm/assets/locale/messages_hu.json +11 -0
- package/dist/esm/assets/locale/messages_id.json +11 -0
- package/dist/esm/assets/locale/messages_it.json +11 -0
- package/dist/esm/assets/locale/messages_ja.json +11 -0
- package/dist/esm/assets/locale/messages_ko.json +11 -0
- package/dist/esm/assets/locale/messages_lt.json +11 -0
- package/dist/esm/assets/locale/messages_ms.json +11 -0
- package/dist/esm/assets/locale/messages_nl.json +11 -0
- package/dist/esm/assets/locale/messages_no.json +11 -0
- package/dist/esm/assets/locale/messages_pl.json +11 -0
- package/dist/esm/assets/locale/messages_pt.json +11 -0
- package/dist/esm/assets/locale/messages_ro.json +11 -0
- package/dist/esm/assets/locale/messages_ru.json +11 -0
- package/dist/esm/assets/locale/messages_sk.json +11 -0
- package/dist/esm/assets/locale/messages_sl.json +11 -0
- package/dist/esm/assets/locale/messages_sv.json +11 -0
- package/dist/esm/assets/locale/messages_th.json +11 -0
- package/dist/esm/assets/locale/messages_tl.json +11 -0
- package/dist/esm/assets/locale/messages_tr.json +11 -0
- package/dist/esm/assets/locale/messages_uk.json +11 -0
- package/dist/esm/assets/locale/messages_vi.json +11 -0
- package/dist/esm/assets/locale/messages_zh.json +11 -0
- package/dist/esm/assets/locale/version_file +1 -1
- package/dist/esm/assets/visual-e2e-locale/messages_visuale2e.json +29 -2
- package/dist/esm/hooks/index.js +0 -1
- package/dist/esm/hooks/index.js.map +1 -1
- package/dist/esm/hooks/useImport.js +2 -7
- package/dist/esm/hooks/useImport.js.map +1 -1
- package/dist/esm/index.js +0 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/state/CustomFieldModalState.js +8 -0
- package/dist/esm/state/CustomFieldModalState.js.map +1 -1
- package/dist/esm/state/ImportState/ImportState.js +299 -108
- package/dist/esm/state/ImportState/ImportState.js.map +1 -1
- package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js +442 -55
- package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -1
- package/dist/esm/state/ImportState/detectFieldType.js +19 -0
- package/dist/esm/state/ImportState/detectFieldType.js.map +1 -1
- package/dist/esm/state/ImportState/fieldEdit.js +44 -0
- package/dist/esm/state/ImportState/fieldEdit.js.map +1 -1
- package/dist/esm/state/TagsState/TagsManagementState.js +17 -33
- package/dist/esm/state/TagsState/TagsManagementState.js.map +1 -1
- package/dist/esm/state/TagsState/TagsSearchState.js +0 -6
- package/dist/esm/state/TagsState/TagsSearchState.js.map +1 -1
- package/dist/esm/testkit/createInMemorySchemaSource.js +140 -0
- package/dist/esm/testkit/createInMemorySchemaSource.js.map +1 -0
- package/dist/esm/testkit/hubJobImportMocks.js +17 -1
- package/dist/esm/testkit/hubJobImportMocks.js.map +1 -1
- package/dist/esm/testkit/index.js +1 -0
- package/dist/esm/testkit/index.js.map +1 -1
- package/dist/esm/types/SchemaSource.js.map +1 -1
- package/dist/types/hooks/index.d.ts +0 -1
- package/dist/types/hooks/index.d.ts.map +1 -1
- package/dist/types/hooks/useImport.d.ts +3 -18
- package/dist/types/hooks/useImport.d.ts.map +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/services/translations.d.ts +20 -1
- package/dist/types/services/translations.d.ts.map +1 -1
- package/dist/types/state/CustomFieldModalState.d.ts +9 -0
- package/dist/types/state/CustomFieldModalState.d.ts.map +1 -1
- package/dist/types/state/ImportState/ImportState.d.ts +201 -62
- package/dist/types/state/ImportState/ImportState.d.ts.map +1 -1
- package/dist/types/state/ImportState/detectFieldType.d.ts +2 -0
- package/dist/types/state/ImportState/detectFieldType.d.ts.map +1 -1
- package/dist/types/state/ImportState/fieldEdit.d.ts +6 -0
- package/dist/types/state/ImportState/fieldEdit.d.ts.map +1 -1
- package/dist/types/state/TagsState/TagsManagementState.d.ts.map +1 -1
- package/dist/types/state/TagsState/TagsSearchState.d.ts.map +1 -1
- package/dist/types/testkit/WixPatternsContainer.driver.d.ts +4 -0
- package/dist/types/testkit/WixPatternsContainer.driver.d.ts.map +1 -1
- package/dist/types/testkit/createInMemorySchemaSource.d.ts +41 -0
- package/dist/types/testkit/createInMemorySchemaSource.d.ts.map +1 -0
- package/dist/types/testkit/hubJobImportMocks.d.ts +11 -1
- package/dist/types/testkit/hubJobImportMocks.d.ts.map +1 -1
- package/dist/types/testkit/index.d.ts +1 -0
- package/dist/types/testkit/index.d.ts.map +1 -1
- package/dist/types/types/FieldsSource.d.ts +5 -1
- package/dist/types/types/FieldsSource.d.ts.map +1 -1
- package/dist/types/types/Schema.d.ts +6 -0
- package/dist/types/types/Schema.d.ts.map +1 -1
- package/dist/types/types/SchemaSource.d.ts +6 -4
- package/dist/types/types/SchemaSource.d.ts.map +1 -1
- package/package.json +4 -4
- package/dist/cjs/hooks/useImportConfig.js +0 -10
- package/dist/cjs/hooks/useImportConfig.js.map +0 -1
- package/dist/esm/hooks/useImportConfig.js +0 -6
- package/dist/esm/hooks/useImportConfig.js.map +0 -1
- package/dist/types/hooks/useImportConfig.d.ts +0 -21
- package/dist/types/hooks/useImportConfig.d.ts.map +0 -1
|
@@ -13,9 +13,15 @@ jest.mock('../hubJobClient');
|
|
|
13
13
|
const COMPLETED_JOB = {
|
|
14
14
|
id: 'job-1',
|
|
15
15
|
status: JobStatus.COMPLETED,
|
|
16
|
-
progress: {
|
|
16
|
+
progress: {
|
|
17
|
+
itemsSuccessful: 2,
|
|
18
|
+
itemsRejected: 0,
|
|
19
|
+
total: 2,
|
|
20
|
+
current: 2,
|
|
21
|
+
itemsSuccessfulInfo: { itemsCreated: 1, itemsUpdated: 1 },
|
|
22
|
+
},
|
|
17
23
|
};
|
|
18
|
-
function setup(clientOverrides = {},
|
|
24
|
+
function setup(clientOverrides = {}, extra = {}) {
|
|
19
25
|
const client = {
|
|
20
26
|
uploadFile: jest.fn(async (_file) => 'file-1'),
|
|
21
27
|
submitImportJob: jest.fn(async () => 'job-1'),
|
|
@@ -32,9 +38,27 @@ function setup(clientOverrides = {}, extraProps = {}) {
|
|
|
32
38
|
const showToast = jest.fn();
|
|
33
39
|
const downloadCsv = jest.fn();
|
|
34
40
|
const translate = (key, params) => params ? `${key}:${JSON.stringify(params)}` : key;
|
|
41
|
+
const { detectColumnType, config: configOverride, collectionId, destination, updateSchema, createFields, fieldManagement, mergeFields, fields, idFieldKey, backup, jobApplicationErrorMap, } = extra;
|
|
42
|
+
const dataSource = destination
|
|
43
|
+
? {
|
|
44
|
+
destination,
|
|
45
|
+
...(createFields && { createFields }),
|
|
46
|
+
...(fieldManagement && { fieldManagement }),
|
|
47
|
+
}
|
|
48
|
+
: {
|
|
49
|
+
collectionId: collectionId ?? 'Movies',
|
|
50
|
+
...(updateSchema && { updateSchema }),
|
|
51
|
+
};
|
|
52
|
+
const config = configOverride ?? {
|
|
53
|
+
fields: fields ?? [{ id: 'name', header: 'Name' }],
|
|
54
|
+
dataSource,
|
|
55
|
+
...(mergeFields && { mergeFields }),
|
|
56
|
+
...(idFieldKey && { idFieldKey }),
|
|
57
|
+
...(backup && { backup }),
|
|
58
|
+
...(jobApplicationErrorMap && { jobApplicationErrorMap }),
|
|
59
|
+
};
|
|
35
60
|
const props = {
|
|
36
|
-
|
|
37
|
-
collectionId: 'Movies',
|
|
61
|
+
config,
|
|
38
62
|
httpClient: {
|
|
39
63
|
request: jest.fn(),
|
|
40
64
|
},
|
|
@@ -46,7 +70,7 @@ function setup(clientOverrides = {}, extraProps = {}) {
|
|
|
46
70
|
},
|
|
47
71
|
showToast: showToast,
|
|
48
72
|
downloadCsv,
|
|
49
|
-
|
|
73
|
+
detectColumnType,
|
|
50
74
|
};
|
|
51
75
|
const state = new ImportState(props);
|
|
52
76
|
// Seed a parsed CSV mapped to an existing field.
|
|
@@ -67,8 +91,8 @@ function setup(clientOverrides = {}, extraProps = {}) {
|
|
|
67
91
|
}
|
|
68
92
|
describe('ImportState engine (data-hub job)', () => {
|
|
69
93
|
beforeEach(() => {
|
|
70
|
-
// The
|
|
71
|
-
//
|
|
94
|
+
// The poll loop awaits real setTimeouts between polls; make sure timers are
|
|
95
|
+
// real even if the preset defaults to fake ones.
|
|
72
96
|
jest.useRealTimers();
|
|
73
97
|
});
|
|
74
98
|
it('uploads the aligned file, submits a job, and reports success', async () => {
|
|
@@ -81,14 +105,53 @@ describe('ImportState engine (data-hub job)', () => {
|
|
|
81
105
|
name: 'data.csv',
|
|
82
106
|
destination: { wixDataCollection: { collectionId: 'Movies' } },
|
|
83
107
|
});
|
|
84
|
-
|
|
108
|
+
// created/updated come from the job's itemsSuccessfulInfo.
|
|
109
|
+
expect(state.result).toEqual({
|
|
110
|
+
succeeded: 2,
|
|
111
|
+
failed: 0,
|
|
112
|
+
created: 1,
|
|
113
|
+
updated: 1,
|
|
114
|
+
});
|
|
85
115
|
expect(state.importStatus.status).toBe('success');
|
|
86
|
-
|
|
116
|
+
// The modal stays open on the summary step; the collection is refreshed now
|
|
117
|
+
// so the table is current once the user closes it via `done`.
|
|
118
|
+
expect(state.currentStep).toBe('summary');
|
|
119
|
+
expect(importModalState.close).not.toHaveBeenCalled();
|
|
87
120
|
expect(collection.clearResultAndMoveToStart).toHaveBeenCalledWith({
|
|
88
121
|
force: true,
|
|
89
122
|
});
|
|
90
123
|
});
|
|
91
|
-
it('
|
|
124
|
+
it('holds the completed (100%) progress bar on the progress step before the summary (CAIRO-4476)', async () => {
|
|
125
|
+
const { state } = setup();
|
|
126
|
+
// The job reports counts only when it finishes, so the bar can only reach
|
|
127
|
+
// 100% at the very end. Capture the step + counts at the moment the linger
|
|
128
|
+
// starts to prove the user sees the completed bar before the summary
|
|
129
|
+
// replaces it.
|
|
130
|
+
let atLinger = null;
|
|
131
|
+
const spy = jest
|
|
132
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
133
|
+
.spyOn(state, '_lingerOnCompletion')
|
|
134
|
+
.mockImplementation(() => {
|
|
135
|
+
atLinger = {
|
|
136
|
+
step: state.currentStep,
|
|
137
|
+
processed: state.processedCount,
|
|
138
|
+
total: state.totalCount,
|
|
139
|
+
};
|
|
140
|
+
return Promise.resolve();
|
|
141
|
+
});
|
|
142
|
+
await state.startImport();
|
|
143
|
+
expect(atLinger).toEqual({ step: 'progress', processed: 2, total: 2 });
|
|
144
|
+
expect(state.currentStep).toBe('summary');
|
|
145
|
+
spy.mockRestore();
|
|
146
|
+
});
|
|
147
|
+
it('closes the modal only when the user dismisses the summary via done', async () => {
|
|
148
|
+
const { state, importModalState } = setup();
|
|
149
|
+
await state.startImport();
|
|
150
|
+
expect(importModalState.close).not.toHaveBeenCalled();
|
|
151
|
+
state.done();
|
|
152
|
+
expect(importModalState.close).toHaveBeenCalled();
|
|
153
|
+
});
|
|
154
|
+
it('shows the summary step (no toast) when some rows are rejected', async () => {
|
|
92
155
|
const { state, showToast, importModalState } = setup({
|
|
93
156
|
getJob: jest.fn(async () => ({
|
|
94
157
|
id: 'job-1',
|
|
@@ -98,16 +161,24 @@ describe('ImportState engine (data-hub job)', () => {
|
|
|
98
161
|
itemsRejected: 1,
|
|
99
162
|
total: 2,
|
|
100
163
|
current: 2,
|
|
164
|
+
itemsSuccessfulInfo: { itemsCreated: 1, itemsUpdated: 0 },
|
|
101
165
|
},
|
|
102
166
|
})),
|
|
103
167
|
});
|
|
104
168
|
await state.startImport();
|
|
105
|
-
expect(state.result).toEqual({
|
|
106
|
-
|
|
107
|
-
|
|
169
|
+
expect(state.result).toEqual({
|
|
170
|
+
succeeded: 1,
|
|
171
|
+
failed: 1,
|
|
172
|
+
created: 1,
|
|
173
|
+
updated: 0,
|
|
174
|
+
});
|
|
175
|
+
expect(state.currentStep).toBe('summary');
|
|
176
|
+
// The completion toast is gone — the summary step reports the outcome.
|
|
177
|
+
expect(showToast).not.toHaveBeenCalled();
|
|
178
|
+
expect(importModalState.close).not.toHaveBeenCalled();
|
|
108
179
|
});
|
|
109
180
|
it('maps the failed "row N" by record (unaffected by an embedded-newline cell)', async () => {
|
|
110
|
-
const { state, downloadCsv
|
|
181
|
+
const { state, downloadCsv } = setup({
|
|
111
182
|
getJob: jest.fn(async () => ({
|
|
112
183
|
id: 'job-1',
|
|
113
184
|
status: JobStatus.PARTIALLY_SUCCESSFUL,
|
|
@@ -137,15 +208,14 @@ describe('ImportState engine (data-hub job)', () => {
|
|
|
137
208
|
};
|
|
138
209
|
});
|
|
139
210
|
await state.startImport();
|
|
140
|
-
|
|
141
|
-
await toast.action.onClick();
|
|
211
|
+
await state.downloadFailedRows();
|
|
142
212
|
// "row 4" → csvRows[2] (Mango), NOT shifted by the embedded newline in row 0.
|
|
143
213
|
const [content] = downloadCsv.mock.calls[0];
|
|
144
214
|
expect(content).toContain('Mango');
|
|
145
215
|
expect(content).not.toContain('Apple');
|
|
146
216
|
});
|
|
147
|
-
it('downloads a re-importable CSV of the failed rows
|
|
148
|
-
const { state, client,
|
|
217
|
+
it('downloads a re-importable CSV of the failed rows, each prefixed with its failure reason', async () => {
|
|
218
|
+
const { state, client, downloadCsv } = setup({
|
|
149
219
|
getJob: jest.fn(async () => ({
|
|
150
220
|
id: 'job-1',
|
|
151
221
|
status: JobStatus.PARTIALLY_SUCCESSFUL,
|
|
@@ -154,18 +224,21 @@ describe('ImportState engine (data-hub job)', () => {
|
|
|
154
224
|
// csvRows[1] (the "Pear" row) failed → file line 3 → "row 3".
|
|
155
225
|
listMovementLogs: jest.fn(async () => ({
|
|
156
226
|
logs: [
|
|
157
|
-
{
|
|
227
|
+
{
|
|
228
|
+
sourceItemId: 'row 3',
|
|
229
|
+
failure: { code: 'bad', description: 'Invalid value' },
|
|
230
|
+
},
|
|
158
231
|
],
|
|
159
232
|
pagingMetadata: { hasNext: false },
|
|
160
233
|
})),
|
|
161
234
|
});
|
|
162
235
|
await state.startImport();
|
|
163
|
-
|
|
164
|
-
const toast = showToast.mock.calls[0][0];
|
|
165
|
-
await toast.action.onClick();
|
|
236
|
+
await state.downloadFailedRows();
|
|
166
237
|
expect(client.listMovementLogs).toHaveBeenCalledWith('job-1', undefined);
|
|
167
|
-
//
|
|
168
|
-
|
|
238
|
+
// A leading reason column (the translated header) + original headers, then
|
|
239
|
+
// the failed row prefixed with the server's failure reason — ready to fix
|
|
240
|
+
// and re-upload. `translate` here echoes the key for the column header.
|
|
241
|
+
expect(downloadCsv).toHaveBeenCalledWith('cairo.import.failureReasonColumn,Name\r\nInvalid value,Pear', 'failed-data.csv');
|
|
169
242
|
});
|
|
170
243
|
it('surfaces an error state with an application-error-shaped error when the job fails', async () => {
|
|
171
244
|
const { state, importModalState, collection } = setup({
|
|
@@ -185,26 +258,21 @@ describe('ImportState engine (data-hub job)', () => {
|
|
|
185
258
|
expect(importModalState.close).not.toHaveBeenCalled();
|
|
186
259
|
expect(collection.clearResultAndMoveToStart).not.toHaveBeenCalled();
|
|
187
260
|
});
|
|
188
|
-
it('
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
// against the real error handler is
|
|
193
|
-
// Table.importButton spec
|
|
194
|
-
|
|
195
|
-
// a passthrough stub, so this just runs `run` and re-rejects.
|
|
196
|
-
const withErrorHandler = (fn) => fn();
|
|
197
|
-
const onJobError = jest.fn((run) => withErrorHandler(run));
|
|
261
|
+
it('resolves a failed job through the consumer jobApplicationErrorMap', async () => {
|
|
262
|
+
// ImportState wraps the failure with `errorHandler.withErrorHandler(run,
|
|
263
|
+
// jobApplicationErrorMap)` itself; the consumer only supplies the per-code
|
|
264
|
+
// map. The surfaced error keeps the application-error-shaped code intact.
|
|
265
|
+
// End-to-end resolution against the real error handler (getResolvedError) is
|
|
266
|
+
// covered in the cairo Table.importButton spec.
|
|
267
|
+
const jobApplicationErrorMap = { duplicate_key: () => ({ message: 'Bad rows' }) };
|
|
198
268
|
const { state } = setup({
|
|
199
269
|
getJob: jest.fn(async () => ({
|
|
200
270
|
id: 'job-1',
|
|
201
271
|
status: JobStatus.FAILED,
|
|
202
272
|
error: { code: 'duplicate_key', description: 'Bad rows' },
|
|
203
273
|
})),
|
|
204
|
-
}, {
|
|
274
|
+
}, { jobApplicationErrorMap });
|
|
205
275
|
await state.startImport();
|
|
206
|
-
expect(onJobError).toHaveBeenCalledTimes(1);
|
|
207
|
-
expect(typeof onJobError.mock.calls[0][0]).toBe('function');
|
|
208
276
|
expect(state.importStatus.status).toBe('error');
|
|
209
277
|
const error = state.importStatus.error;
|
|
210
278
|
expect(error.response?.data?.details?.applicationError?.code).toBe('duplicate_key');
|
|
@@ -246,30 +314,20 @@ describe('ImportState engine (data-hub job)', () => {
|
|
|
246
314
|
// forever; it must stop after the first fetch.
|
|
247
315
|
expect(client.getJob).toHaveBeenCalledTimes(1);
|
|
248
316
|
});
|
|
249
|
-
it('does not restore
|
|
317
|
+
it('does not restore a completed import when the summary is closed', async () => {
|
|
250
318
|
const restoreFn = jest.fn().mockResolvedValue(undefined);
|
|
251
319
|
const { state, importModalState, collection } = setup({}, { backup: async () => ({ restore: restoreFn }) });
|
|
252
|
-
|
|
253
|
-
//
|
|
254
|
-
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
255
|
-
const promise = state.startImport();
|
|
256
|
-
// Run backup → upload → submit → poll (terminal) → success, parking on the
|
|
257
|
-
// 600ms completion linger (still on the 'progress' step) without firing it.
|
|
258
|
-
await flush();
|
|
259
|
-
await flush();
|
|
260
|
-
await flush();
|
|
320
|
+
await state.startImport();
|
|
321
|
+
// The import finished on the summary step and the collection was refreshed.
|
|
261
322
|
expect(state.importStatus.isSuccess).toBe(true);
|
|
262
|
-
expect(state.currentStep).toBe('
|
|
263
|
-
// User closes the modal mid-linger.
|
|
264
|
-
await state.close();
|
|
265
|
-
await promise;
|
|
266
|
-
// A successful import must not be reverted; the modal closes and the
|
|
267
|
-
// collection refreshes.
|
|
268
|
-
expect(restoreFn).not.toHaveBeenCalled();
|
|
269
|
-
expect(importModalState.close).toHaveBeenCalled();
|
|
323
|
+
expect(state.currentStep).toBe('summary');
|
|
270
324
|
expect(collection.clearResultAndMoveToStart).toHaveBeenCalledWith({
|
|
271
325
|
force: true,
|
|
272
326
|
});
|
|
327
|
+
// Closing the summary must not revert a successful import.
|
|
328
|
+
await state.close();
|
|
329
|
+
expect(restoreFn).not.toHaveBeenCalled();
|
|
330
|
+
expect(importModalState.close).toHaveBeenCalled();
|
|
273
331
|
});
|
|
274
332
|
});
|
|
275
333
|
describe('ImportState writePolicy (id-collision handling)', () => {
|
|
@@ -470,5 +528,334 @@ describe('ImportState platformized destination (non–Wix-Data)', () => {
|
|
|
470
528
|
const wixData = setup().state;
|
|
471
529
|
expect(wixData.canConfigureWritePolicy).toBe(true);
|
|
472
530
|
});
|
|
531
|
+
describe('DataExtension create fields (destination branch)', () => {
|
|
532
|
+
it('enables create-new-field when the destination branch supplies createFields', () => {
|
|
533
|
+
const withCreate = setup({}, {
|
|
534
|
+
destination: { storesCatalogProducts: {} },
|
|
535
|
+
createFields: jest.fn(async () => undefined),
|
|
536
|
+
}).state;
|
|
537
|
+
expect(withCreate.canCreateNewField).toBe(true);
|
|
538
|
+
const withoutCreate = setup({}, { destination: { storesCatalogProducts: {} } }).state;
|
|
539
|
+
expect(withoutCreate.canCreateNewField).toBe(false);
|
|
540
|
+
});
|
|
541
|
+
it('hands createFields the deduped pending columns (source builds its own def)', async () => {
|
|
542
|
+
const createFields = jest.fn(async () => undefined);
|
|
543
|
+
const { state, client } = setup({}, { destination: { storesCatalogProducts: {} }, createFields });
|
|
544
|
+
runInAction(() => {
|
|
545
|
+
state.csvHeaders = ['Name', 'Color'];
|
|
546
|
+
state.csvRows = [
|
|
547
|
+
['Apple', 'Red'],
|
|
548
|
+
['Pear', 'Green'],
|
|
549
|
+
];
|
|
550
|
+
state.mappingByHeader = {
|
|
551
|
+
Name: { kind: 'existing', fieldId: 'name' },
|
|
552
|
+
Color: { kind: 'new', fieldType: 'text' },
|
|
553
|
+
};
|
|
554
|
+
});
|
|
555
|
+
await state.startImport();
|
|
556
|
+
expect(createFields).toHaveBeenCalledTimes(1);
|
|
557
|
+
// Core passes the neutral pending column (deduped key + name + the mapping);
|
|
558
|
+
// the DataExtension source turns it into its own field shape.
|
|
559
|
+
expect(createFields).toHaveBeenCalledWith([
|
|
560
|
+
{
|
|
561
|
+
key: 'color',
|
|
562
|
+
displayName: 'Color',
|
|
563
|
+
mapping: { kind: 'new', fieldType: 'text' },
|
|
564
|
+
},
|
|
565
|
+
]);
|
|
566
|
+
// The consumer's destination is still sent verbatim.
|
|
567
|
+
expect(client.submitImportJob).toHaveBeenCalledWith(expect.objectContaining({
|
|
568
|
+
destination: { storesCatalogProducts: {} },
|
|
569
|
+
}));
|
|
570
|
+
});
|
|
571
|
+
it('carries the edited mapping config (precise type, permissions, PII) in the pending column', async () => {
|
|
572
|
+
const createFields = jest.fn(async () => undefined);
|
|
573
|
+
const { state } = setup({}, { destination: { storesCatalogProducts: {} }, createFields });
|
|
574
|
+
const config = {
|
|
575
|
+
deType: 'decimal',
|
|
576
|
+
isPii: true,
|
|
577
|
+
permissions: {
|
|
578
|
+
installedApps: { defaultRead: true, defaultWrite: false },
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
runInAction(() => {
|
|
582
|
+
state.csvHeaders = ['Name', 'Count'];
|
|
583
|
+
state.csvRows = [['Apple', '3']];
|
|
584
|
+
state.mappingByHeader = {
|
|
585
|
+
Name: { kind: 'existing', fieldId: 'name' },
|
|
586
|
+
Count: {
|
|
587
|
+
kind: 'new',
|
|
588
|
+
fieldType: 'number',
|
|
589
|
+
displayName: 'Count',
|
|
590
|
+
config,
|
|
591
|
+
},
|
|
592
|
+
};
|
|
593
|
+
});
|
|
594
|
+
await state.startImport();
|
|
595
|
+
expect(createFields).toHaveBeenCalledWith([
|
|
596
|
+
{
|
|
597
|
+
key: 'count',
|
|
598
|
+
displayName: 'Count',
|
|
599
|
+
mapping: {
|
|
600
|
+
kind: 'new',
|
|
601
|
+
fieldType: 'number',
|
|
602
|
+
displayName: 'Count',
|
|
603
|
+
config,
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
]);
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
describe('created fields (CAIRO-4400)', () => {
|
|
610
|
+
const RETURNED_FIELDS = {
|
|
611
|
+
name: { id: 'name', displayName: 'Name', type: 'SHORT_TEXT' },
|
|
612
|
+
color: { id: 'color', displayName: 'Color', type: 'SHORT_TEXT' },
|
|
613
|
+
};
|
|
614
|
+
// Maps an extra column to a NEW field so `_resolveFieldMapping` calls
|
|
615
|
+
// `updateSchema`. `updateSchema`'s presence is also what enables new mappings.
|
|
616
|
+
function withNewFieldMapping(state) {
|
|
617
|
+
runInAction(() => {
|
|
618
|
+
state.csvHeaders = ['Name', 'Color'];
|
|
619
|
+
state.csvRows = [
|
|
620
|
+
['Apple', 'Red'],
|
|
621
|
+
['Pear', 'Green'],
|
|
622
|
+
];
|
|
623
|
+
state.mappingByHeader = {
|
|
624
|
+
Name: { kind: 'existing', fieldId: 'name' },
|
|
625
|
+
Color: { kind: 'new', fieldType: 'text' },
|
|
626
|
+
};
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
it('calls mergeFields with updateSchema’s returned map after a successful import', async () => {
|
|
630
|
+
const mergeFields = jest.fn();
|
|
631
|
+
const updateSchema = jest.fn(async () => RETURNED_FIELDS);
|
|
632
|
+
const { state, collection } = setup({}, { updateSchema, mergeFields });
|
|
633
|
+
withNewFieldMapping(state);
|
|
634
|
+
await state.startImport();
|
|
635
|
+
expect(updateSchema).toHaveBeenCalledTimes(1);
|
|
636
|
+
expect(mergeFields).toHaveBeenCalledTimes(1);
|
|
637
|
+
expect(mergeFields).toHaveBeenCalledWith(RETURNED_FIELDS);
|
|
638
|
+
// The merge runs alongside the data refetch on the success path.
|
|
639
|
+
expect(collection.clearResultAndMoveToStart).toHaveBeenCalled();
|
|
640
|
+
});
|
|
641
|
+
it('does not call mergeFields for a plain import (no new fields)', async () => {
|
|
642
|
+
const mergeFields = jest.fn();
|
|
643
|
+
const updateSchema = jest.fn(async () => RETURNED_FIELDS);
|
|
644
|
+
const { state } = setup({}, { updateSchema, mergeFields });
|
|
645
|
+
await state.startImport();
|
|
646
|
+
expect(updateSchema).not.toHaveBeenCalled();
|
|
647
|
+
expect(mergeFields).not.toHaveBeenCalled();
|
|
648
|
+
});
|
|
649
|
+
it('does not call mergeFields when updateSchema returns nothing', async () => {
|
|
650
|
+
const mergeFields = jest.fn();
|
|
651
|
+
const updateSchema = jest.fn(async () => undefined);
|
|
652
|
+
const { state } = setup({}, { updateSchema, mergeFields });
|
|
653
|
+
withNewFieldMapping(state);
|
|
654
|
+
await state.startImport();
|
|
655
|
+
expect(updateSchema).toHaveBeenCalledTimes(1);
|
|
656
|
+
expect(mergeFields).not.toHaveBeenCalled();
|
|
657
|
+
});
|
|
658
|
+
it('does not call mergeFields when updateSchema returns an empty map', async () => {
|
|
659
|
+
// A `{}` return is truthy; without a size guard it would reach a full-replace
|
|
660
|
+
// mergeFields({}) and wipe every column. Treat it as "nothing to merge".
|
|
661
|
+
const mergeFields = jest.fn();
|
|
662
|
+
const updateSchema = jest.fn(async () => ({}));
|
|
663
|
+
const { state } = setup({}, { updateSchema, mergeFields });
|
|
664
|
+
withNewFieldMapping(state);
|
|
665
|
+
await state.startImport();
|
|
666
|
+
expect(updateSchema).toHaveBeenCalledTimes(1);
|
|
667
|
+
expect(mergeFields).not.toHaveBeenCalled();
|
|
668
|
+
});
|
|
669
|
+
it('does not call mergeFields when the import fails after creating fields', async () => {
|
|
670
|
+
const mergeFields = jest.fn();
|
|
671
|
+
const updateSchema = jest.fn(async () => RETURNED_FIELDS);
|
|
672
|
+
const { state } = setup({
|
|
673
|
+
uploadFile: jest.fn(async () => {
|
|
674
|
+
throw new Error('upload failed');
|
|
675
|
+
}),
|
|
676
|
+
}, { updateSchema, mergeFields });
|
|
677
|
+
withNewFieldMapping(state);
|
|
678
|
+
await state.startImport();
|
|
679
|
+
// Fields were created pre-upload, but a failed import must not merge them.
|
|
680
|
+
expect(updateSchema).toHaveBeenCalledTimes(1);
|
|
681
|
+
expect(state.importStatus.status).toBe('error');
|
|
682
|
+
expect(mergeFields).not.toHaveBeenCalled();
|
|
683
|
+
});
|
|
684
|
+
it('keeps the import successful when mergeFields throws (no void → duplicate-rows)', async () => {
|
|
685
|
+
const consoleError = jest
|
|
686
|
+
.spyOn(console, 'error')
|
|
687
|
+
.mockImplementation(() => { });
|
|
688
|
+
const mergeFields = jest.fn(() => {
|
|
689
|
+
throw new Error('merge boom');
|
|
690
|
+
});
|
|
691
|
+
const updateSchema = jest.fn(async () => RETURNED_FIELDS);
|
|
692
|
+
const { state, collection } = setup({}, { updateSchema, mergeFields });
|
|
693
|
+
withNewFieldMapping(state);
|
|
694
|
+
await state.startImport();
|
|
695
|
+
expect(mergeFields).toHaveBeenCalledTimes(1);
|
|
696
|
+
// A throwing merge is swallowed — the completed import is untouched.
|
|
697
|
+
expect(state.importStatus.status).toBe('success');
|
|
698
|
+
expect(state.currentStep).toBe('summary');
|
|
699
|
+
expect(collection.clearResultAndMoveToStart).toHaveBeenCalled();
|
|
700
|
+
consoleError.mockRestore();
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
describe('schema-backed config (live getters + create-field permission)', () => {
|
|
704
|
+
// A schema-backed config: the CMS `dataSource` branch a SchemaImportRef
|
|
705
|
+
// satisfies. It supplies `updateSchema` only when create is permitted — its
|
|
706
|
+
// presence is the create-new-field opt-in (no separate flag).
|
|
707
|
+
const schemaConfig = (over = {}) => ({
|
|
708
|
+
fields: over.fields ?? [{ id: 'name', header: 'Name' }],
|
|
709
|
+
dataSource: {
|
|
710
|
+
collectionId: over.collectionId ?? 'RefCollection',
|
|
711
|
+
...(over.updateSchema && { updateSchema: over.updateSchema }),
|
|
712
|
+
},
|
|
713
|
+
...(over.idFieldKey && { idFieldKey: over.idFieldKey }),
|
|
714
|
+
...(over.mergeFields && { mergeFields: over.mergeFields }),
|
|
715
|
+
});
|
|
716
|
+
it('reads fields live from the config rather than snapshotting them', () => {
|
|
717
|
+
const config = schemaConfig();
|
|
718
|
+
const { state } = setup({}, { config });
|
|
719
|
+
expect(state.fields).toBe(config.fields);
|
|
720
|
+
// A later change to the config is reflected — values are read on demand,
|
|
721
|
+
// so a live (getter-backed) config needn't be resolved before the ctor.
|
|
722
|
+
const updated = [
|
|
723
|
+
{ id: 'name', header: 'Name' },
|
|
724
|
+
{ id: 'year', header: 'Year' },
|
|
725
|
+
];
|
|
726
|
+
config.fields = updated;
|
|
727
|
+
expect(state.fields).toBe(updated);
|
|
728
|
+
});
|
|
729
|
+
it('gates create-new-field on the presence of a create callback (updateSchema)', () => {
|
|
730
|
+
// No create callback ⇒ create-new-field off (the source didn't permit it).
|
|
731
|
+
const off = setup({}, { config: schemaConfig() });
|
|
732
|
+
expect(off.state.canCreateNewField).toBe(false);
|
|
733
|
+
// A create callback present ⇒ on.
|
|
734
|
+
const on = setup({}, { config: schemaConfig({ updateSchema: jest.fn() }) });
|
|
735
|
+
expect(on.state.canCreateNewField).toBe(true);
|
|
736
|
+
});
|
|
737
|
+
it('builds the destination from the config collectionId', async () => {
|
|
738
|
+
const { state, client } = setup({}, { config: schemaConfig({ collectionId: 'RefCollection' }) });
|
|
739
|
+
await state.startImport();
|
|
740
|
+
expect(client.submitImportJob).toHaveBeenCalledWith(expect.objectContaining({
|
|
741
|
+
destination: { wixDataCollection: { collectionId: 'RefCollection' } },
|
|
742
|
+
}));
|
|
743
|
+
});
|
|
744
|
+
it('creates new fields through the config updateSchema', async () => {
|
|
745
|
+
const updateSchema = jest.fn(async () => ({
|
|
746
|
+
name: { id: 'name', displayName: 'Name', type: 'SHORT_TEXT' },
|
|
747
|
+
year: { id: 'year', displayName: 'Year', type: 'NUMBER' },
|
|
748
|
+
}));
|
|
749
|
+
const { state } = setup({}, { config: schemaConfig({ updateSchema }) });
|
|
750
|
+
runInAction(() => {
|
|
751
|
+
state.csvHeaders = ['Name', 'Year'];
|
|
752
|
+
state.csvRows = [['Apple', '1999']];
|
|
753
|
+
state.mappingByHeader = {
|
|
754
|
+
Name: { kind: 'existing', fieldId: 'name' },
|
|
755
|
+
Year: { kind: 'new', fieldType: 'number' },
|
|
756
|
+
};
|
|
757
|
+
});
|
|
758
|
+
await state.startImport();
|
|
759
|
+
expect(updateSchema).toHaveBeenCalledTimes(1);
|
|
760
|
+
});
|
|
761
|
+
it('detects the id-mapped column using the config idFieldKey', () => {
|
|
762
|
+
const { state } = setup({}, { config: schemaConfig({ idFieldKey: 'movieId' }) });
|
|
763
|
+
runInAction(() => {
|
|
764
|
+
state.mappingByHeader = {
|
|
765
|
+
Ref: { kind: 'existing', fieldId: 'movieId' },
|
|
766
|
+
};
|
|
767
|
+
});
|
|
768
|
+
expect(state.idMappedHeader).toBe('Ref');
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
describe('ImportState required fields (CAIRO-4452)', () => {
|
|
773
|
+
beforeEach(() => {
|
|
774
|
+
jest.useRealTimers();
|
|
775
|
+
});
|
|
776
|
+
it('is satisfied when a required field has a column mapped to it', () => {
|
|
777
|
+
const { state } = setup({}, {
|
|
778
|
+
config: {
|
|
779
|
+
fields: [{ id: 'name', header: 'Name', required: true }],
|
|
780
|
+
dataSource: { collectionId: 'Movies' },
|
|
781
|
+
},
|
|
782
|
+
});
|
|
783
|
+
// The seeded mapping maps the "Name" column to the `name` field.
|
|
784
|
+
expect(state.hasMissingRequiredFields).toBe(false);
|
|
785
|
+
expect(state.missingRequiredFields).toEqual([]);
|
|
786
|
+
});
|
|
787
|
+
it('flags a required field the user set to "don\'t import"', () => {
|
|
788
|
+
const { state } = setup({}, {
|
|
789
|
+
config: {
|
|
790
|
+
fields: [{ id: 'name', header: 'Name', required: true }],
|
|
791
|
+
dataSource: { collectionId: 'Movies' },
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
state.setMapping('Name', null);
|
|
795
|
+
expect(state.hasMissingRequiredFields).toBe(true);
|
|
796
|
+
expect(state.missingRequiredFields.map((f) => f.id)).toEqual(['name']);
|
|
797
|
+
});
|
|
798
|
+
it('ignores unmapped fields that are not required', () => {
|
|
799
|
+
const { state } = setup({}, {
|
|
800
|
+
config: {
|
|
801
|
+
fields: [
|
|
802
|
+
{ id: 'name', header: 'Name' },
|
|
803
|
+
{ id: 'year', header: 'Year', required: false },
|
|
804
|
+
],
|
|
805
|
+
dataSource: { collectionId: 'Movies' },
|
|
806
|
+
},
|
|
807
|
+
});
|
|
808
|
+
state.setMapping('Name', null);
|
|
809
|
+
expect(state.hasMissingRequiredFields).toBe(false);
|
|
810
|
+
});
|
|
811
|
+
it('is not satisfied by a create-new-field mapping', () => {
|
|
812
|
+
const { state } = setup({}, {
|
|
813
|
+
config: {
|
|
814
|
+
fields: [{ id: 'name', header: 'Name', required: true }],
|
|
815
|
+
dataSource: {
|
|
816
|
+
collectionId: 'Movies',
|
|
817
|
+
updateSchema: async () => undefined,
|
|
818
|
+
},
|
|
819
|
+
},
|
|
820
|
+
});
|
|
821
|
+
// Mapping the column to a brand-new field can't fill the existing required
|
|
822
|
+
// field, so it stays missing.
|
|
823
|
+
state.setNewFieldMapping('Name');
|
|
824
|
+
expect(state.hasMissingRequiredFields).toBe(true);
|
|
825
|
+
});
|
|
826
|
+
it('blocks startImport and shows an error toast when a required field is missing', async () => {
|
|
827
|
+
const { state, client, showToast } = setup({}, {
|
|
828
|
+
config: {
|
|
829
|
+
fields: [
|
|
830
|
+
{ id: 'name', header: 'Name', required: true },
|
|
831
|
+
{ id: 'year', header: 'Year' },
|
|
832
|
+
],
|
|
833
|
+
dataSource: { collectionId: 'Movies' },
|
|
834
|
+
},
|
|
835
|
+
});
|
|
836
|
+
// Keep a mapping (so the import isn't blocked by "no mappings") but leave the
|
|
837
|
+
// required `name` field unmapped.
|
|
838
|
+
runInAction(() => {
|
|
839
|
+
state.csvHeaders = ['Name', 'Year'];
|
|
840
|
+
state.csvRows = [['Apple', '2020']];
|
|
841
|
+
});
|
|
842
|
+
state.setMapping('Name', null);
|
|
843
|
+
state.setMapping('Year', 'year');
|
|
844
|
+
await state.startImport();
|
|
845
|
+
expect(client.uploadFile).not.toHaveBeenCalled();
|
|
846
|
+
expect(client.submitImportJob).not.toHaveBeenCalled();
|
|
847
|
+
expect(state.currentStep).not.toBe('progress');
|
|
848
|
+
expect(showToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'ERROR' }));
|
|
849
|
+
});
|
|
850
|
+
it('proceeds with import when all required fields are mapped', async () => {
|
|
851
|
+
const { state, client } = setup({}, {
|
|
852
|
+
config: {
|
|
853
|
+
fields: [{ id: 'name', header: 'Name', required: true }],
|
|
854
|
+
dataSource: { collectionId: 'Movies' },
|
|
855
|
+
},
|
|
856
|
+
});
|
|
857
|
+
await state.startImport();
|
|
858
|
+
expect(client.submitImportJob).toHaveBeenCalledTimes(1);
|
|
859
|
+
});
|
|
473
860
|
});
|
|
474
861
|
//# sourceMappingURL=ImportState.engine.spec.js.map
|