@wix/bex-core 2.332.0 → 2.336.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 (186) hide show
  1. package/dist/cjs/assets/locale/messages_ar.json +13 -2
  2. package/dist/cjs/assets/locale/messages_bg.json +13 -2
  3. package/dist/cjs/assets/locale/messages_ca.json +13 -2
  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 +13 -2
  8. package/dist/cjs/assets/locale/messages_en.json +13 -2
  9. package/dist/cjs/assets/locale/messages_es.json +11 -0
  10. package/dist/cjs/assets/locale/messages_fi.json +13 -2
  11. package/dist/cjs/assets/locale/messages_fr.json +11 -0
  12. package/dist/cjs/assets/locale/messages_he.json +13 -2
  13. package/dist/cjs/assets/locale/messages_hi.json +11 -0
  14. package/dist/cjs/assets/locale/messages_hu.json +13 -2
  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 +13 -2
  20. package/dist/cjs/assets/locale/messages_ms.json +13 -2
  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 +13 -2
  26. package/dist/cjs/assets/locale/messages_ru.json +11 -0
  27. package/dist/cjs/assets/locale/messages_sk.json +13 -2
  28. package/dist/cjs/assets/locale/messages_sl.json +13 -2
  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 +13 -2
  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 +6 -6
  39. package/dist/cjs/hooks/index.js.map +1 -1
  40. package/dist/cjs/hooks/useImport.js +4 -0
  41. package/dist/cjs/hooks/useImport.js.map +1 -1
  42. package/dist/cjs/hooks/useSchemaSource.js +11 -0
  43. package/dist/cjs/hooks/useSchemaSource.js.map +1 -0
  44. package/dist/cjs/index.js +4 -9
  45. package/dist/cjs/index.js.map +1 -1
  46. package/dist/cjs/services/translations.js.map +1 -1
  47. package/dist/cjs/state/CustomFieldModalState.js +12 -0
  48. package/dist/cjs/state/CustomFieldModalState.js.map +1 -1
  49. package/dist/cjs/state/ImportState/ImportState.js +415 -136
  50. package/dist/cjs/state/ImportState/ImportState.js.map +1 -1
  51. package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js +417 -82
  52. package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -1
  53. package/dist/cjs/state/ImportState/__tests__/buildImportConfig.spec.js +156 -0
  54. package/dist/cjs/state/ImportState/__tests__/buildImportConfig.spec.js.map +1 -0
  55. package/dist/cjs/state/ImportState/detectFieldType.js +41 -0
  56. package/dist/cjs/state/ImportState/detectFieldType.js.map +1 -1
  57. package/dist/cjs/state/ImportState/fieldEdit.js +49 -0
  58. package/dist/cjs/state/ImportState/fieldEdit.js.map +1 -1
  59. package/dist/cjs/state/SourceState.js +150 -0
  60. package/dist/cjs/state/SourceState.js.map +1 -0
  61. package/dist/cjs/state/index.js +7 -0
  62. package/dist/cjs/state/index.js.map +1 -1
  63. package/dist/cjs/testkit/createInMemorySchemaSource.js +190 -0
  64. package/dist/cjs/testkit/createInMemorySchemaSource.js.map +1 -0
  65. package/dist/cjs/testkit/hubJobImportMocks.js +18 -2
  66. package/dist/cjs/testkit/hubJobImportMocks.js.map +1 -1
  67. package/dist/cjs/testkit/index.js +6 -0
  68. package/dist/cjs/testkit/index.js.map +1 -1
  69. package/dist/cjs/types/Field.js.map +1 -1
  70. package/dist/cjs/types/Schema.js.map +1 -1
  71. package/dist/cjs/types/SchemaSource.js +31 -21
  72. package/dist/cjs/types/SchemaSource.js.map +1 -1
  73. package/dist/esm/assets/locale/messages_ar.json +13 -2
  74. package/dist/esm/assets/locale/messages_bg.json +13 -2
  75. package/dist/esm/assets/locale/messages_ca.json +13 -2
  76. package/dist/esm/assets/locale/messages_cs.json +11 -0
  77. package/dist/esm/assets/locale/messages_da.json +11 -0
  78. package/dist/esm/assets/locale/messages_de.json +11 -0
  79. package/dist/esm/assets/locale/messages_el.json +13 -2
  80. package/dist/esm/assets/locale/messages_en.json +13 -2
  81. package/dist/esm/assets/locale/messages_es.json +11 -0
  82. package/dist/esm/assets/locale/messages_fi.json +13 -2
  83. package/dist/esm/assets/locale/messages_fr.json +11 -0
  84. package/dist/esm/assets/locale/messages_he.json +13 -2
  85. package/dist/esm/assets/locale/messages_hi.json +11 -0
  86. package/dist/esm/assets/locale/messages_hu.json +13 -2
  87. package/dist/esm/assets/locale/messages_id.json +11 -0
  88. package/dist/esm/assets/locale/messages_it.json +11 -0
  89. package/dist/esm/assets/locale/messages_ja.json +11 -0
  90. package/dist/esm/assets/locale/messages_ko.json +11 -0
  91. package/dist/esm/assets/locale/messages_lt.json +13 -2
  92. package/dist/esm/assets/locale/messages_ms.json +13 -2
  93. package/dist/esm/assets/locale/messages_nl.json +11 -0
  94. package/dist/esm/assets/locale/messages_no.json +11 -0
  95. package/dist/esm/assets/locale/messages_pl.json +11 -0
  96. package/dist/esm/assets/locale/messages_pt.json +11 -0
  97. package/dist/esm/assets/locale/messages_ro.json +13 -2
  98. package/dist/esm/assets/locale/messages_ru.json +11 -0
  99. package/dist/esm/assets/locale/messages_sk.json +13 -2
  100. package/dist/esm/assets/locale/messages_sl.json +13 -2
  101. package/dist/esm/assets/locale/messages_sv.json +11 -0
  102. package/dist/esm/assets/locale/messages_th.json +11 -0
  103. package/dist/esm/assets/locale/messages_tl.json +13 -2
  104. package/dist/esm/assets/locale/messages_tr.json +11 -0
  105. package/dist/esm/assets/locale/messages_uk.json +11 -0
  106. package/dist/esm/assets/locale/messages_vi.json +11 -0
  107. package/dist/esm/assets/locale/messages_zh.json +11 -0
  108. package/dist/esm/assets/locale/version_file +1 -1
  109. package/dist/esm/assets/visual-e2e-locale/messages_visuale2e.json +29 -2
  110. package/dist/esm/hooks/index.js +1 -1
  111. package/dist/esm/hooks/index.js.map +1 -1
  112. package/dist/esm/hooks/useImport.js +3 -1
  113. package/dist/esm/hooks/useImport.js.map +1 -1
  114. package/dist/esm/hooks/useSchemaSource.js +7 -0
  115. package/dist/esm/hooks/useSchemaSource.js.map +1 -0
  116. package/dist/esm/index.js +1 -2
  117. package/dist/esm/index.js.map +1 -1
  118. package/dist/esm/state/CustomFieldModalState.js +8 -0
  119. package/dist/esm/state/CustomFieldModalState.js.map +1 -1
  120. package/dist/esm/state/ImportState/ImportState.js +306 -113
  121. package/dist/esm/state/ImportState/ImportState.js.map +1 -1
  122. package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js +297 -70
  123. package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -1
  124. package/dist/esm/state/ImportState/__tests__/buildImportConfig.spec.js +100 -0
  125. package/dist/esm/state/ImportState/__tests__/buildImportConfig.spec.js.map +1 -0
  126. package/dist/esm/state/ImportState/detectFieldType.js +34 -15
  127. package/dist/esm/state/ImportState/detectFieldType.js.map +1 -1
  128. package/dist/esm/state/ImportState/fieldEdit.js +44 -0
  129. package/dist/esm/state/ImportState/fieldEdit.js.map +1 -1
  130. package/dist/esm/state/SourceState.js +131 -0
  131. package/dist/esm/state/SourceState.js.map +1 -0
  132. package/dist/esm/state/index.js +1 -0
  133. package/dist/esm/state/index.js.map +1 -1
  134. package/dist/esm/testkit/createInMemorySchemaSource.js +141 -0
  135. package/dist/esm/testkit/createInMemorySchemaSource.js.map +1 -0
  136. package/dist/esm/testkit/hubJobImportMocks.js +17 -1
  137. package/dist/esm/testkit/hubJobImportMocks.js.map +1 -1
  138. package/dist/esm/testkit/index.js +1 -0
  139. package/dist/esm/testkit/index.js.map +1 -1
  140. package/dist/esm/types/SchemaSource.js +21 -14
  141. package/dist/esm/types/SchemaSource.js.map +1 -1
  142. package/dist/types/hooks/index.d.ts +1 -1
  143. package/dist/types/hooks/index.d.ts.map +1 -1
  144. package/dist/types/hooks/useImport.d.ts +9 -3
  145. package/dist/types/hooks/useImport.d.ts.map +1 -1
  146. package/dist/types/hooks/useSchemaSource.d.ts +4 -0
  147. package/dist/types/hooks/useSchemaSource.d.ts.map +1 -0
  148. package/dist/types/index.d.ts +2 -3
  149. package/dist/types/index.d.ts.map +1 -1
  150. package/dist/types/services/translations.d.ts +7 -1
  151. package/dist/types/services/translations.d.ts.map +1 -1
  152. package/dist/types/state/CustomFieldModalState.d.ts +9 -0
  153. package/dist/types/state/CustomFieldModalState.d.ts.map +1 -1
  154. package/dist/types/state/ImportState/ImportState.d.ts +208 -65
  155. package/dist/types/state/ImportState/ImportState.d.ts.map +1 -1
  156. package/dist/types/state/ImportState/__tests__/buildImportConfig.spec.d.ts +2 -0
  157. package/dist/types/state/ImportState/__tests__/buildImportConfig.spec.d.ts.map +1 -0
  158. package/dist/types/state/ImportState/detectFieldType.d.ts +5 -0
  159. package/dist/types/state/ImportState/detectFieldType.d.ts.map +1 -1
  160. package/dist/types/state/ImportState/fieldEdit.d.ts +6 -0
  161. package/dist/types/state/ImportState/fieldEdit.d.ts.map +1 -1
  162. package/dist/types/state/SourceState.d.ts +39 -0
  163. package/dist/types/state/SourceState.d.ts.map +1 -0
  164. package/dist/types/state/index.d.ts +1 -0
  165. package/dist/types/state/index.d.ts.map +1 -1
  166. package/dist/types/testkit/WixPatternsContainer.driver.d.ts +4 -0
  167. package/dist/types/testkit/WixPatternsContainer.driver.d.ts.map +1 -1
  168. package/dist/types/testkit/createInMemorySchemaSource.d.ts +43 -0
  169. package/dist/types/testkit/createInMemorySchemaSource.d.ts.map +1 -0
  170. package/dist/types/testkit/hubJobImportMocks.d.ts +11 -1
  171. package/dist/types/testkit/hubJobImportMocks.d.ts.map +1 -1
  172. package/dist/types/testkit/index.d.ts +1 -0
  173. package/dist/types/testkit/index.d.ts.map +1 -1
  174. package/dist/types/types/Field.d.ts +3 -0
  175. package/dist/types/types/Field.d.ts.map +1 -1
  176. package/dist/types/types/Schema.d.ts +10 -6
  177. package/dist/types/types/Schema.d.ts.map +1 -1
  178. package/dist/types/types/SchemaSource.d.ts +30 -13
  179. package/dist/types/types/SchemaSource.d.ts.map +1 -1
  180. package/package.json +4 -4
  181. package/dist/cjs/hooks/useImportConfig.js +0 -12
  182. package/dist/cjs/hooks/useImportConfig.js.map +0 -1
  183. package/dist/esm/hooks/useImportConfig.js +0 -6
  184. package/dist/esm/hooks/useImportConfig.js.map +0 -1
  185. package/dist/types/hooks/useImportConfig.d.ts +0 -5
  186. package/dist/types/hooks/useImportConfig.d.ts.map +0 -1
