@wix/bex-core 2.332.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 (149) hide show
  1. package/dist/cjs/assets/locale/messages_ar.json +3 -0
  2. package/dist/cjs/assets/locale/messages_bg.json +3 -0
  3. package/dist/cjs/assets/locale/messages_ca.json +3 -0
  4. package/dist/cjs/assets/locale/messages_cs.json +3 -0
  5. package/dist/cjs/assets/locale/messages_da.json +3 -0
  6. package/dist/cjs/assets/locale/messages_de.json +3 -0
  7. package/dist/cjs/assets/locale/messages_el.json +3 -0
  8. package/dist/cjs/assets/locale/messages_en.json +3 -0
  9. package/dist/cjs/assets/locale/messages_es.json +3 -0
  10. package/dist/cjs/assets/locale/messages_fi.json +3 -0
  11. package/dist/cjs/assets/locale/messages_fr.json +3 -0
  12. package/dist/cjs/assets/locale/messages_he.json +3 -0
  13. package/dist/cjs/assets/locale/messages_hi.json +3 -0
  14. package/dist/cjs/assets/locale/messages_hu.json +3 -0
  15. package/dist/cjs/assets/locale/messages_id.json +3 -0
  16. package/dist/cjs/assets/locale/messages_it.json +3 -0
  17. package/dist/cjs/assets/locale/messages_ja.json +3 -0
  18. package/dist/cjs/assets/locale/messages_ko.json +3 -0
  19. package/dist/cjs/assets/locale/messages_lt.json +3 -0
  20. package/dist/cjs/assets/locale/messages_ms.json +3 -0
  21. package/dist/cjs/assets/locale/messages_nl.json +3 -0
  22. package/dist/cjs/assets/locale/messages_no.json +3 -0
  23. package/dist/cjs/assets/locale/messages_pl.json +3 -0
  24. package/dist/cjs/assets/locale/messages_pt.json +3 -0
  25. package/dist/cjs/assets/locale/messages_ro.json +3 -0
  26. package/dist/cjs/assets/locale/messages_ru.json +3 -0
  27. package/dist/cjs/assets/locale/messages_sk.json +3 -0
  28. package/dist/cjs/assets/locale/messages_sl.json +3 -0
  29. package/dist/cjs/assets/locale/messages_sv.json +3 -0
  30. package/dist/cjs/assets/locale/messages_th.json +3 -0
  31. package/dist/cjs/assets/locale/messages_tl.json +3 -0
  32. package/dist/cjs/assets/locale/messages_tr.json +3 -0
  33. package/dist/cjs/assets/locale/messages_uk.json +3 -0
  34. package/dist/cjs/assets/locale/messages_vi.json +3 -0
  35. package/dist/cjs/assets/locale/messages_zh.json +3 -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/index.js +0 -7
  41. package/dist/cjs/index.js.map +1 -1
  42. package/dist/cjs/services/translations.js.map +1 -1
  43. package/dist/cjs/state/CustomFieldModalState.js +12 -0
  44. package/dist/cjs/state/CustomFieldModalState.js.map +1 -1
  45. package/dist/cjs/state/ImportState/ImportState.js +324 -133
  46. package/dist/cjs/state/ImportState/ImportState.js.map +1 -1
  47. package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js +413 -79
  48. package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -1
  49. package/dist/cjs/state/ImportState/detectFieldType.js +23 -0
  50. package/dist/cjs/state/ImportState/detectFieldType.js.map +1 -1
  51. package/dist/cjs/state/ImportState/fieldEdit.js +49 -0
  52. package/dist/cjs/state/ImportState/fieldEdit.js.map +1 -1
  53. package/dist/cjs/testkit/createInMemorySchemaSource.js +186 -0
  54. package/dist/cjs/testkit/createInMemorySchemaSource.js.map +1 -0
  55. package/dist/cjs/testkit/hubJobImportMocks.js +18 -2
  56. package/dist/cjs/testkit/hubJobImportMocks.js.map +1 -1
  57. package/dist/cjs/testkit/index.js +6 -0
  58. package/dist/cjs/testkit/index.js.map +1 -1
  59. package/dist/cjs/types/SchemaSource.js +2 -1
  60. package/dist/cjs/types/SchemaSource.js.map +1 -1
  61. package/dist/esm/assets/locale/messages_ar.json +3 -0
  62. package/dist/esm/assets/locale/messages_bg.json +3 -0
  63. package/dist/esm/assets/locale/messages_ca.json +3 -0
  64. package/dist/esm/assets/locale/messages_cs.json +3 -0
  65. package/dist/esm/assets/locale/messages_da.json +3 -0
  66. package/dist/esm/assets/locale/messages_de.json +3 -0
  67. package/dist/esm/assets/locale/messages_el.json +3 -0
  68. package/dist/esm/assets/locale/messages_en.json +3 -0
  69. package/dist/esm/assets/locale/messages_es.json +3 -0
  70. package/dist/esm/assets/locale/messages_fi.json +3 -0
  71. package/dist/esm/assets/locale/messages_fr.json +3 -0
  72. package/dist/esm/assets/locale/messages_he.json +3 -0
  73. package/dist/esm/assets/locale/messages_hi.json +3 -0
  74. package/dist/esm/assets/locale/messages_hu.json +3 -0
  75. package/dist/esm/assets/locale/messages_id.json +3 -0
  76. package/dist/esm/assets/locale/messages_it.json +3 -0
  77. package/dist/esm/assets/locale/messages_ja.json +3 -0
  78. package/dist/esm/assets/locale/messages_ko.json +3 -0
  79. package/dist/esm/assets/locale/messages_lt.json +3 -0
  80. package/dist/esm/assets/locale/messages_ms.json +3 -0
  81. package/dist/esm/assets/locale/messages_nl.json +3 -0
  82. package/dist/esm/assets/locale/messages_no.json +3 -0
  83. package/dist/esm/assets/locale/messages_pl.json +3 -0
  84. package/dist/esm/assets/locale/messages_pt.json +3 -0
  85. package/dist/esm/assets/locale/messages_ro.json +3 -0
  86. package/dist/esm/assets/locale/messages_ru.json +3 -0
  87. package/dist/esm/assets/locale/messages_sk.json +3 -0
  88. package/dist/esm/assets/locale/messages_sl.json +3 -0
  89. package/dist/esm/assets/locale/messages_sv.json +3 -0
  90. package/dist/esm/assets/locale/messages_th.json +3 -0
  91. package/dist/esm/assets/locale/messages_tl.json +3 -0
  92. package/dist/esm/assets/locale/messages_tr.json +3 -0
  93. package/dist/esm/assets/locale/messages_uk.json +3 -0
  94. package/dist/esm/assets/locale/messages_vi.json +3 -0
  95. package/dist/esm/assets/locale/messages_zh.json +3 -0
  96. package/dist/esm/assets/locale/version_file +1 -1
  97. package/dist/esm/assets/visual-e2e-locale/messages_visuale2e.json +29 -2
  98. package/dist/esm/hooks/index.js +0 -1
  99. package/dist/esm/hooks/index.js.map +1 -1
  100. package/dist/esm/index.js +0 -1
  101. package/dist/esm/index.js.map +1 -1
  102. package/dist/esm/state/CustomFieldModalState.js +8 -0
  103. package/dist/esm/state/CustomFieldModalState.js.map +1 -1
  104. package/dist/esm/state/ImportState/ImportState.js +246 -111
  105. package/dist/esm/state/ImportState/ImportState.js.map +1 -1
  106. package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js +293 -67
  107. package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -1
  108. package/dist/esm/state/ImportState/detectFieldType.js +19 -0
  109. package/dist/esm/state/ImportState/detectFieldType.js.map +1 -1
  110. package/dist/esm/state/ImportState/fieldEdit.js +44 -0
  111. package/dist/esm/state/ImportState/fieldEdit.js.map +1 -1
  112. package/dist/esm/testkit/createInMemorySchemaSource.js +140 -0
  113. package/dist/esm/testkit/createInMemorySchemaSource.js.map +1 -0
  114. package/dist/esm/testkit/hubJobImportMocks.js +17 -1
  115. package/dist/esm/testkit/hubJobImportMocks.js.map +1 -1
  116. package/dist/esm/testkit/index.js +1 -0
  117. package/dist/esm/testkit/index.js.map +1 -1
  118. package/dist/esm/types/SchemaSource.js.map +1 -1
  119. package/dist/types/hooks/index.d.ts +0 -1
  120. package/dist/types/hooks/index.d.ts.map +1 -1
  121. package/dist/types/index.d.ts +0 -1
  122. package/dist/types/index.d.ts.map +1 -1
  123. package/dist/types/services/translations.d.ts +7 -1
  124. package/dist/types/services/translations.d.ts.map +1 -1
  125. package/dist/types/state/CustomFieldModalState.d.ts +9 -0
  126. package/dist/types/state/CustomFieldModalState.d.ts.map +1 -1
  127. package/dist/types/state/ImportState/ImportState.d.ts +165 -61
  128. package/dist/types/state/ImportState/ImportState.d.ts.map +1 -1
  129. package/dist/types/state/ImportState/detectFieldType.d.ts +2 -0
  130. package/dist/types/state/ImportState/detectFieldType.d.ts.map +1 -1
  131. package/dist/types/state/ImportState/fieldEdit.d.ts +6 -0
  132. package/dist/types/state/ImportState/fieldEdit.d.ts.map +1 -1
  133. package/dist/types/testkit/WixPatternsContainer.driver.d.ts +4 -0
  134. package/dist/types/testkit/WixPatternsContainer.driver.d.ts.map +1 -1
  135. package/dist/types/testkit/createInMemorySchemaSource.d.ts +41 -0
  136. package/dist/types/testkit/createInMemorySchemaSource.d.ts.map +1 -0
  137. package/dist/types/testkit/hubJobImportMocks.d.ts +11 -1
  138. package/dist/types/testkit/hubJobImportMocks.d.ts.map +1 -1
  139. package/dist/types/testkit/index.d.ts +1 -0
  140. package/dist/types/testkit/index.d.ts.map +1 -1
  141. package/dist/types/types/SchemaSource.d.ts +6 -4
  142. package/dist/types/types/SchemaSource.d.ts.map +1 -1
  143. package/package.json +4 -4
  144. package/dist/cjs/hooks/useImportConfig.js +0 -12
  145. package/dist/cjs/hooks/useImportConfig.js.map +0 -1
  146. package/dist/esm/hooks/useImportConfig.js +0 -6
  147. package/dist/esm/hooks/useImportConfig.js.map +0 -1
  148. package/dist/types/hooks/useImportConfig.d.ts +0 -5
  149. 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 = {
@@ -49,15 +53,49 @@ function setup(clientOverrides = {}, extra = {}) {
49
53
  const {
50
54
  detectColumnType,
51
55
  config: configOverride,
52
- ...configOverrides
56
+ collectionId,
57
+ destination,
58
+ updateSchema,
59
+ createFields,
60
+ fieldManagement,
61
+ mergeFields,
62
+ fields,
63
+ idFieldKey,
64
+ backup,
65
+ jobApplicationErrorMap
53
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
+ };
54
81
  const config = configOverride ?? {
55
- fields: [{
82
+ fields: fields ?? [{
56
83
  id: 'name',
57
84
  header: 'Name'
58
85
  }],
59
- collectionId: 'Movies',
60
- ...configOverrides
86
+ dataSource,
87
+ ...(mergeFields && {
88
+ mergeFields
89
+ }),
90
+ ...(idFieldKey && {
91
+ idFieldKey
92
+ }),
93
+ ...(backup && {
94
+ backup
95
+ }),
96
+ ...(jobApplicationErrorMap && {
97
+ jobApplicationErrorMap
98
+ })
61
99
  };
62
100
  const props = {
63
101
  config,
@@ -99,8 +137,8 @@ function setup(clientOverrides = {}, extra = {}) {
99
137
  }
100
138
  describe('ImportState engine (data-hub job)', () => {
101
139
  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.
140
+ // The poll loop awaits real setTimeouts between polls; make sure timers are
141
+ // real even if the preset defaults to fake ones.
104
142
  jest.useRealTimers();
105
143
  });
106
144
  it('uploads the aligned file, submits a job, and reports success', async () => {
@@ -122,17 +160,63 @@ describe('ImportState engine (data-hub job)', () => {
122
160
  }
123
161
  }
124
162
  });
163
+
164
+ // created/updated come from the job's itemsSuccessfulInfo.
125
165
  expect(state.result).toEqual({
126
166
  succeeded: 2,
127
- failed: 0
167
+ failed: 0,
168
+ created: 1,
169
+ updated: 1
128
170
  });
129
171
  expect(state.importStatus.status).toBe('success');
130
- 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();
131
176
  expect(collection.clearResultAndMoveToStart).toHaveBeenCalledWith({
132
177
  force: true
133
178
  });
134
179
  });
135
- 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 () => {
136
220
  const {
137
221
  state,
138
222
  showToast,
@@ -145,25 +229,30 @@ describe('ImportState engine (data-hub job)', () => {
145
229
  itemsSuccessful: 1,
146
230
  itemsRejected: 1,
147
231
  total: 2,
148
- current: 2
232
+ current: 2,
233
+ itemsSuccessfulInfo: {
234
+ itemsCreated: 1,
235
+ itemsUpdated: 0
236
+ }
149
237
  }
150
238
  }))
151
239
  });
152
240
  await state.startImport();
153
241
  expect(state.result).toEqual({
154
242
  succeeded: 1,
155
- failed: 1
243
+ failed: 1,
244
+ created: 1,
245
+ updated: 0
156
246
  });
157
- expect(showToast).toHaveBeenCalledWith(expect.objectContaining({
158
- type: 'WARNING'
159
- }));
160
- 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();
161
251
  });
