@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
@@ -17,33 +17,39 @@ var _detectFieldType = require("./detectFieldType");
17
17
  var _fieldEdit = require("./fieldEdit");
18
18
  /** Outcome of a server-side import job, derived from the job's progress. */
19
19
 
20
+ /** A rejected row: its index into `csvRows` and the server's failure reason. */
21
+
20
22
  /**
21
23
  * Optional snapshot taken before the import runs, returning a `restore` used to
22
24
  * undo the import on cancel. See {@link BackupResult.restore} for the revert
23
25
  * contract under upsert.
24
26
  */
25
27
 
28
+ /** The application error from a failed import job, handed to a resolver. */
29
+
26
30
  /**
27
- * Consumer hook for resolving a failed import job's message. An import runs
28
- * server-side, so a failure arrives on a 200 (the poll succeeds; the job
29
- * doesn't) with no HTTP error to carry the application error. Cairo hands the
30
- * consumer `run` an operation that rejects with a synthetic, HTTP-error-shaped
31
- * error carrying the job's application errorand the consumer wraps it with
32
- * their own `withErrorHandler` + error-code map (their codes, their
33
- * translations), keeping the map coupled to the `withErrorHandler` call as
34
- * everywhere else (e.g. `fetchData`). The map is keyed by application-error code
35
- * (the v2 errorHandler shape, like `getExportAsyncJobErrors`):
31
+ * Consumer-supplied map from a failed import job's application-error code to a
32
+ * resolver that returns the message to show. An import runs server-side, so a
33
+ * failure arrives on a 200 (the poll succeeds; the job doesn't) with the
34
+ * application error in the job status. The consumer supplies only the
35
+ * code→message map (their codes, their translations)ImportState applies it
36
+ * through the container's `errorHandler` internally, so the progress step can
37
+ * resolve the message via `getResolvedError`. The resolver receives the job's
38
+ * application error (its `code`/`description`/`data`):
36
39
  *
37
- * onJobError: (run) =>
38
- * errorHandler.withErrorHandler(run, {
39
- * [jobErrorCode]: (error) => ({ message: translate(...) }),
40
- * })
40
+ * jobApplicationErrorMap: {
41
+ * [jobErrorCode]: (error) => ({ message: translate(..., error.data) }),
42
+ * }
41
43
  *
42
- * `withErrorHandler` attaches the map to the error object and re-rejects it, so
43
- * the progress step resolves a consumer-specific message via `getResolvedError`.
44
- * When omitted, the error still surfaces but resolves to the generic message.
44
+ * When omitted, a failed job falls back to the generic error message.
45
45
  */
46
46
 
47
+ // `required` is added on top of the ambassador descriptor (which carries only
48
+ // `id`/`header`): a field the destination mandates. Consumers set it — the
49
+ // schema-backed config forwards `validation.required`, manual/platformized
50
+ // configs set it per field — and the mapping step blocks import while any
51
+ // required field has no column mapped to it (CAIRO-4452).
52
+
47
53
  // `NewFieldType`, `detectColumnType`, etc. are re-exported from the package via
48
54
  // the `ImportState` barrel (`./index`), which also exports `./detectFieldType`.
49
55
 
@@ -56,8 +62,16 @@ var _fieldEdit = require("./fieldEdit");
56
62
  // Per-type configuration for a new field, captured from the "Edit field" editor
57
63
  // (CAIRO-4398). All optional — an unedited field carries none and is created
58
64
  // from detected defaults only. `displayName` is intentionally not here: the
59
- // (possibly user-overridden) name is deduped and surfaced as
60
- // `NewFieldDefinition.displayName`.
65
+ // (possibly user-overridden) name is deduped and surfaced as the field def's
66
+ // `displayName`.
67
+ //
68
+ // This is core-internal storage on the column mapping, shared by both sources —
69
+ // the CMS editor fills `validation`/`isPii`/`helpText`, the DataExtension editor
70
+ // fills `permissions`/`isPii`/`helpText`/`deType`. The two never cross: which
71
+ // subset is read is decided by the config's `dataSource` branch when the field
72
+ // def is built (`_resolveFieldMapping`). The consumer-facing create callbacks
73
+ // still receive distinct def types (`NewFieldDefinition` vs
74
+ // `NewDataExtensionFieldDef`), so this superset is not part of the public API.
61
75
 
62
76
  // A new field to add to the collection schema during import. `key` is decided by
63
77
  // Cairo (derived from the column header — or the user's name override — deduped)
@@ -80,6 +94,17 @@ var _fieldEdit = require("./fieldEdit");
80
94
  * load.
81
95
  */
82
96
 