@@ -19,13 +19,17 @@ 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
 
26
- // Config overrides merge into the default config; `config` fully replaces it
27
- // (for schema-backed configs). `detectColumnType` is an infra prop; `mergeFields`
28
- // is part of the config (merged via configOverrides).
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.
29
33
 
30
34
  function setup(clientOverrides = {}, extra = {}) {
31
35
  const client = {
@@ -40,24 +44,59 @@ function setup(clientOverrides = {}, extra = {}) {
40
44
  const importModalState = {
41
45
  close: jest.fn()
42
46
  };
43
- const collection = {
44
- clearResultAndMoveToStart: jest.fn()
45
- };
46
47
  const showToast = jest.fn();
47
48
  const downloadCsv = jest.fn();
48
49
  const translate = (key, params) => params ? `${key}:${JSON.stringify(params)}` : key;
49
50
  const {
50
51
  detectColumnType,
51
52
  config: configOverride,
52
- ...configOverrides
53
+ collectionId,
54
+ destination,
55
+ updateSchema,
56
+ createFields,
57
+ fieldManagement,
58
+ mergeFields,
59
+ fields,
60
+ idFieldKey,
61
+ backup,
62
+ jobApplicationErrorMap
53
63
  } = extra;
64
+ const collection = {
65
+ clearResultAndMoveToStart: jest.fn(),
66
+ queryName: collectionId ?? 'Movies'
67
+ };
68
+ const dataSource = destination ? {
69
+ destination,
70
+ ...(createFields && {
71
+ createFields
72
+ }),
73
+ ...(fieldManagement && {
74
+ fieldManagement
75
+ })
76
+ } : {
77
+ collectionId: collectionId ?? 'Movies',
78
+ ...(updateSchema && {
79
+ updateSchema
80
+ })
81
+ };
54
82
  const config = configOverride ?? {
55
- fields: [{
83
+ fields: fields ?? [{
56
84
  id: 'name',
57
85
  header: 'Name'
58
86
  }],
59
- collectionId: 'Movies',
60
- ...configOverrides
87
+ dataSource,
88
+ ...(mergeFields && {
89
+ mergeFields
90
+ }),
91
+ ...(idFieldKey && {
92
+ idFieldKey
93
+ }),
94
+ ...(backup && {
95
+ backup
96
+ }),
97
+ ...(jobApplicationErrorMap && {
98
+ jobApplicationErrorMap
99
+ })
61
100
  };
62
101
  const props = {
63
102
  config,
@@ -99,8 +138,8 @@ function setup(clientOverrides = {}, extra = {}) {
99
138
  }
100
139
  describe('ImportState engine (data-hub job)', () => {
101
140
  beforeEach(() => {
102
- // The success path awaits a real setTimeout (completion linger); make sure
103
- // timers are real even if the preset defaults to fake ones.
141
+ // The poll loop awaits real setTimeouts between polls; make sure timers are
142
+ // real even if the preset defaults to fake ones.
104
143
  jest.useRealTimers();
105
144
  });
106
145
  it('uploads the aligned file, submits a job, and reports success', async () => {
@@ -122,17 +161,63 @@ describe('ImportState engine (data-hub job)', () => {
122
161
  }
123
162
  }
124
163
  });
164
+
165
+ // created/updated come from the job's itemsSuccessfulInfo.
125
166
  expect(state.result).toEqual({
126
167
  succeeded: 2,
127
- failed: 0
168
+ failed: 0,
169
+ created: 1,
170
+ updated: 1
128
171
  });
129
172
  expect(state.importStatus.status).toBe('success');
130
- expect(importModalState.close).toHaveBeenCalled();
173
+ // The modal stays open on the summary step; the collection is refreshed now
174
+ // so the table is current once the user closes it via `done`.
175
+ expect(state.currentStep).toBe('summary');
176
+ expect(importModalState.close).not.toHaveBeenCalled();
131
177
  expect(collection.clearResultAndMoveToStart).toHaveBeenCalledWith({
132
178
  force: true
133
179
  });
134
180
  });
135
- it('shows a partial-success toast when some rows are rejected', async () => {
181
+ it('holds the completed (100%) progress bar on the progress step before the summary (CAIRO-4476)', async () => {
182
+ const {
183
+ state
184
+ } = setup();
185
+
186
+ // The job reports counts only when it finishes, so the bar can only reach
187
+ // 100% at the very end. Capture the step + counts at the moment the linger
188
+ // starts to prove the user sees the completed bar before the summary
189
+ // replaces it.
190
+ let atLinger = null;
191
+ const spy = jest
192
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
193
+ .spyOn(state, '_lingerOnCompletion').mockImplementation(() => {
194
+ atLinger = {
195
+ step: state.currentStep,
196
+ processed: state.processedCount,
197
+ total: state.totalCount
198
+ };
199
+ return Promise.resolve();
200
+ });
201
+ await state.startImport();
202
+ expect(atLinger).toEqual({
203
+ step: 'progress',
204
+ processed: 2,
205
+ total: 2
206
+ });
207
+ expect(state.currentStep).toBe('summary');
208
+ spy.mockRestore();
209
+ });
210
+ it('closes the modal only when the user dismisses the summary via done', async () => {
211
+ const {
212
+ state,
213
+ importModalState
214
+ } = setup();
215
+ await state.startImport();
216
+ expect(importModalState.close).not.toHaveBeenCalled();
217
+ state.done();
218
+ expect(importModalState.close).toHaveBeenCalled();
219
+ });
220
+ it('shows the summary step (no toast) when some rows are rejected', async () => {
136
221
  const {
137
222
  state,
138
223
  showToast,
@@ -145,25 +230,30 @@ describe('ImportState engine (data-hub job)', () => {
145
230
  itemsSuccessful: 1,
146
231
  itemsRejected: 1,
147
232
  total: 2,
148
- current: 2
233
+ current: 2,
234
+ itemsSuccessfulInfo: {
235
+ itemsCreated: 1,
236
+ itemsUpdated: 0
237
+ }
149
238
  }
150
239
  }))
151
240
  });
152
241
  await state.startImport();
153
242
  expect(state.result).toEqual({
154
243
  succeeded: 1,
155
- failed: 1
244
+ failed: 1,
245
+ created: 1,
246
+ updated: 0
156
247
  });
157
- expect(showToast).toHaveBeenCalledWith(expect.objectContaining({
158
- type: 'WARNING'
159
- }));
160
- expect(importModalState.close).toHaveBeenCalled();
248
+ expect(state.currentStep).toBe('summary');
249
+ // The completion toast is gone — the summary step reports the outcome.
250
+ expect(showToast).not.toHaveBeenCalled();
251
+ expect(importModalState.close).not.toHaveBeenCalled();
161
252
  });
162
253
  it('maps the failed "row N" by record (unaffected by an embedded-newline cell)', async () => {
163
254
  const {
164
255
  state,
165
- downloadCsv,
166
- showToast
256
+ downloadCsv
167
257
  } = setup({
168
258
  getJob: jest.fn(async () => ({
169
259
  id: 'job-1',
@@ -206,19 +296,17 @@ describe('ImportState engine (data-hub job)', () => {
206
296
  };
207
297
  });
208
298
  await state.startImport();
209
- const toast = showToast.mock.calls[0][0];
210
- await toast.action.onClick();
299
+ await state.downloadFailedRows();
211
300
 
212
301
  // "row 4" → csvRows[2] (Mango), NOT shifted by the embedded newline in row 0.
213
302
  const [content] = downloadCsv.mock.calls[0];
214
303
  expect(content).toContain('Mango');
215
304
  expect(content).not.toContain('Apple');
216
305
  });
217
- it('downloads a re-importable CSV of the failed rows from the partial toast action', async () => {
306
+ it('downloads a re-importable CSV of the failed rows, each prefixed with its failure reason', async () => {
218
307
  const {
219
308
  state,
220
309
  client,
221
- showToast,
222
310
  downloadCsv
223
311
  } = setup({
224
312
  getJob: jest.fn(async () => ({
@@ -235,7 +323,7 @@ describe('ImportState engine (data-hub job)', () => {
235
323
  sourceItemId: 'row 3',
236
324
  failure: {
237
325
  code: 'bad',
238
- description: 'x'
326
+ description: 'Invalid value'
239
327
  }
240
328
  }],
241
329
  pagingMetadata: {
@@ -244,13 +332,12 @@ describe('ImportState engine (data-hub job)', () => {
244
332
  }))
245
333
  });
246
334
  await state.startImport();
247
-
248
- // Trigger the toast's download action.
249
- const toast = showToast.mock.calls[0][0];
250
- await toast.action.onClick();
335
+ await state.downloadFailedRows();
251
336
  expect(client.listMovementLogs).toHaveBeenCalledWith('job-1', undefined);
252
- // Original headers + only the failed row, ready to fix and re-upload.
253
- expect(downloadCsv).toHaveBeenCalledWith('Name\r\nPear', 'failed-data.csv');
337
+ // A leading reason column (the translated header) + original headers, then
338
+ // the failed row prefixed with the server's failure reason — ready to fix
339
+ // and re-upload. `translate` here echoes the key for the column header.
340
+ expect(downloadCsv).toHaveBeenCalledWith('cairo.import.failureReasonColumn,Name\r\nInvalid value,Pear', 'failed-data.csv');
254
341
  });
255
342
  it('surfaces an error state with an application-error-shaped error when the job fails', async () => {
256
343
  var _error$response;
@@ -278,17 +365,18 @@ describe('ImportState engine (data-hub job)', () => {
278
365
  expect(importModalState.close).not.toHaveBeenCalled();
279
366
  expect(collection.clearResultAndMoveToStart).not.toHaveBeenCalled();
280
367
  });
281
- it('hands a failed job to onJobError, which surfaces the wrapped error', async () => {
368
+ it('resolves a failed job through the consumer jobApplicationErrorMap', async () => {
282
369
  var _error$response2;
283
- // Wiring only: the consumer forwards `run` to their withErrorHandler exactly
284
- // as documented, and the error it re-rejects is the one surfaced to the UI
285
- // (with the application-error-shaped code intact). End-to-end resolution
286
- // against the real error handler is covered in the cairo
287
- // Table.importButton spec, which has a real errorHandler instance.
288
- // Mirror `(run) => errorHandler.withErrorHandler(run, errorCodesMap)`;
289
- // a passthrough stub, so this just runs `run` and re-rejects.
290
- const withErrorHandler = fn => fn();
291
- const onJobError = jest.fn(run => withErrorHandler(run));
370
+ // ImportState wraps the failure with `errorHandler.withErrorHandler(run,
371
+ // jobApplicationErrorMap)` itself; the consumer only supplies the per-code
372
+ // map. The surfaced error keeps the application-error-shaped code intact.
373
+ // End-to-end resolution against the real error handler (getResolvedError) is
374
+ // covered in the cairo Table.importButton spec.
375
+ const jobApplicationErrorMap = {
376
+ duplicate_key: () => ({
377
+ message: 'Bad rows'
378
+ })
379
+ };
292
380
  const {
293
381
  state
294
382
  } = setup({
@@ -301,11 +389,9 @@ describe('ImportState engine (data-hub job)', () => {
301
389
  }
302
390
  }))
303
391
  }, {
304
- onJobError
392
+ jobApplicationErrorMap
305
393
  });
306
394
  await state.startImport();
307
- expect(onJobError).toHaveBeenCalledTimes(1);
308
- expect(typeof onJobError.mock.calls[0][0]).toBe('function');
309
395
  expect(state.importStatus.status).toBe('error');
310
396
  const error = state.importStatus.error;
311
397
  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');
@@ -365,7 +451,7 @@ describe('ImportState engine (data-hub job)', () => {
365
451
  // forever; it must stop after the first fetch.
366
452
  expect(client.getJob).toHaveBeenCalledTimes(1);
367
453
  });
368
- it('does not restore when closed during the success completion linger', async () => {
454
+ it('does not restore a completed import when the summary is closed', async () => {
369
455
  const restoreFn = jest.fn().mockResolvedValue(undefined);
370
456
  const {
371
457
  state,
@@ -376,30 +462,19 @@ describe('ImportState engine (data-hub job)', () => {
376
462
  restore: restoreFn
377
463
  })
378
464
  });
465
+ await state.startImport();
379
466
 
380
- // A 0ms macrotask drains the pending async chain without firing the 600ms
381
- // linger timer.
382
- const flush = () => new Promise(resolve => setTimeout(resolve, 0));
383
- const promise = state.startImport();
384
- // Run backup → upload → submit → poll (terminal) → success, parking on the
385
- // 600ms completion linger (still on the 'progress' step) without firing it.
386
- await flush();
387
- await flush();
388
- await flush();
467
+ // The import finished on the summary step and the collection was refreshed.
389
468
  expect(state.importStatus.isSuccess).toBe(true);
390
- expect(state.currentStep).toBe('progress');
469
+ expect(state.currentStep).toBe('summary');
470
+ expect(collection.clearResultAndMoveToStart).toHaveBeenCalledWith({
471
+ force: true
472
+ });
391
473
 
392
- // User closes the modal mid-linger.
474
+ // Closing the summary must not revert a successful import.
393
475
  await state.close();
394
- await promise;
395
-
396
- // A successful import must not be reverted; the modal closes and the
397
- // collection refreshes.
398
476
  expect(restoreFn).not.toHaveBeenCalled();
399
477
  expect(importModalState.close).toHaveBeenCalled();
400
- expect(collection.clearResultAndMoveToStart).toHaveBeenCalledWith({
401
- force: true
402
- });
403
478
  });
404
479
  });
405
480
  describe('ImportState writePolicy (id-collision handling)', () => {
@@ -685,6 +760,115 @@ describe('ImportState platformized destination (non–Wix-Data)', () => {
685
760
  const wixData = setup().state;
686
761
  expect(wixData.canConfigureWritePolicy).toBe(true);
687
762
  });
763
+ describe('DataExtension create fields (destination branch)', () => {
764
+ it('enables create-new-field when the destination branch supplies createFields', () => {
765
+ const withCreate = setup({}, {
766
+ destination: {
767
+ storesCatalogProducts: {}
768
+ },
769
+ createFields: jest.fn(async () => undefined)
770
+ }).state;
771
+ expect(withCreate.canCreateNewField).toBe(true);
772
+ const withoutCreate = setup({}, {
773
+ destination: {
774
+ storesCatalogProducts: {}
775
+ }
776
+ }).state;
777
+ expect(withoutCreate.canCreateNewField).toBe(false);
778
+ });
779
+ it('hands createFields the deduped pending columns (source builds its own def)', async () => {
780
+ const createFields = jest.fn(async () => undefined);
781
+ const {
782
+ state,
783
+ client
784
+ } = setup({}, {
785
+ destination: {
786
+ storesCatalogProducts: {}
787
+ },
788
+ createFields
789
+ });
790
+ (0, _mobx.runInAction)(() => {
791
+ state.csvHeaders = ['Name', 'Color'];
792
+ state.csvRows = [['Apple', 'Red'], ['Pear', 'Green']];
793
+ state.mappingByHeader = {
794
+ Name: {
795
+ kind: 'existing',
796
+ fieldId: 'name'
797
+ },
798
+ Color: {
799
+ kind: 'new',
800
+ fieldType: 'text'
801
+ }
802
+ };
803
+ });
804
+ await state.startImport();
805
+ expect(createFields).toHaveBeenCalledTimes(1);
806
+ // Core passes the neutral pending column (deduped key + name + the mapping);
807
+ // the DataExtension source turns it into its own field shape.
808
+ expect(createFields).toHaveBeenCalledWith([{
809
+ key: 'color',
810
+ displayName: 'Color',
811
+ mapping: {
812
+ kind: 'new',
813
+ fieldType: 'text'
814
+ }
815
+ }]);
816
+ // The consumer's destination is still sent verbatim.
817
+ expect(client.submitImportJob).toHaveBeenCalledWith(expect.objectContaining({
818
+ destination: {
819
+ storesCatalogProducts: {}
820
+ }
821
+ }));
822
+ });
823
+ it('carries the edited mapping config (precise type, permissions, PII) in the pending column', async () => {
824
+ const createFields = jest.fn(async () => undefined);
825
+ const {
826
+ state
827
+ } = setup({}, {
828
+ destination: {
829
+ storesCatalogProducts: {}
830
+ },
831
+ createFields
832
+ });
833
+ const config = {
834
+ deType: 'decimal',
835
+ isPii: true,
836
+ permissions: {
837
+ installedApps: {
838
+ defaultRead: true,
839
+ defaultWrite: false
840
+ }
841
+ }
842
+ };
843
+ (0, _mobx.runInAction)(() => {
844
+ state.csvHeaders = ['Name', 'Count'];
845
+ state.csvRows = [['Apple', '3']];
846
+ state.mappingByHeader = {
847
+ Name: {
848
+ kind: 'existing',
849
+ fieldId: 'name'
850
+ },
851
+ Count: {
852
+ kind: 'new',
853
+ fieldType: 'number',
854
+ displayName: 'Count',
855
+ config
856
+ }
857
+ };
858
+ });
859
+ await state.startImport();
860
+ expect(createFields).toHaveBeenCalledWith([{
861
+ key: 'count',
862
+ displayName: 'Count',
863
+ mapping: {
864
+ kind: 'new',
865
+ fieldType: 'number',
866
+ displayName: 'Count',
867
+ config
868
+ }
869
+ }]);
870
+ });
871
+ });
688
872
  describe('created fields (CAIRO-4400)', () => {
689
873
  const RETURNED_FIELDS = {
690
874
  name: {
@@ -807,7 +991,6 @@ describe('ImportState platformized destination (non–Wix-Data)', () => {
807
991
  const updateSchema = jest.fn(async () => RETURNED_FIELDS);
808
992
  const {
809
993
  state,
810
- importModalState,
811
994
  collection
812
995
  } = setup({}, {
813
996
  updateSchema,
@@ -818,22 +1001,32 @@ describe('ImportState platformized destination (non–Wix-Data)', () => {
818
1001
  expect(mergeFields).toHaveBeenCalledTimes(1);
819
1002
  // A throwing merge is swallowed — the completed import is untouched.
820
1003
  expect(state.importStatus.status).toBe('success');
821
- expect(importModalState.close).toHaveBeenCalled();
1004
+ expect(state.currentStep).toBe('summary');
822
1005
  expect(collection.clearResultAndMoveToStart).toHaveBeenCalled();
823
1006
  consoleError.mockRestore();
824
1007
  });
825
1008
  });
826
1009
  describe('schema-backed config (live getters + create-field permission)', () => {
827
- // A schema-backed config sets canAddFields explicitly and supplies
828
- // updateSchema/idFieldKey the shape a SchemaImportRef satisfies.
1010
+ // A schema-backed config: the CMS `dataSource` branch a SchemaImportRef
1011
+ // satisfies. It supplies `updateSchema` only when create is permitted — its
1012
+ // presence is the create-new-field opt-in (no separate flag).
829
1013
  const schemaConfig = (over = {}) => ({
830
- fields: [{
1014
+ fields: over.fields ?? [{
831
1015
  id: 'name',
832
1016
  header: 'Name'
833
1017
  }],
834
- collectionId: 'RefCollection',
835
- canAddFields: false,
836
- ...over
1018
+ dataSource: {
1019
+ collectionId: over.collectionId ?? 'RefCollection',
1020
+ ...(over.updateSchema && {
1021
+ updateSchema: over.updateSchema
1022
+ })
1023
+ },
1024
+ ...(over.idFieldKey && {
1025
+ idFieldKey: over.idFieldKey
1026
+ }),
1027
+ ...(over.mergeFields && {
1028
+ mergeFields: over.mergeFields
1029
+ })
837
1030
  });
838
1031
  it('reads fields live from the config rather than snapshotting them', () => {
839
1032
  const config = schemaConfig();
@@ -856,17 +1049,17 @@ describe('ImportState platformized destination (non–Wix-Data)', () => {
856
1049
  config.fields = updated;
857
1050
  expect(state.fields).toBe(updated);
858
1051
  });
859
- it('gates create-new-field on canAddFields even when updateSchema is present', () => {
1052
+ it('gates create-new-field on the presence of a create callback (updateSchema)', () => {
1053
+ // No create callback ⇒ create-new-field off (the source didn't permit it).
860
1054
  const off = setup({}, {
861
- config: schemaConfig({
862
- canAddFields: false,
863
- updateSchema: jest.fn()
864
- })
1055
+ config: schemaConfig()
865
1056
  });
866
1057
  expect(off.state.canCreateNewField).toBe(false);
1058
+
1059
+ // A create callback present ⇒ on.
867
1060
  const on = setup({}, {
868
1061
  config: schemaConfig({
869
- canAddFields: true
1062
+ updateSchema: jest.fn()
870
1063
  })
871
1064
  });
872
1065
  expect(on.state.canCreateNewField).toBe(true);
@@ -906,7 +1099,6 @@ describe('ImportState platformized destination (non–Wix-Data)', () => {
906
1099
  state
907
1100
  } = setup({}, {
908
1101
  config: schemaConfig({
909
- canAddFields: true,
910
1102
  updateSchema
911
1103
  })
912
1104
  });
@@ -947,4 +1139,147 @@ describe('ImportState platformized destination (non–Wix-Data)', () => {
947
1139
  });
948
1140
  });
949
1141
  });
1142
+ describe('ImportState required fields (CAIRO-4452)', () => {
1143
+ beforeEach(() => {
1144
+ jest.useRealTimers();
1145
+ });
1146
+ it('is satisfied when a required field has a column mapped to it', () => {
1147
+ const {
1148
+ state
1149
+ } = setup({}, {
1150
+ config: {
1151
+ fields: [{
1152
+ id: 'name',
1153
+ header: 'Name',
1154
+ required: true
1155
+ }],
1156
+ dataSource: {
1157
+ collectionId: 'Movies'
1158
+ }
1159
+ }
1160
+ });
1161
+
1162
+ // The seeded mapping maps the "Name" column to the `name` field.
1163
+ expect(state.hasMissingRequiredFields).toBe(false);
1164
+ expect(state.missingRequiredFields).toEqual([]);
1165
+ });
1166
+ it('flags a required field the user set to "don\'t import"', () => {
1167
+ const {
1168
+ state
1169
+ } = setup({}, {
1170
+ config: {
1171
+ fields: [{
1172
+ id: 'name',
1173
+ header: 'Name',
1174
+ required: true
1175
+ }],
1176
+ dataSource: {
1177
+ collectionId: 'Movies'
1178
+ }
1179
+ }
1180
+ });
1181
+ state.setMapping('Name', null);
1182
+ expect(state.hasMissingRequiredFields).toBe(true);
1183
+ expect(state.missingRequiredFields.map(f => f.id)).toEqual(['name']);
1184
+ });
1185
+ it('ignores unmapped fields that are not required', () => {
1186
+ const {
1187
+ state
1188
+ } = setup({}, {
1189
+ config: {
1190
+ fields: [{
1191
+ id: 'name',
1192
+ header: 'Name'
1193
+ }, {
1194
+ id: 'year',
1195
+ header: 'Year',
1196
+ required: false
1197
+ }],
1198
+ dataSource: {
1199
+ collectionId: 'Movies'
1200
+ }
1201
+ }
1202
+ });
1203
+ state.setMapping('Name', null);
1204
+ expect(state.hasMissingRequiredFields).toBe(false);
1205
+ });
1206
+ it('is not satisfied by a create-new-field mapping', () => {
1207
+ const {
1208
+ state
1209
+ } = setup({}, {
1210
+ config: {
1211
+ fields: [{
1212
+ id: 'name',
1213
+ header: 'Name',
1214
+ required: true
1215
+ }],
1216
+ dataSource: {
1217
+ collectionId: 'Movies',
1218
+ updateSchema: async () => undefined
1219
+ }
1220
+ }
1221
+ });
1222
+
1223
+ // Mapping the column to a brand-new field can't fill the existing required
1224
+ // field, so it stays missing.
1225
+ state.setNewFieldMapping('Name');
1226
+ expect(state.hasMissingRequiredFields).toBe(true);
1227
+ });
1228
+ it('blocks startImport and shows an error toast when a required field is missing', async () => {
1229
+ const {
1230
+ state,
1231
+ client,
1232
+ showToast
1233
+ } = setup({}, {
1234
+ config: {
1235
+ fields: [{
1236
+ id: 'name',
1237
+ header: 'Name',
1238
+ required: true
1239
+ }, {
1240
+ id: 'year',
1241
+ header: 'Year'
1242
+ }],
1243
+ dataSource: {
1244
+ collectionId: 'Movies'
1245
+ }
1246
+ }
1247
+ });
1248
+
1249
+ // Keep a mapping (so the import isn't blocked by "no mappings") but leave the
1250
+ // required `name` field unmapped.
1251
+ (0, _mobx.runInAction)(() => {
1252
+ state.csvHeaders = ['Name', 'Year'];
1253
+ state.csvRows = [['Apple', '2020']];
1254
+ });
1255
+ state.setMapping('Name', null);
1256
+ state.setMapping('Year', 'year');
1257
+ await state.startImport();
1258
+ expect(client.uploadFile).not.toHaveBeenCalled();
1259
+ expect(client.submitImportJob).not.toHaveBeenCalled();
1260
+ expect(state.currentStep).not.toBe('progress');
1261
+ expect(showToast).toHaveBeenCalledWith(expect.objectContaining({
1262
+ type: 'ERROR'
1263
+ }));
1264
+ });
1265
+ it('proceeds with import when all required fields are mapped', async () => {
1266
+ const {
1267
+ state,
1268
+ client
1269
+ } = setup({}, {
1270
+ config: {
1271
+ fields: [{
1272
+ id: 'name',
1273
+ header: 'Name',
1274
+ required: true
1275
+ }],
1276
+ dataSource: {
1277
+ collectionId: 'Movies'
1278
+ }
1279
+ }
1280
+ });
1281
+ await state.startImport();
1282
+ expect(client.submitImportJob).toHaveBeenCalledTimes(1);
1283
+ });
1284
+ });
950
1285
  //# sourceMappingURL=ImportState.engine.spec.js.map