@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.
Files changed (171) hide show
  1. package/dist/cjs/assets/locale/messages_ar.json +11 -0
  2. package/dist/cjs/assets/locale/messages_bg.json +11 -0
  3. package/dist/cjs/assets/locale/messages_ca.json +11 -0
  4. package/dist/cjs/assets/locale/messages_cs.json +11 -0
  5. package/dist/cjs/assets/locale/messages_da.json +11 -0
  6. package/dist/cjs/assets/locale/messages_de.json +11 -0
  7. package/dist/cjs/assets/locale/messages_el.json +11 -0
  8. package/dist/cjs/assets/locale/messages_en.json +11 -0
  9. package/dist/cjs/assets/locale/messages_es.json +11 -0
  10. package/dist/cjs/assets/locale/messages_fi.json +11 -0
  11. package/dist/cjs/assets/locale/messages_fr.json +11 -0
  12. package/dist/cjs/assets/locale/messages_he.json +11 -0
  13. package/dist/cjs/assets/locale/messages_hi.json +11 -0
  14. package/dist/cjs/assets/locale/messages_hu.json +11 -0
  15. package/dist/cjs/assets/locale/messages_id.json +11 -0
  16. package/dist/cjs/assets/locale/messages_it.json +11 -0
  17. package/dist/cjs/assets/locale/messages_ja.json +11 -0
  18. package/dist/cjs/assets/locale/messages_ko.json +11 -0
  19. package/dist/cjs/assets/locale/messages_lt.json +11 -0
  20. package/dist/cjs/assets/locale/messages_ms.json +11 -0
  21. package/dist/cjs/assets/locale/messages_nl.json +11 -0
  22. package/dist/cjs/assets/locale/messages_no.json +11 -0
  23. package/dist/cjs/assets/locale/messages_pl.json +11 -0
  24. package/dist/cjs/assets/locale/messages_pt.json +11 -0
  25. package/dist/cjs/assets/locale/messages_ro.json +11 -0
  26. package/dist/cjs/assets/locale/messages_ru.json +11 -0
  27. package/dist/cjs/assets/locale/messages_sk.json +11 -0
  28. package/dist/cjs/assets/locale/messages_sl.json +11 -0
  29. package/dist/cjs/assets/locale/messages_sv.json +11 -0
  30. package/dist/cjs/assets/locale/messages_th.json +11 -0
  31. package/dist/cjs/assets/locale/messages_tl.json +11 -0
  32. package/dist/cjs/assets/locale/messages_tr.json +11 -0
  33. package/dist/cjs/assets/locale/messages_uk.json +11 -0
  34. package/dist/cjs/assets/locale/messages_vi.json +11 -0
  35. package/dist/cjs/assets/locale/messages_zh.json +11 -0
  36. package/dist/cjs/assets/locale/version_file +1 -1
  37. package/dist/cjs/assets/visual-e2e-locale/messages_visuale2e.json +29 -2
  38. package/dist/cjs/hooks/index.js +0 -6
  39. package/dist/cjs/hooks/index.js.map +1 -1
  40. package/dist/cjs/hooks/useImport.js +3 -13
  41. package/dist/cjs/hooks/useImport.js.map +1 -1
  42. package/dist/cjs/index.js +0 -7
  43. package/dist/cjs/index.js.map +1 -1
  44. package/dist/cjs/services/translations.js.map +1 -1
  45. package/dist/cjs/state/CustomFieldModalState.js +12 -0
  46. package/dist/cjs/state/CustomFieldModalState.js.map +1 -1
  47. package/dist/cjs/state/ImportState/ImportState.js +401 -133
  48. package/dist/cjs/state/ImportState/ImportState.js.map +1 -1
  49. package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js +671 -62
  50. package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -1
  51. package/dist/cjs/state/ImportState/detectFieldType.js +23 -0
  52. package/dist/cjs/state/ImportState/detectFieldType.js.map +1 -1
  53. package/dist/cjs/state/ImportState/fieldEdit.js +49 -0
  54. package/dist/cjs/state/ImportState/fieldEdit.js.map +1 -1
  55. package/dist/cjs/state/TagsState/TagsManagementState.js +17 -44
  56. package/dist/cjs/state/TagsState/TagsManagementState.js.map +1 -1
  57. package/dist/cjs/state/TagsState/TagsSearchState.js +0 -6
  58. package/dist/cjs/state/TagsState/TagsSearchState.js.map +1 -1
  59. package/dist/cjs/testkit/createInMemorySchemaSource.js +186 -0
  60. package/dist/cjs/testkit/createInMemorySchemaSource.js.map +1 -0
  61. package/dist/cjs/testkit/hubJobImportMocks.js +18 -2
  62. package/dist/cjs/testkit/hubJobImportMocks.js.map +1 -1
  63. package/dist/cjs/testkit/index.js +6 -0
  64. package/dist/cjs/testkit/index.js.map +1 -1
  65. package/dist/cjs/types/FieldsSource.js.map +1 -1
  66. package/dist/cjs/types/Schema.js.map +1 -1
  67. package/dist/cjs/types/SchemaSource.js +2 -1
  68. package/dist/cjs/types/SchemaSource.js.map +1 -1
  69. package/dist/esm/assets/locale/messages_ar.json +11 -0
  70. package/dist/esm/assets/locale/messages_bg.json +11 -0
  71. package/dist/esm/assets/locale/messages_ca.json +11 -0
  72. package/dist/esm/assets/locale/messages_cs.json +11 -0
  73. package/dist/esm/assets/locale/messages_da.json +11 -0
  74. package/dist/esm/assets/locale/messages_de.json +11 -0
  75. package/dist/esm/assets/locale/messages_el.json +11 -0
  76. package/dist/esm/assets/locale/messages_en.json +11 -0
  77. package/dist/esm/assets/locale/messages_es.json +11 -0
  78. package/dist/esm/assets/locale/messages_fi.json +11 -0
  79. package/dist/esm/assets/locale/messages_fr.json +11 -0
  80. package/dist/esm/assets/locale/messages_he.json +11 -0
  81. package/dist/esm/assets/locale/messages_hi.json +11 -0
  82. package/dist/esm/assets/locale/messages_hu.json +11 -0
  83. package/dist/esm/assets/locale/messages_id.json +11 -0
  84. package/dist/esm/assets/locale/messages_it.json +11 -0
  85. package/dist/esm/assets/locale/messages_ja.json +11 -0
  86. package/dist/esm/assets/locale/messages_ko.json +11 -0
  87. package/dist/esm/assets/locale/messages_lt.json +11 -0
  88. package/dist/esm/assets/locale/messages_ms.json +11 -0
  89. package/dist/esm/assets/locale/messages_nl.json +11 -0
  90. package/dist/esm/assets/locale/messages_no.json +11 -0
  91. package/dist/esm/assets/locale/messages_pl.json +11 -0
  92. package/dist/esm/assets/locale/messages_pt.json +11 -0
  93. package/dist/esm/assets/locale/messages_ro.json +11 -0
  94. package/dist/esm/assets/locale/messages_ru.json +11 -0
  95. package/dist/esm/assets/locale/messages_sk.json +11 -0
  96. package/dist/esm/assets/locale/messages_sl.json +11 -0
  97. package/dist/esm/assets/locale/messages_sv.json +11 -0
  98. package/dist/esm/assets/locale/messages_th.json +11 -0
  99. package/dist/esm/assets/locale/messages_tl.json +11 -0
  100. package/dist/esm/assets/locale/messages_tr.json +11 -0
  101. package/dist/esm/assets/locale/messages_uk.json +11 -0
  102. package/dist/esm/assets/locale/messages_vi.json +11 -0
  103. package/dist/esm/assets/locale/messages_zh.json +11 -0
  104. package/dist/esm/assets/locale/version_file +1 -1
  105. package/dist/esm/assets/visual-e2e-locale/messages_visuale2e.json +29 -2
  106. package/dist/esm/hooks/index.js +0 -1
  107. package/dist/esm/hooks/index.js.map +1 -1
  108. package/dist/esm/hooks/useImport.js +2 -7
  109. package/dist/esm/hooks/useImport.js.map +1 -1
  110. package/dist/esm/index.js +0 -1
  111. package/dist/esm/index.js.map +1 -1
  112. package/dist/esm/state/CustomFieldModalState.js +8 -0
  113. package/dist/esm/state/CustomFieldModalState.js.map +1 -1
  114. package/dist/esm/state/ImportState/ImportState.js +299 -108
  115. package/dist/esm/state/ImportState/ImportState.js.map +1 -1
  116. package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js +442 -55
  117. package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -1
  118. package/dist/esm/state/ImportState/detectFieldType.js +19 -0
  119. package/dist/esm/state/ImportState/detectFieldType.js.map +1 -1
  120. package/dist/esm/state/ImportState/fieldEdit.js +44 -0
  121. package/dist/esm/state/ImportState/fieldEdit.js.map +1 -1
  122. package/dist/esm/state/TagsState/TagsManagementState.js +17 -33
  123. package/dist/esm/state/TagsState/TagsManagementState.js.map +1 -1
  124. package/dist/esm/state/TagsState/TagsSearchState.js +0 -6
  125. package/dist/esm/state/TagsState/TagsSearchState.js.map +1 -1
  126. package/dist/esm/testkit/createInMemorySchemaSource.js +140 -0
  127. package/dist/esm/testkit/createInMemorySchemaSource.js.map +1 -0
  128. package/dist/esm/testkit/hubJobImportMocks.js +17 -1
  129. package/dist/esm/testkit/hubJobImportMocks.js.map +1 -1
  130. package/dist/esm/testkit/index.js +1 -0
  131. package/dist/esm/testkit/index.js.map +1 -1
  132. package/dist/esm/types/SchemaSource.js.map +1 -1
  133. package/dist/types/hooks/index.d.ts +0 -1
  134. package/dist/types/hooks/index.d.ts.map +1 -1
  135. package/dist/types/hooks/useImport.d.ts +3 -18
  136. package/dist/types/hooks/useImport.d.ts.map +1 -1
  137. package/dist/types/index.d.ts +0 -1
  138. package/dist/types/index.d.ts.map +1 -1
  139. package/dist/types/services/translations.d.ts +20 -1
  140. package/dist/types/services/translations.d.ts.map +1 -1
  141. package/dist/types/state/CustomFieldModalState.d.ts +9 -0
  142. package/dist/types/state/CustomFieldModalState.d.ts.map +1 -1
  143. package/dist/types/state/ImportState/ImportState.d.ts +201 -62
  144. package/dist/types/state/ImportState/ImportState.d.ts.map +1 -1
  145. package/dist/types/state/ImportState/detectFieldType.d.ts +2 -0
  146. package/dist/types/state/ImportState/detectFieldType.d.ts.map +1 -1
  147. package/dist/types/state/ImportState/fieldEdit.d.ts +6 -0
  148. package/dist/types/state/ImportState/fieldEdit.d.ts.map +1 -1
  149. package/dist/types/state/TagsState/TagsManagementState.d.ts.map +1 -1
  150. package/dist/types/state/TagsState/TagsSearchState.d.ts.map +1 -1
  151. package/dist/types/testkit/WixPatternsContainer.driver.d.ts +4 -0
  152. package/dist/types/testkit/WixPatternsContainer.driver.d.ts.map +1 -1
  153. package/dist/types/testkit/createInMemorySchemaSource.d.ts +41 -0
  154. package/dist/types/testkit/createInMemorySchemaSource.d.ts.map +1 -0
  155. package/dist/types/testkit/hubJobImportMocks.d.ts +11 -1
  156. package/dist/types/testkit/hubJobImportMocks.d.ts.map +1 -1
  157. package/dist/types/testkit/index.d.ts +1 -0
  158. package/dist/types/testkit/index.d.ts.map +1 -1
  159. package/dist/types/types/FieldsSource.d.ts +5 -1
  160. package/dist/types/types/FieldsSource.d.ts.map +1 -1
  161. package/dist/types/types/Schema.d.ts +6 -0
  162. package/dist/types/types/Schema.d.ts.map +1 -1
  163. package/dist/types/types/SchemaSource.d.ts +6 -4
  164. package/dist/types/types/SchemaSource.d.ts.map +1 -1
  165. package/package.json +4 -4
  166. package/dist/cjs/hooks/useImportConfig.js +0 -10
  167. package/dist/cjs/hooks/useImportConfig.js.map +0 -1
  168. package/dist/esm/hooks/useImportConfig.js +0 -6
  169. package/dist/esm/hooks/useImportConfig.js.map +0 -1
  170. package/dist/types/hooks/useImportConfig.d.ts +0 -21
  171. 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