97
+ /**
98
+ * Creates the pending new DataExtension custom fields before the rows are
99
+ * uploaded — the `destination` branch's counterpart to `updateSchema`. Its
100
+ * presence is the "create new field" opt-in for a DataExtension import. Receives
101
+ * the source-neutral pending columns (Cairo-decided keys + names + the edited
102
+ * config); the DataExtension source (`DataExtensionImportRef`) builds its own
103
+ * field shape from them, so no DataExtension-specific shape leaks into core.
104
+ * Optionally returns the updated fields map to merge into the table (CAIRO-4400);
105
+ * DataExtension refreshes its own schema in place, so returning `void` is fine.
106
+ */
107
+
83
108
  /**
84
109
  * How a CSV column is mapped:
85
110
  * - `existing` — into an existing collection field by id
@@ -91,18 +116,31 @@ var _fieldEdit = require("./fieldEdit");
91
116
  * - `null` — don't import (skip)
92
117
  */
93
118
 
119
+ /** A `new`-mapped column with its deduped key + display name, handed to a
120
+ * source's create callback so it can build its own field shape. */
121
+
122
+ /**
123
+ * Where the import writes and how it creates new fields, split per source. The
124
+ * discriminant (`collectionId` vs `destination`) makes the two mutually
125
+ * exclusive and pairs each target with its own create-fields callback, so a CMS
126
+ * target can't be paired with a DataExtension create callback (or vice versa).
127
+ * Each branch's create members are optional — a plain import supplies none and
128
+ * is mapping-only.
129
+ */
130
+
94
131
  /**
95
132
  * The import inputs a consumer supplies. Satisfied both by a plain object (the
96
- * manual case) and by a live schema reference (`useSchemaImport` in
97
- * `@wix/patterns`) whose members are getters read on demand — so the async
98
- * schema load needn't resolve before `ImportState` is built. `ImportState` holds
99
- * it by reference (never spreads it) so those getters stay live.
133
+ * manual case) and by a live reference (`useSchemaImport` in `@wix/patterns`, or
134
+ * the internally-built `DataExtensionImportRef`) whose members are getters read
135
+ * on demand — so the async schema load needn't resolve before `ImportState` is
136
+ * built. `ImportState` holds it by reference (never spreads it) so those getters
137
+ * stay live.
100
138
  */
101
139
 
102
- // How long to keep the completed progress bar (100%) on screen before closing
103
- // the modal on a clean import. Without it, an import of very few rows finishes
104
- // near-instantly and the modal appears to vanish without ever showing the bar
105
- // reaching completion.
140
+ // How long to hold the completed (100%) progress bar on screen before moving to
141
+ // the summary step (CAIRO-4476). The job reports counts only when it finishes,
142
+ // so the bar sits at 0 for the whole import and would otherwise flip straight to
143
+ // the summary without the user ever seeing it reach completion.
106
144
  const COMPLETION_LINGER_MS = 600;
107
145
 
108
146
  // The Wix Data collection's id field key. A CSV column mapped to this field lets
