@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
|
@@ -19,10 +19,19 @@ const COMPLETED_JOB = {
|
|
|
19
19
|
itemsSuccessful: 2,
|
|
20
20
|
itemsRejected: 0,
|
|
21
21
|
total: 2,
|
|
22
|
-
current: 2
|
|
22
|
+
current: 2,
|
|
23
|
+
itemsSuccessfulInfo: {
|
|
24
|
+
itemsCreated: 1,
|
|
25
|
+
itemsUpdated: 1
|
|
26
|
+
}
|
|
23
27
|
}
|
|
24
28
|
};
|
|
25
|
-
|
|
29
|
+
|
|
30
|
+
// Convenience flat overrides, assembled into the `dataSource` union below;
|
|
31
|
+
// `config` fully replaces the assembled config (for schema-backed configs).
|
|
32
|
+
// `detectColumnType` is an infra prop.
|
|
33
|
+
|
|
34
|
+
function setup(clientOverrides = {}, extra = {}) {
|
|
26
35
|
const client = {
|
|
27
36
|
uploadFile: jest.fn(async _file => 'file-1'),
|
|
28
37
|
submitImportJob: jest.fn(async () => 'job-1'),
|
|
@@ -41,12 +50,55 @@ function setup(clientOverrides = {}, extraProps = {}) {
|
|
|
41
50
|
const showToast = jest.fn();
|
|
42
51
|
const downloadCsv = jest.fn();
|
|
43
52
|
const translate = (key, params) => params ? `${key}:${JSON.stringify(params)}` : key;
|
|
44
|
-
const
|
|
45
|
-
|
|
53
|
+
const {
|
|
54
|
+
detectColumnType,
|
|
55
|
+
config: configOverride,
|
|
56
|
+
collectionId,
|
|
57
|
+
destination,
|
|
58
|
+
updateSchema,
|
|
59
|
+
createFields,
|
|
60
|
+
fieldManagement,
|
|
61
|
+
mergeFields,
|
|
62
|
+
fields,
|
|
63
|
+
idFieldKey,
|
|
64
|
+
backup,
|
|
65
|
+
jobApplicationErrorMap
|
|
66
|
+
} = extra;
|
|
67
|
+
const dataSource = destination ? {
|
|
68
|
+
destination,
|
|
69
|
+
...(createFields && {
|
|
70
|
+
createFields
|
|
71
|
+
}),
|
|
72
|
+
...(fieldManagement && {
|
|
73
|
+
fieldManagement
|
|
74
|
+
})
|
|
75
|
+
} : {
|
|
76
|
+
collectionId: collectionId ?? 'Movies',
|
|
77
|
+
...(updateSchema && {
|
|
78
|
+
updateSchema
|
|
79
|
+
})
|
|
80
|
+
};
|
|
81
|
+
const config = configOverride ?? {
|
|
82
|
+
fields: fields ?? [{
|
|
46
83
|
id: 'name',
|
|
47
84
|
header: 'Name'
|
|
48
85
|
}],
|
|
49
|
-
|
|
86
|
+
dataSource,
|
|
87
|
+
...(mergeFields && {
|
|
88
|
+
mergeFields
|
|
89
|
+
}),
|
|
90
|
+
...(idFieldKey && {
|
|
91
|
+
idFieldKey
|
|
92
|
+
}),
|
|
93
|
+
...(backup && {
|
|
94
|
+
backup
|
|
95
|
+
}),
|
|
96
|
+
...(jobApplicationErrorMap && {
|
|
97
|
+
jobApplicationErrorMap
|
|
98
|
+
})
|
|
99
|
+
};
|
|
100
|
+
const props = {
|
|
101
|
+
config,
|
|
50
102
|
httpClient: {
|
|
51
103
|
request: jest.fn()
|
|
52
104
|
},
|
|
@@ -58,7 +110,7 @@ function setup(clientOverrides = {}, extraProps = {}) {
|
|
|
58
110
|
},
|
|
59
111
|
showToast: showToast,
|
|
60
112
|
downloadCsv,
|
|
61
|
-
|
|
113
|
+
detectColumnType
|
|
62
114
|
};
|
|
63
115
|
const state = new _ImportState.ImportState(props);
|
|
64
116
|
|
|
@@ -85,8 +137,8 @@ function setup(clientOverrides = {}, extraProps = {}) {
|
|
|
85
137
|
}
|
|
86
138
|
describe('ImportState engine (data-hub job)', () => {
|
|
87
139
|
beforeEach(() => {
|
|
88
|
-
// The
|
|
89
|
-
//
|
|
140
|
+
// The poll loop awaits real setTimeouts between polls; make sure timers are
|
|
141
|
+
// real even if the preset defaults to fake ones.
|
|
90
142
|
jest.useRealTimers();
|
|
91
143
|
});
|
|
92
144
|
it('uploads the aligned file, submits a job, and reports success', async () => {
|
|
@@ -108,17 +160,63 @@ describe('ImportState engine (data-hub job)', () => {
|
|
|
108
160
|
}
|
|
109
161
|
}
|
|
110
162
|
});
|
|
163
|
+
|
|
164
|
+
// created/updated come from the job's itemsSuccessfulInfo.
|
|
111
165
|
expect(state.result).toEqual({
|
|
112
166
|
succeeded: 2,
|
|
113
|
-
failed: 0
|
|
167
|
+
failed: 0,
|
|
168
|
+
created: 1,
|
|
169
|
+
updated: 1
|
|
114
170
|
});
|
|
115
171
|
expect(state.importStatus.status).toBe('success');
|
|
116
|
-
|
|
172
|
+
// The modal stays open on the summary step; the collection is refreshed now
|
|
173
|
+
// so the table is current once the user closes it via `done`.
|
|
174
|
+
expect(state.currentStep).toBe('summary');
|
|
175
|
+
expect(importModalState.close).not.toHaveBeenCalled();
|
|
117
176
|
expect(collection.clearResultAndMoveToStart).toHaveBeenCalledWith({
|
|
118
177
|
force: true
|
|
119
178
|
});
|
|
120
179
|
});
|
|
121
|
-
it('
|
|
180
|
+
it('holds the completed (100%) progress bar on the progress step before the summary (CAIRO-4476)', async () => {
|
|
181
|
+
const {
|
|
182
|
+
state
|
|
183
|
+
} = setup();
|
|
184
|
+
|
|
185
|
+
// The job reports counts only when it finishes, so the bar can only reach
|
|
186
|
+
// 100% at the very end. Capture the step + counts at the moment the linger
|
|
187
|
+
// starts to prove the user sees the completed bar before the summary
|
|
188
|
+
// replaces it.
|
|
189
|
+
let atLinger = null;
|
|
190
|
+
const spy = jest
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
192
|
+
.spyOn(state, '_lingerOnCompletion').mockImplementation(() => {
|
|
193
|
+
atLinger = {
|
|
194
|
+
step: state.currentStep,
|
|
195
|
+
processed: state.processedCount,
|
|
196
|
+
total: state.totalCount
|
|
197
|
+
};
|
|
198
|
+
return Promise.resolve();
|
|
199
|
+
});
|
|
200
|
+
await state.startImport();
|
|
201
|
+
expect(atLinger).toEqual({
|
|
202
|
+
step: 'progress',
|
|
203
|
+
processed: 2,
|
|
204
|
+
total: 2
|
|
205
|
+
});
|
|
206
|
+
expect(state.currentStep).toBe('summary');
|
|
207
|
+
spy.mockRestore();
|
|
208
|
+
});
|
|
209
|
+
it('closes the modal only when the user dismisses the summary via done', async () => {
|
|
210
|
+
const {
|
|
211
|
+
state,
|
|
212
|
+
importModalState
|
|
213
|
+
} = setup();
|
|
214
|
+
await state.startImport();
|
|
215
|
+
expect(importModalState.close).not.toHaveBeenCalled();
|
|
216
|
+
state.done();
|
|
217
|
+
expect(importModalState.close).toHaveBeenCalled();
|
|
218
|
+
});
|
|
219
|
+
it('shows the summary step (no toast) when some rows are rejected', async () => {
|
|
122
220
|
const {
|
|
123
221
|
state,
|
|
124
222
|
showToast,
|
|
@@ -131,25 +229,30 @@ describe('ImportState engine (data-hub job)', () => {
|
|
|
131
229
|
itemsSuccessful: 1,
|
|
132
230
|
itemsRejected: 1,
|
|
133
231
|
total: 2,
|
|
134
|
-
current: 2
|
|
232
|
+
current: 2,
|
|
233
|
+
itemsSuccessfulInfo: {
|
|
234
|
+
itemsCreated: 1,
|
|
235
|
+
itemsUpdated: 0
|
|
236
|
+
}
|
|
135
237
|
}
|
|
136
238
|
}))
|
|
137
239
|
});
|
|
138
240
|
await state.startImport();
|
|
139
241
|
expect(state.result).toEqual({
|
|
140
242
|
succeeded: 1,
|
|
141
|
-
failed: 1
|
|
243
|
+
failed: 1,
|
|
244
|
+
created: 1,
|
|
245
|
+
updated: 0
|
|
142
246
|
});
|
|
143
|
-
expect(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
expect(importModalState.close).toHaveBeenCalled();
|
|
247
|
+
expect(state.currentStep).toBe('summary');
|
|
248
|
+
// The completion toast is gone — the summary step reports the outcome.
|
|
249
|
+
expect(showToast).not.toHaveBeenCalled();
|
|
250
|
+
expect(importModalState.close).not.toHaveBeenCalled();
|
|
147
251
|
});
|
|
148
252
|
it('maps the failed "row N" by record (unaffected by an embedded-newline cell)', async () => {
|
|
149
253
|
const {
|
|
150
254
|
state,
|
|
151
|
-
downloadCsv
|
|
152
|
-
showToast
|
|
255
|
+
downloadCsv
|
|
153
256
|
} = setup({
|
|
154
257
|
getJob: jest.fn(async () => ({
|
|
155
258
|
id: 'job-1',
|
|
@@ -192,19 +295,17 @@ describe('ImportState engine (data-hub job)', () => {
|
|
|
192
295
|
};
|
|
193
296
|
});
|
|
194
297
|
await state.startImport();
|
|
195
|
-
|
|
196
|
-
await toast.action.onClick();
|
|
298
|
+
await state.downloadFailedRows();
|
|
197
299
|
|
|
198
300
|
// "row 4" → csvRows[2] (Mango), NOT shifted by the embedded newline in row 0.
|
|
199
301
|
const [content] = downloadCsv.mock.calls[0];
|
|
200
302
|
expect(content).toContain('Mango');
|
|
201
303
|
expect(content).not.toContain('Apple');
|
|
202
304
|
});
|
|
203
|
-
it('downloads a re-importable CSV of the failed rows
|
|
305
|
+
it('downloads a re-importable CSV of the failed rows, each prefixed with its failure reason', async () => {
|
|
204
306
|
const {
|
|
205
307
|
state,
|
|
206
308
|
client,
|
|
207
|
-
showToast,
|
|
208
309
|
downloadCsv
|
|
209
310
|
} = setup({
|
|
210
311
|
getJob: jest.fn(async () => ({
|
|
@@ -221,7 +322,7 @@ describe('ImportState engine (data-hub job)', () => {
|
|
|
221
322
|
sourceItemId: 'row 3',
|
|
222
323
|
failure: {
|
|
223
324
|
code: 'bad',
|
|
224
|
-
description: '
|
|
325
|
+
description: 'Invalid value'
|
|
225
326
|
}
|
|
226
327
|
}],
|
|
227
328
|
pagingMetadata: {
|
|
@@ -230,13 +331,12 @@ describe('ImportState engine (data-hub job)', () => {
|
|
|
230
331
|
}))
|
|
231
332
|
});
|
|
232
333
|
await state.startImport();
|
|
233
|
-
|
|
234
|
-
// Trigger the toast's download action.
|
|
235
|
-
const toast = showToast.mock.calls[0][0];
|
|
236
|
-
await toast.action.onClick();
|
|
334
|
+
await state.downloadFailedRows();
|
|
237
335
|
expect(client.listMovementLogs).toHaveBeenCalledWith('job-1', undefined);
|
|
238
|
-
//
|
|
239
|
-
|
|
336
|
+
// A leading reason column (the translated header) + original headers, then
|
|
337
|
+
// the failed row prefixed with the server's failure reason — ready to fix
|
|
338
|
+
// and re-upload. `translate` here echoes the key for the column header.
|
|
339
|
+
expect(downloadCsv).toHaveBeenCalledWith('cairo.import.failureReasonColumn,Name\r\nInvalid value,Pear', 'failed-data.csv');
|
|
240
340
|
});
|
|
241
341
|
it('surfaces an error state with an application-error-shaped error when the job fails', async () => {
|
|
242
342
|
var _error$response;
|
|
@@ -264,17 +364,18 @@ describe('ImportState engine (data-hub job)', () => {
|
|
|
264
364
|
expect(importModalState.close).not.toHaveBeenCalled();
|
|
265
365
|
expect(collection.clearResultAndMoveToStart).not.toHaveBeenCalled();
|
|
266
366
|
});
|
|
267
|
-
it('
|
|
367
|
+
it('resolves a failed job through the consumer jobApplicationErrorMap', async () => {
|
|
268
368
|
var _error$response2;
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
// against the real error handler is
|
|
273
|
-
// Table.importButton spec
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
369
|
+
// ImportState wraps the failure with `errorHandler.withErrorHandler(run,
|
|
370
|
+
// jobApplicationErrorMap)` itself; the consumer only supplies the per-code
|
|
371
|
+
// map. The surfaced error keeps the application-error-shaped code intact.
|
|
372
|
+
// End-to-end resolution against the real error handler (getResolvedError) is
|
|
373
|
+
// covered in the cairo Table.importButton spec.
|
|
374
|
+
const jobApplicationErrorMap = {
|
|
375
|
+
duplicate_key: () => ({
|
|
376
|
+
message: 'Bad rows'
|
|
377
|
+
})
|
|
378
|
+
};
|
|
278
379
|
const {
|
|
279
380
|
state
|
|
280
381
|
} = setup({
|
|
@@ -287,11 +388,9 @@ describe('ImportState engine (data-hub job)', () => {
|
|
|
287
388
|
}
|
|
288
389
|
}))
|
|
289
390
|
}, {
|
|
290
|
-
|
|
391
|
+
jobApplicationErrorMap
|
|
291
392
|
});
|
|
292
393
|
await state.startImport();
|
|
293
|
-
expect(onJobError).toHaveBeenCalledTimes(1);
|
|
294
|
-
expect(typeof onJobError.mock.calls[0][0]).toBe('function');
|
|
295
394
|
expect(state.importStatus.status).toBe('error');
|
|
296
395
|
const error = state.importStatus.error;
|
|
297
396
|
expect((_error$response2 = error.response) == null || (_error$response2 = _error$response2.data) == null || (_error$response2 = _error$response2.details) == null || (_error$response2 = _error$response2.applicationError) == null ? void 0 : _error$response2.code).toBe('duplicate_key');
|
|
@@ -351,7 +450,7 @@ describe('ImportState engine (data-hub job)', () => {
|
|
|
351
450
|
// forever; it must stop after the first fetch.
|
|
352
451
|
expect(client.getJob).toHaveBeenCalledTimes(1);
|
|
353
452
|
});
|
|
354
|
-
it('does not restore
|
|
453
|
+
it('does not restore a completed import when the summary is closed', async () => {
|
|
355
454
|
const restoreFn = jest.fn().mockResolvedValue(undefined);
|
|
356
455
|
const {
|
|
357
456
|
state,
|
|
@@ -362,30 +461,19 @@ describe('ImportState engine (data-hub job)', () => {
|
|
|
362
461
|
restore: restoreFn
|
|
363
462
|
})
|
|
364
463
|
});
|
|
464
|
+
await state.startImport();
|
|
365
465
|
|
|
366
|
-
//
|
|
367
|
-
// linger timer.
|
|
368
|
-
const flush = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
369
|
-
const promise = state.startImport();
|
|
370
|
-
// Run backup → upload → submit → poll (terminal) → success, parking on the
|
|
371
|
-
// 600ms completion linger (still on the 'progress' step) without firing it.
|
|
372
|
-
await flush();
|
|
373
|
-
await flush();
|
|
374
|
-
await flush();
|
|
466
|
+
// The import finished on the summary step and the collection was refreshed.
|
|
375
467
|
expect(state.importStatus.isSuccess).toBe(true);
|
|
376
|
-
expect(state.currentStep).toBe('
|
|
468
|
+
expect(state.currentStep).toBe('summary');
|
|
469
|
+
expect(collection.clearResultAndMoveToStart).toHaveBeenCalledWith({
|
|
470
|
+
force: true
|
|
471
|
+
});
|
|
377
472
|
|
|
378
|
-
//
|
|
473
|
+
// Closing the summary must not revert a successful import.
|
|
379
474
|
await state.close();
|
|
380
|
-
await promise;
|
|
381
|
-
|
|
382
|
-
// A successful import must not be reverted; the modal closes and the
|
|
383
|
-
// collection refreshes.
|
|
384
475
|
expect(restoreFn).not.toHaveBeenCalled();
|
|
385
476
|
expect(importModalState.close).toHaveBeenCalled();
|
|
386
|
-
expect(collection.clearResultAndMoveToStart).toHaveBeenCalledWith({
|
|
387
|
-
force: true
|
|
388
|
-
});
|
|
389
477
|
});
|
|
390
478
|
});
|
|
391
479
|
describe('ImportState writePolicy (id-collision handling)', () => {
|
|
@@ -671,5 +759,526 @@ describe('ImportState platformized destination (non–Wix-Data)', () => {
|
|
|
671
759
|
const wixData = setup().state;
|
|
672
760
|
expect(wixData.canConfigureWritePolicy).toBe(true);
|
|
673
761
|
});
|
|
762
|
+
describe('DataExtension create fields (destination branch)', () => {
|
|
763
|
+
it('enables create-new-field when the destination branch supplies createFields', () => {
|
|
764
|
+
const withCreate = setup({}, {
|
|
765
|
+
destination: {
|
|
766
|
+
storesCatalogProducts: {}
|
|
767
|
+
},
|
|
768
|
+
createFields: jest.fn(async () => undefined)
|
|
769
|
+
}).state;
|
|
770
|
+
expect(withCreate.canCreateNewField).toBe(true);
|
|
771
|
+
const withoutCreate = setup({}, {
|
|
772
|
+
destination: {
|
|
773
|
+
storesCatalogProducts: {}
|
|
774
|
+
}
|
|
775
|
+
}).state;
|
|
776
|
+
expect(withoutCreate.canCreateNewField).toBe(false);
|
|
777
|
+
});
|
|
778
|
+
it('hands createFields the deduped pending columns (source builds its own def)', async () => {
|
|
779
|
+
const createFields = jest.fn(async () => undefined);
|
|
780
|
+
const {
|
|
781
|
+
state,
|
|
782
|
+
client
|
|
783
|
+
} = setup({}, {
|
|
784
|
+
destination: {
|
|
785
|
+
storesCatalogProducts: {}
|
|
786
|
+
},
|
|
787
|
+
createFields
|
|
788
|
+
});
|
|
789
|
+
(0, _mobx.runInAction)(() => {
|
|
790
|
+
state.csvHeaders = ['Name', 'Color'];
|
|
791
|
+
state.csvRows = [['Apple', 'Red'], ['Pear', 'Green']];
|
|
792
|
+
state.mappingByHeader = {
|
|
793
|
+
Name: {
|
|
794
|
+
kind: 'existing',
|
|
795
|
+
fieldId: 'name'
|
|
796
|
+
},
|
|
797
|
+
Color: {
|
|
798
|
+
kind: 'new',
|
|
799
|
+
fieldType: 'text'
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
});
|
|
803
|
+
await state.startImport();
|
|
804
|
+
expect(createFields).toHaveBeenCalledTimes(1);
|
|
805
|
+
// Core passes the neutral pending column (deduped key + name + the mapping);
|
|
806
|
+
// the DataExtension source turns it into its own field shape.
|
|
807
|
+
expect(createFields).toHaveBeenCalledWith([{
|
|
808
|
+
key: 'color',
|
|
809
|
+
displayName: 'Color',
|
|
810
|
+
mapping: {
|
|
811
|
+
kind: 'new',
|
|
812
|
+
fieldType: 'text'
|
|
813
|
+
}
|
|
814
|
+
}]);
|
|
815
|
+
// The consumer's destination is still sent verbatim.
|
|
816
|
+
expect(client.submitImportJob).toHaveBeenCalledWith(expect.objectContaining({
|
|
817
|
+
destination: {
|
|
818
|
+
storesCatalogProducts: {}
|
|
819
|
+
}
|
|
820
|
+
}));
|
|
821
|
+
});
|
|
822
|
+
it('carries the edited mapping config (precise type, permissions, PII) in the pending column', async () => {
|
|
823
|
+
const createFields = jest.fn(async () => undefined);
|
|
824
|
+
const {
|
|
825
|
+
state
|
|
826
|
+
} = setup({}, {
|
|
827
|
+
destination: {
|
|
828
|
+
storesCatalogProducts: {}
|
|
829
|
+
},
|
|
830
|
+
createFields
|
|
831
|
+
});
|
|
832
|
+
const config = {
|
|
833
|
+
deType: 'decimal',
|
|
834
|
+
isPii: true,
|
|
835
|
+
permissions: {
|
|
836
|
+
installedApps: {
|
|
837
|
+
defaultRead: true,
|
|
838
|
+
defaultWrite: false
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
(0, _mobx.runInAction)(() => {
|
|
843
|
+
state.csvHeaders = ['Name', 'Count'];
|
|
844
|
+
state.csvRows = [['Apple', '3']];
|
|
845
|
+
state.mappingByHeader = {
|
|
846
|
+
Name: {
|
|
847
|
+
kind: 'existing',
|
|
848
|
+
fieldId: 'name'
|
|
849
|
+
},
|
|
850
|
+
Count: {
|
|
851
|
+
kind: 'new',
|
|
852
|
+
fieldType: 'number',
|
|
853
|
+
displayName: 'Count',
|
|
854
|
+
config
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
});
|
|
858
|
+
await state.startImport();
|
|
859
|
+
expect(createFields).toHaveBeenCalledWith([{
|
|
860
|
+
key: 'count',
|
|
861
|
+
displayName: 'Count',
|
|
862
|
+
mapping: {
|
|
863
|
+
kind: 'new',
|
|
864
|
+
fieldType: 'number',
|
|
865
|
+
displayName: 'Count',
|
|
866
|
+
config
|
|
867
|
+
}
|
|
868
|
+
}]);
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
describe('created fields (CAIRO-4400)', () => {
|
|
872
|
+
const RETURNED_FIELDS = {
|
|
873
|
+
name: {
|
|
874
|
+
id: 'name',
|
|
875
|
+
displayName: 'Name',
|
|
876
|
+
type: 'SHORT_TEXT'
|
|
877
|
+
},
|
|
878
|
+
color: {
|
|
879
|
+
id: 'color',
|
|
880
|
+
displayName: 'Color',
|
|
881
|
+
type: 'SHORT_TEXT'
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
// Maps an extra column to a NEW field so `_resolveFieldMapping` calls
|
|
886
|
+
// `updateSchema`. `updateSchema`'s presence is also what enables new mappings.
|
|
887
|
+
function withNewFieldMapping(state) {
|
|
888
|
+
(0, _mobx.runInAction)(() => {
|
|
889
|
+
state.csvHeaders = ['Name', 'Color'];
|
|
890
|
+
state.csvRows = [['Apple', 'Red'], ['Pear', 'Green']];
|
|
891
|
+
state.mappingByHeader = {
|
|
892
|
+
Name: {
|
|
893
|
+
kind: 'existing',
|
|
894
|
+
fieldId: 'name'
|
|
895
|
+
},
|
|
896
|
+
Color: {
|
|
897
|
+
kind: 'new',
|
|
898
|
+
fieldType: 'text'
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
it('calls mergeFields with updateSchema’s returned map after a successful import', async () => {
|
|
904
|
+
const mergeFields = jest.fn();
|
|
905
|
+
const updateSchema = jest.fn(async () => RETURNED_FIELDS);
|
|
906
|
+
const {
|
|
907
|
+
state,
|
|
908
|
+
collection
|
|
909
|
+
} = setup({}, {
|
|
910
|
+
updateSchema,
|
|
911
|
+
mergeFields
|
|
912
|
+
});
|
|
913
|
+
withNewFieldMapping(state);
|
|
914
|
+
await state.startImport();
|
|
915
|
+
expect(updateSchema).toHaveBeenCalledTimes(1);
|
|
916
|
+
expect(mergeFields).toHaveBeenCalledTimes(1);
|
|
917
|
+
expect(mergeFields).toHaveBeenCalledWith(RETURNED_FIELDS);
|
|
918
|
+
// The merge runs alongside the data refetch on the success path.
|
|
919
|
+
expect(collection.clearResultAndMoveToStart).toHaveBeenCalled();
|
|
920
|
+
});
|
|
921
|
+
it('does not call mergeFields for a plain import (no new fields)', async () => {
|
|
922
|
+
const mergeFields = jest.fn();
|
|
923
|
+
const updateSchema = jest.fn(async () => RETURNED_FIELDS);
|
|
924
|
+
const {
|
|
925
|
+
state
|
|
926
|
+
} = setup({}, {
|
|
927
|
+
updateSchema,
|
|
928
|
+
mergeFields
|
|
929
|
+
});
|
|
930
|
+
await state.startImport();
|
|
931
|
+
expect(updateSchema).not.toHaveBeenCalled();
|
|
932
|
+
expect(mergeFields).not.toHaveBeenCalled();
|
|
933
|
+
});
|
|
934
|
+
it('does not call mergeFields when updateSchema returns nothing', async () => {
|
|
935
|
+
const mergeFields = jest.fn();
|
|
936
|
+
const updateSchema = jest.fn(async () => undefined);
|
|
937
|
+
const {
|
|
938
|
+
state
|
|
939
|
+
} = setup({}, {
|
|
940
|
+
updateSchema,
|
|
941
|
+
mergeFields
|
|
942
|
+
});
|
|
943
|
+
withNewFieldMapping(state);
|
|
944
|
+
await state.startImport();
|
|
945
|
+
expect(updateSchema).toHaveBeenCalledTimes(1);
|
|
946
|
+
expect(mergeFields).not.toHaveBeenCalled();
|
|
947
|
+
});
|
|
948
|
+
it('does not call mergeFields when updateSchema returns an empty map', async () => {
|
|
949
|
+
// A `{}` return is truthy; without a size guard it would reach a full-replace
|
|
950
|
+
// mergeFields({}) and wipe every column. Treat it as "nothing to merge".
|
|
951
|
+
const mergeFields = jest.fn();
|
|
952
|
+
const updateSchema = jest.fn(async () => ({}));
|
|
953
|
+
const {
|
|
954
|
+
state
|
|
955
|
+
} = setup({}, {
|
|
956
|
+
updateSchema,
|
|
957
|
+
mergeFields
|
|
958
|
+
});
|
|
959
|
+
withNewFieldMapping(state);
|
|
960
|
+
await state.startImport();
|
|
961
|
+
expect(updateSchema).toHaveBeenCalledTimes(1);
|
|
962
|
+
expect(mergeFields).not.toHaveBeenCalled();
|
|
963
|
+
});
|
|
964
|
+
it('does not call mergeFields when the import fails after creating fields', async () => {
|
|
965
|
+
const mergeFields = jest.fn();
|
|
966
|
+
const updateSchema = jest.fn(async () => RETURNED_FIELDS);
|
|
967
|
+
const {
|
|
968
|
+
state
|
|
969
|
+
} = setup({
|
|
970
|
+
uploadFile: jest.fn(async () => {
|
|
971
|
+
throw new Error('upload failed');
|
|
972
|
+
})
|
|
973
|
+
}, {
|
|
974
|
+
updateSchema,
|
|
975
|
+
mergeFields
|
|
976
|
+
});
|
|
977
|
+
withNewFieldMapping(state);
|
|
978
|
+
await state.startImport();
|
|
979
|
+
|
|
980
|
+
// Fields were created pre-upload, but a failed import must not merge them.
|
|
981
|
+
expect(updateSchema).toHaveBeenCalledTimes(1);
|
|
982
|
+
expect(state.importStatus.status).toBe('error');
|
|
983
|
+
expect(mergeFields).not.toHaveBeenCalled();
|
|
984
|
+
});
|
|
985
|
+
it('keeps the import successful when mergeFields throws (no void → duplicate-rows)', async () => {
|
|
986
|
+
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
987
|
+
const mergeFields = jest.fn(() => {
|
|
988
|
+
throw new Error('merge boom');
|
|
989
|
+
});
|
|
990
|
+
const updateSchema = jest.fn(async () => RETURNED_FIELDS);
|
|
991
|
+
const {
|
|
992
|
+
state,
|
|
993
|
+
collection
|
|
994
|
+
} = setup({}, {
|
|
995
|
+
updateSchema,
|
|
996
|
+
mergeFields
|
|
997
|
+
});
|
|
998
|
+
withNewFieldMapping(state);
|
|
999
|
+
await state.startImport();
|
|
1000
|
+
expect(mergeFields).toHaveBeenCalledTimes(1);
|
|
1001
|
+
// A throwing merge is swallowed — the completed import is untouched.
|
|
1002
|
+
expect(state.importStatus.status).toBe('success');
|
|
1003
|
+
expect(state.currentStep).toBe('summary');
|
|
1004
|
+
expect(collection.clearResultAndMoveToStart).toHaveBeenCalled();
|
|
1005
|
+
consoleError.mockRestore();
|
|
1006
|
+
});
|
|
1007
|
+
});
|
|
1008
|
+
describe('schema-backed config (live getters + create-field permission)', () => {
|
|
1009
|
+
// A schema-backed config: the CMS `dataSource` branch a SchemaImportRef
|
|
1010
|
+
// satisfies. It supplies `updateSchema` only when create is permitted — its
|
|
1011
|
+
// presence is the create-new-field opt-in (no separate flag).
|
|
1012
|
+
const schemaConfig = (over = {}) => ({
|
|
1013
|
+
fields: over.fields ?? [{
|
|
1014
|
+
id: 'name',
|
|
1015
|
+
header: 'Name'
|
|
1016
|
+
}],
|
|
1017
|
+
dataSource: {
|
|
1018
|
+
collectionId: over.collectionId ?? 'RefCollection',
|
|
1019
|
+
...(over.updateSchema && {
|
|
1020
|
+
updateSchema: over.updateSchema
|
|
1021
|
+
})
|
|
1022
|
+
},
|
|
1023
|
+
...(over.idFieldKey && {
|
|
1024
|
+
idFieldKey: over.idFieldKey
|
|
1025
|
+
}),
|
|
1026
|
+
...(over.mergeFields && {
|
|
1027
|
+
mergeFields: over.mergeFields
|
|
1028
|
+
})
|
|
1029
|
+
});
|
|
1030
|
+
it('reads fields live from the config rather than snapshotting them', () => {
|
|
1031
|
+
const config = schemaConfig();
|
|
1032
|
+
const {
|
|
1033
|
+
state
|
|
1034
|
+
} = setup({}, {
|
|
1035
|
+
config
|
|
1036
|
+
});
|
|
1037
|
+
expect(state.fields).toBe(config.fields);
|
|
1038
|
+
|
|
1039
|
+
// A later change to the config is reflected — values are read on demand,
|
|
1040
|
+
// so a live (getter-backed) config needn't be resolved before the ctor.
|
|
1041
|
+
const updated = [{
|
|
1042
|
+
id: 'name',
|
|
1043
|
+
header: 'Name'
|
|
1044
|
+
}, {
|
|
1045
|
+
id: 'year',
|
|
1046
|
+
header: 'Year'
|
|
1047
|
+
}];
|
|
1048
|
+
config.fields = updated;
|
|
1049
|
+
expect(state.fields).toBe(updated);
|
|
1050
|
+
});
|
|
1051
|
+
it('gates create-new-field on the presence of a create callback (updateSchema)', () => {
|
|
1052
|
+
// No create callback ⇒ create-new-field off (the source didn't permit it).
|
|
1053
|
+
const off = setup({}, {
|
|
1054
|
+
config: schemaConfig()
|
|
1055
|
+
});
|
|
1056
|
+
expect(off.state.canCreateNewField).toBe(false);
|
|
1057
|
+
|
|
1058
|
+
// A create callback present ⇒ on.
|
|
1059
|
+
const on = setup({}, {
|
|
1060
|
+
config: schemaConfig({
|
|
1061
|
+
updateSchema: jest.fn()
|
|
1062
|
+
})
|
|
1063
|
+
});
|
|
1064
|
+
expect(on.state.canCreateNewField).toBe(true);
|
|
1065
|
+
});
|
|
1066
|
+
it('builds the destination from the config collectionId', async () => {
|
|
1067
|
+
const {
|
|
1068
|
+
state,
|
|
1069
|
+
client
|
|
1070
|
+
} = setup({}, {
|
|
1071
|
+
config: schemaConfig({
|
|
1072
|
+
collectionId: 'RefCollection'
|
|
1073
|
+
})
|
|
1074
|
+
});
|
|
1075
|
+
await state.startImport();
|
|
1076
|
+
expect(client.submitImportJob).toHaveBeenCalledWith(expect.objectContaining({
|
|
1077
|
+
destination: {
|
|
1078
|
+
wixDataCollection: {
|
|
1079
|
+
collectionId: 'RefCollection'
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}));
|
|
1083
|
+
});
|
|
1084
|
+
it('creates new fields through the config updateSchema', async () => {
|
|
1085
|
+
const updateSchema = jest.fn(async () => ({
|
|
1086
|
+
name: {
|
|
1087
|
+
id: 'name',
|
|
1088
|
+
displayName: 'Name',
|
|
1089
|
+
type: 'SHORT_TEXT'
|
|
1090
|
+
},
|
|
1091
|
+
year: {
|
|
1092
|
+
id: 'year',
|
|
1093
|
+
displayName: 'Year',
|
|
1094
|
+
type: 'NUMBER'
|
|
1095
|
+
}
|
|
1096
|
+
}));
|
|
1097
|
+
const {
|
|
1098
|
+
state
|
|
1099
|
+
} = setup({}, {
|
|
1100
|
+
config: schemaConfig({
|
|
1101
|
+
updateSchema
|
|
1102
|
+
})
|
|
1103
|
+
});
|
|
1104
|
+
(0, _mobx.runInAction)(() => {
|
|
1105
|
+
state.csvHeaders = ['Name', 'Year'];
|
|
1106
|
+
state.csvRows = [['Apple', '1999']];
|
|
1107
|
+
state.mappingByHeader = {
|
|
1108
|
+
Name: {
|
|
1109
|
+
kind: 'existing',
|
|
1110
|
+
fieldId: 'name'
|
|
1111
|
+
},
|
|
1112
|
+
Year: {
|
|
1113
|
+
kind: 'new',
|
|
1114
|
+
fieldType: 'number'
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
});
|
|
1118
|
+
await state.startImport();
|
|
1119
|
+
expect(updateSchema).toHaveBeenCalledTimes(1);
|
|
1120
|
+
});
|
|
1121
|
+
it('detects the id-mapped column using the config idFieldKey', () => {
|
|
1122
|
+
const {
|
|
1123
|
+
state
|
|
1124
|
+
} = setup({}, {
|
|
1125
|
+
config: schemaConfig({
|
|
1126
|
+
idFieldKey: 'movieId'
|
|
1127
|
+
})
|
|
1128
|
+
});
|
|
1129
|
+
(0, _mobx.runInAction)(() => {
|
|
1130
|
+
state.mappingByHeader = {
|
|
1131
|
+
Ref: {
|
|
1132
|
+
kind: 'existing',
|
|
1133
|
+
fieldId: 'movieId'
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
});
|
|
1137
|
+
expect(state.idMappedHeader).toBe('Ref');
|
|
1138
|
+
});
|
|
1139
|
+
});
|
|
1140
|
+
});
|
|
1141
|
+
describe('ImportState required fields (CAIRO-4452)', () => {
|
|
1142
|
+
beforeEach(() => {
|
|
1143
|
+
jest.useRealTimers();
|
|
1144
|
+
});
|
|
1145
|
+
it('is satisfied when a required field has a column mapped to it', () => {
|
|
1146
|
+
const {
|
|
1147
|
+
state
|
|
1148
|
+
} = setup({}, {
|
|
1149
|
+
config: {
|
|
1150
|
+
fields: [{
|
|
1151
|
+
id: 'name',
|
|
1152
|
+
header: 'Name',
|
|
1153
|
+
required: true
|
|
1154
|
+
}],
|
|
1155
|
+
dataSource: {
|
|
1156
|
+
collectionId: 'Movies'
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
// The seeded mapping maps the "Name" column to the `name` field.
|
|
1162
|
+
expect(state.hasMissingRequiredFields).toBe(false);
|
|
1163
|
+
expect(state.missingRequiredFields).toEqual([]);
|
|
1164
|
+
});
|
|
1165
|
+
it('flags a required field the user set to "don\'t import"', () => {
|
|
1166
|
+
const {
|
|
1167
|
+
state
|
|
1168
|
+
} = setup({}, {
|
|
1169
|
+
config: {
|
|
1170
|
+
fields: [{
|
|
1171
|
+
id: 'name',
|
|
1172
|
+
header: 'Name',
|
|
1173
|
+
required: true
|
|
1174
|
+
}],
|
|
1175
|
+
dataSource: {
|
|
1176
|
+
collectionId: 'Movies'
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
state.setMapping('Name', null);
|
|
1181
|
+
expect(state.hasMissingRequiredFields).toBe(true);
|
|
1182
|
+
expect(state.missingRequiredFields.map(f => f.id)).toEqual(['name']);
|
|
1183
|
+
});
|
|
1184
|
+
it('ignores unmapped fields that are not required', () => {
|
|
1185
|
+
const {
|
|
1186
|
+
state
|
|
1187
|
+
} = setup({}, {
|
|
1188
|
+
config: {
|
|
1189
|
+
fields: [{
|
|
1190
|
+
id: 'name',
|
|
1191
|
+
header: 'Name'
|
|
1192
|
+
}, {
|
|
1193
|
+
id: 'year',
|
|
1194
|
+
header: 'Year',
|
|
1195
|
+
required: false
|
|
1196
|
+
}],
|
|
1197
|
+
dataSource: {
|
|
1198
|
+
collectionId: 'Movies'
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
state.setMapping('Name', null);
|
|
1203
|
+
expect(state.hasMissingRequiredFields).toBe(false);
|
|
1204
|
+
});
|
|
1205
|
+
it('is not satisfied by a create-new-field mapping', () => {
|
|
1206
|
+
const {
|
|
1207
|
+
state
|
|
1208
|
+
} = setup({}, {
|
|
1209
|
+
config: {
|
|
1210
|
+
fields: [{
|
|
1211
|
+
id: 'name',
|
|
1212
|
+
header: 'Name',
|
|
1213
|
+
required: true
|
|
1214
|
+
}],
|
|
1215
|
+
dataSource: {
|
|
1216
|
+
collectionId: 'Movies',
|
|
1217
|
+
updateSchema: async () => undefined
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
// Mapping the column to a brand-new field can't fill the existing required
|
|
1223
|
+
// field, so it stays missing.
|
|
1224
|
+
state.setNewFieldMapping('Name');
|
|
1225
|
+
expect(state.hasMissingRequiredFields).toBe(true);
|
|
1226
|
+
});
|
|
1227
|
+
it('blocks startImport and shows an error toast when a required field is missing', async () => {
|
|
1228
|
+
const {
|
|
1229
|
+
state,
|
|
1230
|
+
client,
|
|
1231
|
+
showToast
|
|
1232
|
+
} = setup({}, {
|
|
1233
|
+
config: {
|
|
1234
|
+
fields: [{
|
|
1235
|
+
id: 'name',
|
|
1236
|
+
header: 'Name',
|
|
1237
|
+
required: true
|
|
1238
|
+
}, {
|
|
1239
|
+
id: 'year',
|
|
1240
|
+
header: 'Year'
|
|
1241
|
+
}],
|
|
1242
|
+
dataSource: {
|
|
1243
|
+
collectionId: 'Movies'
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
// Keep a mapping (so the import isn't blocked by "no mappings") but leave the
|
|
1249
|
+
// required `name` field unmapped.
|
|
1250
|
+
(0, _mobx.runInAction)(() => {
|
|
1251
|
+
state.csvHeaders = ['Name', 'Year'];
|
|
1252
|
+
state.csvRows = [['Apple', '2020']];
|
|
1253
|
+
});
|
|
1254
|
+
state.setMapping('Name', null);
|
|
1255
|
+
state.setMapping('Year', 'year');
|
|
1256
|
+
await state.startImport();
|
|
1257
|
+
expect(client.uploadFile).not.toHaveBeenCalled();
|
|
1258
|
+
expect(client.submitImportJob).not.toHaveBeenCalled();
|
|
1259
|
+
expect(state.currentStep).not.toBe('progress');
|
|
1260
|
+
expect(showToast).toHaveBeenCalledWith(expect.objectContaining({
|
|
1261
|
+
type: 'ERROR'
|
|
1262
|
+
}));
|
|
1263
|
+
});
|
|
1264
|
+
it('proceeds with import when all required fields are mapped', async () => {
|
|
1265
|
+
const {
|
|
1266
|
+
state,
|
|
1267
|
+
client
|
|
1268
|
+
} = setup({}, {
|
|
1269
|
+
config: {
|
|
1270
|
+
fields: [{
|
|
1271
|
+
id: 'name',
|
|
1272
|
+
header: 'Name',
|
|
1273
|
+
required: true
|
|
1274
|
+
}],
|
|
1275
|
+
dataSource: {
|
|
1276
|
+
collectionId: 'Movies'
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
await state.startImport();
|
|
1281
|
+
expect(client.submitImportJob).toHaveBeenCalledTimes(1);
|
|
1282
|
+
});
|
|
674
1283
|
});
|
|
675
1284
|
//# sourceMappingURL=ImportState.engine.spec.js.map
|