- function setup(clientOverrides = {}, extraProps = {}) {
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 props = {
45
- fields: [{
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
- collectionId: 'Movies',
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
- ...extraProps
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 success path awaits a real setTimeout (completion linger); make sure
89
- // timers are real even if the preset defaults to fake ones.
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
- expect(importModalState.close).toHaveBeenCalled();
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('shows a partial-success toast when some rows are rejected', async () => {
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(showToast).toHaveBeenCalledWith(expect.objectContaining({
144
- type: 'WARNING'
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
- const toast = showToast.mock.calls[0][0];
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 from the partial toast action', async () => {
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: 'x'
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
- // Original headers + only the failed row, ready to fix and re-upload.
239
- expect(downloadCsv).toHaveBeenCalledWith('Name\r\nPear', 'failed-data.csv');
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('hands a failed job to onJobError, which surfaces the wrapped error', async () => {
367
+ it('resolves a failed job through the consumer jobApplicationErrorMap', async () => {
268
368
  var _error$response2;
269
- // Wiring only: the consumer forwards `run` to their withErrorHandler exactly
270
- // as documented, and the error it re-rejects is the one surfaced to the UI
271
- // (with the application-error-shaped code intact). End-to-end resolution
272
- // against the real error handler is covered in the cairo
273
- // Table.importButton spec, which has a real errorHandler instance.
274
- // Mirror `(run) => errorHandler.withErrorHandler(run, errorCodesMap)`;
275
- // a passthrough stub, so this just runs `run` and re-rejects.
276
- const withErrorHandler = fn => fn();
277
- const onJobError = jest.fn(run => withErrorHandler(run));
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
- onJobError
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 when closed during the success completion linger', async () => {
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
- // A 0ms macrotask drains the pending async chain without firing the 600ms
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('progress');
468
+ expect(state.currentStep).toBe('summary');
469
+ expect(collection.clearResultAndMoveToStart).toHaveBeenCalledWith({
470
+ force: true
471
+ });
377
472
 
378
- // User closes the modal mid-linger.
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