@@ -185,7 +223,9 @@ class ImportState {
185
223
  (0, _defineProperty2.default)(this, "totalCount", 0);
186
224
  (0, _defineProperty2.default)(this, "result", {
187
225
  succeeded: 0,
188
- failed: 0
226
+ failed: 0,
227
+ created: 0,
228
+ updated: 0
189
229
  });
190
230
  (0, _defineProperty2.default)(this, "isBackingUp", false);
191
231
  (0, _defineProperty2.default)(this, "isRestoring", false);
@@ -195,10 +235,11 @@ class ImportState {
195
235
  // Id of the running data-movement job, set once submitted — used to cancel it
196
236
  // (`terminateJob`) if the user closes mid-import.
197
237
  (0, _defineProperty2.default)(this, "_jobId", null);
198
- // The column whose pending new field the field editor is currently editing.
199
- // Not observable transient wiring between `beginFieldEdit` and the editor's
200
- // `applyFieldEdit` callback, never rendered.
201
- (0, _defineProperty2.default)(this, "_editingHeader", null);
238
+ // The column whose pending new field the field editor is currently editing, or
239
+ // `null` when the editor is closed. Observable so the source's field-editor
240
+ // component (rendered via `renderFieldEditor`) opens/prefills itself when this
241
+ // is set and closes when it clears — the editor is decoupled from the layout.
242
+ (0, _defineProperty2.default)(this, "editingHeader", null);
202
243
  // Holds `updateSchema`'s returned fields map between field creation (pre-upload)
203
244
  // and success. Kept private/non-observable so it can't update columns mid-import;
204
245
  // it's promoted to the observable `createdFieldsMap` only on success.
@@ -233,7 +274,12 @@ class ImportState {
233
274
  hasMappings: _mobx.computed,
234
275
  hasNoMappedData: _mobx.computed,
235
276
  selectedFieldIds: _mobx.computed,
277
+ missingRequiredFields: _mobx.computed,
278
+ hasMissingRequiredFields: _mobx.computed,
236
279
  idMappedHeader: _mobx.computed,
280
+ editingHeader: _mobx.observable.ref,
281
+ editingMapping: _mobx.computed,
282
+ endFieldEdit: _mobx.action.bound,
237
283
  setWritePolicy: _mobx.action.bound,
238
284
  onUploadContinue: _mobx.action.bound,
239
285
  goToUpload: _mobx.action.bound,
@@ -242,7 +288,10 @@ class ImportState {
242
288
  setNewFieldConfig: _mobx.action.bound,
243
289
  beginFieldEdit: _mobx.action.bound,
244
290
  applyFieldEdit: _mobx.action.bound,
291
+ applyDataExtensionFieldEdit: _mobx.action.bound,
245
292
  close: _mobx.action.bound,
293
+ done: _mobx.action.bound,
294
+ downloadFailedRows: _mobx.action.bound,
246
295
  onFileSelected: _mobx.action.bound,
247
296
  startImport: _mobx.action.bound,
248
297
  reset: _mobx.action.bound
@@ -259,19 +308,36 @@ class ImportState {
259
308
 
260
309
  /** Wix Data collection the import writes into (unset for platformized). */
261
310
  get collectionId() {
262
- return this.config.collectionId;
311
+ const ds = this.config.dataSource;
312
+ return 'collectionId' in ds ? ds.collectionId : undefined;
263
313
  }
264
314
  get destination() {
265
- return this.config.destination;
315
+ const ds = this.config.dataSource;
316
+ return 'destination' in ds ? ds.destination : undefined;
266
317
  }
267
318
  get backup() {
268
319
  return this.config.backup;
269
320
  }
321
+
322
+ /** CMS create-fields callback, present only on the `collectionId` branch. */
270
323
  get updateSchema() {
271
- return this.config.updateSchema;
324
+ const ds = this.config.dataSource;
325
+ return 'collectionId' in ds ? ds.updateSchema : undefined;
272
326
  }
273
- get onJobError() {
274
- return this.config.onJobError;
327
+
328
+ /** DataExtension create-fields callback, present only on the `destination` branch. */
329
+ get createDataExtensionFields() {
330
+ const ds = this.config.dataSource;
331
+ return 'destination' in ds ? ds.createFields : undefined;
332
+ }
333
+
334
+ /** DataExtension field-management for the editor, on the `destination` branch. */
335
+ get dataExtensionFieldManagement() {
336
+ const ds = this.config.dataSource;
337
+ return 'destination' in ds ? ds.fieldManagement : undefined;
338
+ }
339
+ get jobApplicationErrorMap() {
340
+ return this.config.jobApplicationErrorMap;
275
341
  }
276
342
 
277
343
  // The collection's id-field key; a schema-backed config may report a non-`_id`
@@ -320,6 +386,27 @@ class ImportState {
320
386
  return new Set(Object.values(this.mappingByHeader).filter(m => (m == null ? void 0 : m.kind) === 'existing').map(m => m.fieldId));
321
387
  }
322
388
 
389
+ /**
390
+ * Required destination fields that no CSV column is mapped to (CAIRO-4452).
391
+ * A required field is an existing collection field, so only an `existing`
392
+ * mapping can satisfy it — a "create new field" column can't. Covers both
393
+ * ways a required field ends up unmapped: the CSV has no matching column at
394
+ * all, or the user set its column to "don't import". Drives the footer
395
+ * indicator and blocks `startImport`.
396
+ */
397
+ get missingRequiredFields() {
398
+ return this.fields.filter(field => field.required && field.id != null && !this.selectedFieldIds.has(field.id));
399
+ }
400
+ get hasMissingRequiredFields() {
401
+ return this.missingRequiredFields.length > 0;
402
+ }
403
+
404
+ // Comma-separated display names of the missing required fields, for the block
405
+ // toast. Falls back to the field id when a field carries no header.
406
+ _missingRequiredFieldNames() {
407
+ return this.missingRequiredFields.map(field => field.header || field.id || '').filter(Boolean).join(', ');
408
+ }
409
+
323
410
  /**
324
411
  * The CSV header currently mapped to the collection's `_id` field, or `null`
325
412
  * when no column targets it. Drives both the "When items share the same ID"
@@ -337,12 +424,13 @@ class ImportState {
337
424
 
338
425
  /**
339
426
  * Whether columns may be mapped to a brand-new field created during import.
340
- * When the config sets `canAddFields` (e.g. a schema's create-field permission)
341
- * that gates it; otherwise the presence of `updateSchema` is the opt-in (see
342
- * `getDefaultMapping`).
427
+ * The opt-in is the presence of the active `dataSource` branch's create
428
+ * callback `updateSchema` (CMS) or `createFields` (DataExtension). A source
429
+ * that can't create fields (e.g. no create permission) supplies neither, so
430
+ * columns fall back to "don't import" (see `getDefaultMapping`).
343
431
  */
344
432
  get canCreateNewField() {
345
- return this.config.canAddFields ?? this.config.updateSchema != null;
433
+ return this.updateSchema != null || this.createDataExtensionFields != null;
346
434
  }
347
435
 
348
436
  /**
@@ -375,7 +463,9 @@ class ImportState {
375
463
  this.totalCount = 0;
376
464
  this.result = {
377
465
  succeeded: 0,
378
- failed: 0
466
+ failed: 0,
467
+ created: 0,
468
+ updated: 0
379
469
  };
380
470
  this._pendingFieldsMap = undefined;
381
471
  this.isBackingUp = false;
@@ -386,11 +476,10 @@ class ImportState {
386
476
  }
387
477
  async close() {
388
478
  // While on the progress step we treat close as a cancel — terminate the job
389
- // and restore the backup — UNLESS the import already succeeded. On the
390
- // success path `startImport` lingers ~600ms on the completed bar with the
391
- // step still 'progress'; closing during that window must NOT restore (it
392
- // would undo a clean import). A successful close just closes the modal and
393
- // lets the success path do its collection refresh.
479
+ // and restore the backup — UNLESS the import already succeeded. On success
480
+ // `startImport` moves straight to the 'summary' step, so a close from there
481
+ // just closes the modal (the summary's own `done` is the normal exit); this
482
+ // guard covers only closing mid-import.
394
483
  if (this.currentStep === 'progress' && !this.importStatus.isSuccess) {
395
484
  var _this$_abortControlle2;
396
485
  this._cancelled = true;
@@ -414,6 +503,14 @@ class ImportState {
414
503
  }
415
504
  this.importModalState.close();
416
505
  }
506
+
507
+ /**
508
+ * Closes the summary step at the end of a completed import. The collection was
509
+ * already refreshed when the summary opened, so this only dismisses the modal.
510
+ */
511
+ done() {
512
+ this.importModalState.close();
513
+ }
417
514
  async _restoreBackup() {
418
515
  const restoreFn = this.restoreFn;
419
516
  if (!restoreFn) {
@@ -584,51 +681,44 @@ class ImportState {
584
681
  }
585
682
  });
586
683
  }
587
- _showPartialSuccessToast() {
588
- var _this$showToast3;
589
- (_this$showToast3 = this.showToast) == null || _this$showToast3.call(this, {
590
- type: 'WARNING',
591
- message: this.translate('cairo.import.partialSuccessToast', {
592
- failed: String(this.result.failed),
593
- imported: String(this.result.succeeded)
594
- }),
595
- action: {
596
- text: this.translate('cairo.import.downloadFailedRows'),
597
- onClick: () => this._downloadFailedRows(),
598
- removeToastOnClick: true
599
- }
600
- });
601
- }
602
684
 
603
685
  /**
604
- * Builds a plain, re-importable CSV of the rows the server rejected the
605
- * original headers plus just the failed rows and hands it to `downloadCsv`.
606
- * The failed rows come from the job's movement logs (which row failed), mapped
607
- * back to the parsed `csvRows` we still hold in memory.
686
+ * Builds a re-importable CSV of the rows the server rejected and hands it to
687
+ * `downloadCsv`. Each failed row is prefixed with a leading "why it failed"
688
+ * column holding the server's error message, so the user can see and fix each
689
+ * reason. The failed rows (and their reasons) come from the job's movement
690
+ * logs, mapped back to the parsed `csvRows` we still hold in memory.
608
691
  */
609
- async _downloadFailedRows() {
692
+ async downloadFailedRows() {
610
693
  var _this$downloadCsv;
611
694
  const jobId = this._jobId;
612
695
  if (!jobId) {
613
696
  return;
614
697
  }
615
- let indices;
698
+ let failures;
616
699
  try {
617
- indices = await this._fetchFailedRowIndices(jobId);
700
+ failures = await this._fetchFailedRows(jobId);
618
701
  } catch (error) {
619
702
  console.error('Fetching failed rows report failed', error);
620
703
  return;
621
704
  }
622
- const failedRows = indices.map(index => this.csvRows[index]).filter(row => row != null);
623
- if (failedRows.length === 0) {
705
+ const rows = failures.map(({
706
+ index,
707
+ reason
708
+ }) => {
709
+ const row = this.csvRows[index];
710
+ return row != null ? [reason, ...row] : null;
711
+ }).filter(row => row != null);
712
+ if (rows.length === 0) {
624
713
  return;
625
714
  }
626
- const content = (0, _parseCsv.unparseCsv)(this.csvHeaders, failedRows);
715
+ const headers = [this.translate('cairo.import.failureReasonColumn'), ...this.csvHeaders];
716
+ const content = (0, _parseCsv.unparseCsv)(headers, rows);
627
717
  (_this$downloadCsv = this.downloadCsv) == null || _this$downloadCsv.call(this, content, `failed-${this.fileName || 'import.csv'}`);
628
718
  }
629
719
 
630
- // Pages through the job's movement logs and returns the `csvRows` indices of
631
- // the rows that failed. data-hub's CSV decoder
720
+ // Pages through the job's movement logs and returns, for each row that failed,
721
+ // its `csvRows` index and the server's failure reason. data-hub's CSV decoder
632
722
  // (wix-data-hub: packages/wix-data-hub-executor-c/src/plugins/formats/csv/
633
723
  // csv-binary-decoder.ts) parses with the `csv-parse` library (`columns: true`)
634
724
  // and sets `sourceItemId = "row ${++index + 1}"`, incrementing once **per
@@ -637,8 +727,8 @@ class ImportState {
637
727
  // `csvRows[0]`, i.e. `csvRows[N - 2]`. Because `csv-parse` is RFC-4180,
638
728
  // quoted cells containing embedded newlines are a single record, so they do
639
729
  // NOT shift the numbering. Out-of-range indices are guarded below.
640
- async _fetchFailedRowIndices(jobId) {
641
- const indices = [];
730
+ async _fetchFailedRows(jobId) {
731
+ const failures = [];
642
732
  let cursor;
643
733
  do {
644
734
  var _pagingMetadata$curso;
@@ -656,12 +746,15 @@ class ImportState {
656
746
  const fileLine = Number((_exec = /\d+/.exec(log.sourceItemId)) == null ? void 0 : _exec[0]);
657
747
  const index = fileLine - 2;
658
748
  if (Number.isInteger(index) && index >= 0 && index < this.csvRows.length) {
659
- indices.push(index);
749
+ failures.push({
750
+ index,
751
+ reason: log.failure.description ?? ''
752
+ });
660
753
  }
661
754
  }
662
755
  cursor = (pagingMetadata == null ? void 0 : pagingMetadata.hasNext) && ((_pagingMetadata$curso = pagingMetadata.cursors) == null ? void 0 : _pagingMetadata$curso.next) || undefined;
663
756
  } while (cursor);
664
- return indices;
757
+ return failures;
665
758
  }
666
759
  setMapping(header, fieldId) {
667
760
  this.mappingByHeader = {
@@ -711,24 +804,56 @@ class ImportState {
711
804
  }
712
805
 
713
806
  /**
714
- * Marks which column's pending new field the field editor is currently
715
- * editing, so a later `applyFieldEdit` (the editor's `onSubmit`) knows where to
716
- * write the result.
807
+ * Opens the field editor for a column's pending new field: sets
808
+ * `editingHeader`, which the source's editor component watches to open and
809
+ * prefill itself. No-op if the column isn't mapped to a new field.
717
810
  */
718
811
  beginFieldEdit(header) {
719
- this._editingHeader = header;
812
+ var _this$mappingByHeader;
813
+ if (((_this$mappingByHeader = this.mappingByHeader[header]) == null ? void 0 : _this$mappingByHeader.kind) === 'new') {
814
+ this.editingHeader = header;
815
+ }
816
+ }
817
+
818
+ /** Closes the field editor (clears `editingHeader`). */
819
+ endFieldEdit() {
820
+ this.editingHeader = null;
720
821
  }
721
822
 
722
823
  /**
723
- * `onSubmit` handler for the field editor: maps the edited field spec into the
724
- * column being edited (set via `beginFieldEdit`). No-op if nothing is being
824
+ * The `new` mapping currently being edited, or `null` when the editor is
825
+ * closed. The editor component reads this to prefill its form.
826
+ */
827
+ get editingMapping() {
828
+ if (this.editingHeader == null) {
829
+ return null;
830
+ }
831
+ const mapping = this.mappingByHeader[this.editingHeader];
832
+ return (mapping == null ? void 0 : mapping.kind) === 'new' ? mapping : null;
833
+ }
834
+
835
+ /**
836
+ * `onSubmit` handler for the CMS field editor: maps the edited field spec into
837
+ * the column being edited (set via `beginFieldEdit`). No-op if nothing is being
725
838
  * edited.
726
839
  */
727
840
  applyFieldEdit(field) {
728
- if (this._editingHeader == null) {
841
+ if (this.editingHeader == null) {
842
+ return;
843
+ }
844
+ this.setNewFieldConfig(this.editingHeader, (0, _fieldEdit.toFieldEdit)(field));
845
+ }
846
+
847
+ /**
848
+ * `onSubmit` handler for the DataExtension field editor: maps the edited
849
+ * DataExtension draft (which carries per-audience permissions) into the column
850
+ * being edited. The `destination`-branch counterpart to `applyFieldEdit`.
851
+ */
852
+ applyDataExtensionFieldEdit(draft) {
853
+ if (this.editingHeader == null) {
729
854
  return;
730
855
  }
731
- this.setNewFieldConfig(this._editingHeader, (0, _fieldEdit.toFieldEdit)(field));
856
+ this.setNewFieldConfig(this.editingHeader, (0, _fieldEdit.toDataExtensionFieldEdit)(draft));
732
857
  }
733
858
 
734
859
  /**
@@ -776,15 +901,15 @@ class ImportState {
776
901
  // The job ran server-side, so a failure arrives on a 200 (the poll
777
902
  // succeeded; the job didn't) with no HTTP error to carry the application
778
903
  // error. Build a synthetic, HTTP-error-shaped error (the error handler
779
- // reads the code from `response.data.details.applicationError`) and hand
780
- // the throwing operation to the consumer's `onJobError` so THEY wrap it
781
- // with their own `withErrorHandler` + error-code map — `job.error` is the
782
- // consumer's application error (their import business logic), so only they
783
- // can map its codes, and the map stays coupled to the `withErrorHandler`
784
- // call as everywhere else (e.g. `fetchData`). The map is attached to the
785
- // error object, which the progress step resolves via `getResolvedError`.
786
- // Without a handler the same error still throws, falling back to the
787
- // generic message.
904
+ // reads the code from `response.data.details.applicationError`), then
905
+ // resolve it through the consumer's per-code map via `withErrorHandler`
906
+ // the same way as `fetchData` etc. `job.error` is the consumer's
907
+ // application error (their import business logic), so only they can map
908
+ // its codes; they supply just the code→message map and we do the
909
+ // wrapping. `withErrorHandler` attaches the map to the error and
910
+ // re-rejects it, so the progress step resolves a consumer-specific
911
+ // message via `getResolvedError`. Without a map the same error still
912
+ // throws, falling back to the generic message.
788
913
  const error = Object.assign(new Error(`ImportJob.${((_job$error = job.error) == null ? void 0 : _job$error.code) ?? 'failed'}`), {
789
914
  response: {
790
915
  data: {
@@ -794,11 +919,17 @@ class ImportState {
794
919
  }
795
920
  }
796
921
  });
797
- const run = () => Promise.reject(error);
798
- if (this.onJobError) {
799
- // The consumer wraps `run` with `withErrorHandler`; it re-rejects with
800
- // `error` (now carrying their map), which propagates from here.
801
- await this.onJobError(run);
922
+ const jobApplicationErrorMap = this.jobApplicationErrorMap;
923
+ if (jobApplicationErrorMap) {
924
+ // Adapt the consumer's code→message map into the error handler's
925
+ // `errorCodesMap`: each entry, when its code matches the job's
926
+ // application error, resolves with that error. `withErrorHandler`
927
+ // attaches the resolved message to `error` and re-rejects it, so the
928
+ // progress step surfaces it via `getResolvedError` — the consumer only
929
+ // supplies the map, not the wrapping.
930
+ const appError = job.error ?? {};
931
+ const errorCodesMap = Object.fromEntries(Object.entries(jobApplicationErrorMap).map(([code, resolve]) => [code, () => resolve(appError)]));
932
+ await this.errorHandler.withErrorHandler(() => Promise.reject(error), errorCodesMap);
802
933
  }
803
934
  throw error;
804
935
  }
@@ -826,10 +957,16 @@ class ImportState {
826
957
  };
827
958
  }
828
959
  _applyJobProgress(job) {
960
+ var _progress$itemsSucces, _progress$itemsSucces2;
829
961
  const progress = job.progress;
830
962
  const succeeded = (progress == null ? void 0 : progress.itemsSuccessful) ?? 0;
831
963
  const failed = (progress == null ? void 0 : progress.itemsRejected) ?? 0;
832
964
  const current = (progress == null ? void 0 : progress.current) ?? succeeded + failed;
965
+ // The server splits successful items into created (new) and updated
966
+ // (existing) — the summary step reports each. Absent on older jobs, in
967
+ // which case both stay 0 and only the total `succeeded` is shown.
968
+ const created = (progress == null || (_progress$itemsSucces = progress.itemsSuccessfulInfo) == null ? void 0 : _progress$itemsSucces.itemsCreated) ?? 0;
969
+ const updated = (progress == null || (_progress$itemsSucces2 = progress.itemsSuccessfulInfo) == null ? void 0 : _progress$itemsSucces2.itemsUpdated) ?? 0;
833
970
 
834
971
  // `totalCount` is fixed up front in `startImport` from the CSV row count
835
972
  // (one uploaded row per CSV row); polling only advances `processedCount`.
@@ -837,10 +974,15 @@ class ImportState {
837
974
  this.processedCount = Math.min(current, this.totalCount);
838
975
  this.result = {
839
976
  succeeded,
840
- failed
977
+ failed,
978
+ created,
979
+ updated
841
980
  };
842
981
  });
843
982
  }
983
+ _lingerOnCompletion() {
984
+ return new Promise(resolve => setTimeout(resolve, COMPLETION_LINGER_MS));
985
+ }
844
986
  async _runBackup() {
845
987
  const backup = this.backup;
846
988
  if (!backup) {
@@ -872,9 +1014,6 @@ class ImportState {
872
1014
  });
873
1015
  return false;
874
1016
  }
875
- _lingerOnCompletion() {
876
- return new Promise(resolve => setTimeout(resolve, COMPLETION_LINGER_MS));
877
- }
878
1017
 
879
1018
  /**
880
1019
  * Resolves every column to the final destination key used in the upload
@@ -885,18 +1024,21 @@ class ImportState {
885
1024
  */
886
1025
  async _resolveFieldMapping() {
887
1026
  const resolved = {};
888
- const newFields = [];
1027
+ // Pending new columns with their deduped key + display name. Source-neutral;
1028
+ // the per-source field def is built from these in `_createPendingFields`.
1029
+ const pending = [];
889
1030
  // Seed with existing field ids/display names so derived keys and names never
890
1031
  // collide with them or with each other.
891
1032
  const usedKeys = new Set(this.fields.map(field => field.id).filter(id => !!id));
892
1033
  const usedNames = new Set(this.fields.map(field => field.header).filter(header => !!header));
1034
+ const canCreate = this.canCreateNewField;
893
1035
  for (const header of this.csvHeaders) {
894
1036
  const mapping = this.mappingByHeader[header];
895
1037
  if (mapping == null) {
896
1038
  resolved[header] = null;
897
1039
  } else if (mapping.kind === 'existing') {
898
1040
  resolved[header] = mapping.fieldId;
899
- } else if (this.updateSchema) {
1041
+ } else if (canCreate) {
900
1042
  var _mapping$displayName;
901
1043
  // The user's name override (from the field editor) seeds the unique
902
1044
  // name/key derivation; otherwise fall back to the column header.
@@ -907,34 +1049,79 @@ class ImportState {
907
1049
  } = uniqueFieldNameAndKey(baseName, usedNames, usedKeys);
908
1050
  usedKeys.add(key);
909
1051
  usedNames.add(displayName);
910
- newFields.push({
1052
+ pending.push({
911
1053
  key,
912
- type: mapping.fieldType,
913
1054
  displayName,
914
- ...(mapping.config && {
915
- config: mapping.config
916
- })
1055
+ mapping
917
1056
  });
918
1057
  resolved[header] = key;
919
1058
  } else {
920
- // "new" mapping without an `updateSchema` callback — nothing to map into.
1059
+ // "new" mapping with no create callback on this source — nothing to map into.
921
1060
  resolved[header] = null;
922
1061
  }
923
1062
  }
924
- if (newFields.length > 0 && this.updateSchema) {
1063
+ if (pending.length > 0) {
925
1064
  // Stash the returned fields map to merge into the table on success; kept off
926
- // the observable until then so columns don't update mid-import. Guard the
927
- // empty map: SchemaState.mergeFields is a full replace, so merging `{}` would
928
- // wipe every column — treat a missing/empty return as "nothing to merge".
929
- const updated = await this.updateSchema(newFields);
930
- this._pendingFieldsMap = updated && Object.keys(updated).length > 0 ? updated : undefined;
1065
+ // the observable until then so columns don't update mid-import.
1066
+ this._pendingFieldsMap = await this._createPendingFields(pending);
931
1067
  }
932
1068
  return resolved;
933
1069
  }
1070
+
1071
+ /**
1072
+ * Creates the pending new fields via the active `dataSource` branch's callback
1073
+ * — `updateSchema` (CMS) or `createFields` (DataExtension) — building the
1074
+ * source-specific field def from the shared, already-deduped pending columns.
1075
+ * The callback may throw; `startImport` surfaces that as the error state.
1076
+ */
1077
+ async _createPendingFields(pending) {
1078
+ const updateSchema = this.updateSchema;
1079
+ const createDataExtensionFields = this.createDataExtensionFields;
1080
+ let updated;
1081
+ if (updateSchema) {
1082
+ updated = await updateSchema(pending.map(({
1083
+ key,
1084
+ displayName,
1085
+ mapping
1086
+ }) => ({
1087
+ key,
1088
+ type: mapping.fieldType,
1089
+ displayName,
1090
+ ...(mapping.config && {
1091
+ config: mapping.config
1092
+ })
1093
+ })));
1094
+ } else if (createDataExtensionFields) {
1095
+ // The DataExtension source builds its own field shape from the neutral
1096
+ // pending columns — no DataExtension-specific def-building in core.
1097
+ updated = await createDataExtensionFields(pending);
1098
+ } else {
1099
+ return undefined;
1100
+ }
1101
+
1102
+ // Guard the empty map: SchemaState.mergeFields is a full replace, so merging
1103
+ // `{}` would wipe every column — treat a missing/empty return as "nothing to
1104
+ // merge".
1105
+ return updated && Object.keys(updated).length > 0 ? updated : undefined;
1106
+ }
934
1107
  async startImport() {
935
1108
  if (!this.hasMappings) {
936
1109
  return;
937
1110
  }
1111
+
1112
+ // Block import while any required destination field is unmapped and tell the
1113
+ // user which (CAIRO-4452). The footer already flags this; the Import button
1114
+ // stays enabled, so this is where the click is stopped.
1115
+ if (this.hasMissingRequiredFields) {
1116
+ var _this$showToast3;
1117
+ (_this$showToast3 = this.showToast) == null || _this$showToast3.call(this, {
1118
+ type: 'ERROR',
1119
+ message: this.translate('cairo.import.missingRequiredMessage', {
1120
+ fields: this._missingRequiredFieldNames()
1121
+ })
1122
+ });
1123
+ return;
1124
+ }
938
1125
  this._cancelled = false;
939
1126
  this._abortController = new AbortController();
940
1127
 
@@ -953,7 +1140,9 @@ class ImportState {
953
1140
  };
954
1141
  this.result = {
955
1142
  succeeded: 0,
956
- failed: 0
1143
+ failed: 0,
1144
+ created: 0,
1145
+ updated: 0
957
1146
  };
958
1147
  this._jobId = null;
959
1148
  // Drop any fields map from a prior attempt so a no-new-fields "Try again"
@@ -1007,17 +1196,19 @@ class ImportState {
1007
1196
  ..._model.queryStatus.success,
1008
1197
  data: this.result
1009
1198
  };
1199
+ // The job reports counts only when it finishes, so the bar sat at 0 for
1200
+ // the whole import (CAIRO-4476). Snap it to 100% and hold it (below)
1201
+ // before the summary step replaces the progress step, so the user sees
1202
+ // the import reach completion.
1203
+ this.processedCount = this.totalCount;
1010
1204
  });
1011
1205
 
1012
- // On a clean import, hold the completed (100%) progress bar on screen
1013
- // briefly so the completion is perceptible even for very few rows. Skip
1014
- // the wait when there are failed rows that path closes and surfaces a
1015
- // toast instead.
1016
- if (this.result.failed === 0) {
1017
- await this._lingerOnCompletion();
1018
- if (this._cancelled) {
1019
- return;
1020
- }
1206
+ // Keep the completed bar on screen briefly before moving on. `importStatus`
1207
+ // is already success, so a close during this window just dismisses the
1208
+ // modal (the `close()` guard doesn't treat it as a cancel).
1209
+ await this._lingerOnCompletion();
1210
+ if (this._cancelled) {
1211
+ return;
1021
1212
  }
1022
1213
 
1023
1214
  // Success path only. `_pendingFieldsMap` holds this run's returned map — it's
@@ -1025,12 +1216,12 @@ class ImportState {
1025
1216
  // `_resolveFieldMapping`; snapshotting here keeps the merge success-gated.
1026
1217
  createdFields = this._pendingFieldsMap;
1027
1218
  (0, _mobx.runInAction)(() => {
1028
- // Rows the server rejected are reported via a toast (the downloadable
1029
- // failed-rows report is added by the failed-rows-report task).
1030
- if (this.result.failed > 0) {
1031
- this._showPartialSuccessToast();
1032
- }
1033
- this.importModalState.close();
1219
+ // Import finished: show the summary step (created / updated / failed
1220
+ // counts, plus a "Download CSV" of the failed rows when any failed).
1221
+ // The modal stays open; the user dismisses it from the summary via
1222
+ // `done`. Refresh the collection now so the table behind the modal is
1223
+ // up to date by the time it closes.
1224
+ this.currentStep = 'summary';
1034
1225
  this.collection.clearResultAndMoveToStart({
1035
1226
  force: true
1036
1227
  });