162
252
  it('maps the failed "row N" by record (unaffected by an embedded-newline cell)', async () => {
163
253
  const {
164
254
  state,
165
- downloadCsv,
166
- showToast
255
+ downloadCsv
167
256
  } = setup({
168
257
  getJob: jest.fn(async () => ({
169
258
  id: 'job-1',
@@ -206,19 +295,17 @@ describe('ImportState engine (data-hub job)', () => {
206
295
  };
207
296
  });
208
297
  await state.startImport();
209
- const toast = showToast.mock.calls[0][0];
210
- await toast.action.onClick();
298
+ await state.downloadFailedRows();
211
299
 
212
300
  // "row 4" → csvRows[2] (Mango), NOT shifted by the embedded newline in row 0.
213
301
  const [content] = downloadCsv.mock.calls[0];
214
302
  expect(content).toContain('Mango');
215
303
  expect(content).not.toContain('Apple');
216
304
  });
217
- 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 () => {
218
306
  const {
219
307
  state,
220
308
  client,
221
- showToast,
222
309
  downloadCsv
223
310
  } = setup({
224
311
  getJob: jest.fn(async () => ({
@@ -235,7 +322,7 @@ describe('ImportState engine (data-hub job)', () => {
235
322
  sourceItemId: 'row 3',
236
323
  failure: {
237
324
  code: 'bad',
238
- description: 'x'
325
+ description: 'Invalid value'
239
326
  }
240
327
  }],
241
328
  pagingMetadata: {
@@ -244,13 +331,12 @@ describe('ImportState engine (data-hub job)', () => {
244
331
  }))
245
332
  });
246
333
  await state.startImport();
247
-
248
- // Trigger the toast's download action.
249
- const toast = showToast.mock.calls[0][0];
250
- await toast.action.onClick();
334
+ await state.downloadFailedRows();
251
335
  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');
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');
254
340
  });
255
341
  it('surfaces an error state with an application-error-shaped error when the job fails', async () => {
256
342
  var _error$response;
@@ -278,17 +364,18 @@ describe('ImportState engine (data-hub job)', () => {
278
364
  expect(importModalState.close).not.toHaveBeenCalled();
279
365
  expect(collection.clearResultAndMoveToStart).not.toHaveBeenCalled();
280
366
  });
281
- it('hands a failed job to onJobError, which surfaces the wrapped error', async () => {
367
+ it('resolves a failed job through the consumer jobApplicationErrorMap', async () => {
282
368
  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));
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
+ };
292
379
  const {
293
380
  state
294
381
  } = setup({
@@ -301,11 +388,9 @@ describe('ImportState engine (data-hub job)', () => {
301
388
  }
302
389
  }))
303
390
  }, {
304
- onJobError
391
+ jobApplicationErrorMap
305
392
  });
306
393
  await state.startImport();
307
- expect(onJobError).toHaveBeenCalledTimes(1);
308
- expect(typeof onJobError.mock.calls[0][0]).toBe('function');
309
394
  expect(state.importStatus.status).toBe('error');
310
395
  const error = state.importStatus.error;
311
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');
@@ -365,7 +450,7 @@ describe('ImportState engine (data-hub job)', () => {
365
450
  // forever; it must stop after the first fetch.
366
451
  expect(client.getJob).toHaveBeenCalledTimes(1);
367
452
  });
368
- 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 () => {
369
454
  const restoreFn = jest.fn().mockResolvedValue(undefined);
370
455
  const {
371
456
  state,
@@ -376,30 +461,19 @@ describe('ImportState engine (data-hub job)', () => {
376
461
  restore: restoreFn
377
462
  })
378
463
  });
464
+ await state.startImport();
379
465
 
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();
466
+ // The import finished on the summary step and the collection was refreshed.
389
467
  expect(state.importStatus.isSuccess).toBe(true);
390
- expect(state.currentStep).toBe('progress');
468
+ expect(state.currentStep).toBe('summary');
469
+ expect(collection.clearResultAndMoveToStart).toHaveBeenCalledWith({
470
+ force: true
471
+ });
391
472
 
392
- // User closes the modal mid-linger.
473
+ // Closing the summary must not revert a successful import.
393
474
  await state.close();
394
- await promise;
395
-
396
- // A successful import must not be reverted; the modal closes and the
397
- // collection refreshes.
398
475
  expect(restoreFn).not.toHaveBeenCalled();
399
476
  expect(importModalState.close).toHaveBeenCalled();
400
- expect(collection.clearResultAndMoveToStart).toHaveBeenCalledWith({
401
- force: true
402
- });
403
477
  });
404
478
  });
405
479
  describe('ImportState writePolicy (id-collision handling)', () => {
@@ -685,6 +759,115 @@ describe('ImportState platformized destination (non–Wix-Data)', () => {
685
759
  const wixData = setup().state;
686
760
  expect(wixData.canConfigureWritePolicy).toBe(true);
687
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
+ });
688
871
  describe('created fields (CAIRO-4400)', () => {
689
872
  const RETURNED_FIELDS = {
690
873
  name: {
@@ -807,7 +990,6 @@ describe('ImportState platformized destination (non–Wix-Data)', () => {
807
990
  const updateSchema = jest.fn(async () => RETURNED_FIELDS);
808
991
  const {
809
992
  state,
810
- importModalState,
811
993
  collection
812
994
  } = setup({}, {
813
995
  updateSchema,
@@ -818,22 +1000,32 @@ describe('ImportState platformized destination (non–Wix-Data)', () => {
818
1000
  expect(mergeFields).toHaveBeenCalledTimes(1);
819
1001
  // A throwing merge is swallowed — the completed import is untouched.
820
1002
  expect(state.importStatus.status).toBe('success');
821
- expect(importModalState.close).toHaveBeenCalled();
1003
+ expect(state.currentStep).toBe('summary');
822
1004
  expect(collection.clearResultAndMoveToStart).toHaveBeenCalled();
823
1005
  consoleError.mockRestore();
824
1006
  });
825
1007
  });
826
1008
  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.
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).
829
1012
  const schemaConfig = (over = {}) => ({
830
- fields: [{
1013
+ fields: over.fields ?? [{
831
1014
  id: 'name',
832
1015
  header: 'Name'
833
1016
  }],
834
- collectionId: 'RefCollection',
835
- canAddFields: false,
836
- ...over
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
+ })
837
1029
  });
838
1030
  it('reads fields live from the config rather than snapshotting them', () => {
839
1031
  const config = schemaConfig();
@@ -856,17 +1048,17 @@ describe('ImportState platformized destination (non–Wix-Data)', () => {
856
1048
  config.fields = updated;
857
1049
  expect(state.fields).toBe(updated);
858
1050
  });
859
- it('gates create-new-field on canAddFields even when updateSchema is present', () => {
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).
860
1053
  const off = setup({}, {
861
- config: schemaConfig({
862
- canAddFields: false,
863
- updateSchema: jest.fn()
864
- })
1054
+ config: schemaConfig()
865
1055
  });
866
1056
  expect(off.state.canCreateNewField).toBe(false);
1057
+
1058
+ // A create callback present ⇒ on.
867
1059
  const on = setup({}, {
868
1060
  config: schemaConfig({
869
- canAddFields: true
1061
+ updateSchema: jest.fn()
870
1062
  })
871
1063
  });
872
1064
  expect(on.state.canCreateNewField).toBe(true);
@@ -906,7 +1098,6 @@ describe('ImportState platformized destination (non–Wix-Data)', () => {
906
1098
  state
907
1099
  } = setup({}, {
908
1100
  config: schemaConfig({
909
- canAddFields: true,
910
1101
  updateSchema
911
1102
  })
912
1103
  });
@@ -947,4 +1138,147 @@ describe('ImportState platformized destination (non–Wix-Data)', () => {
947
1138
  });
948
1139
  });
949
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
+ });
1283
+ });
950
1284
  //# sourceMappingURL=ImportState.engine.spec.js.map