@wix/bex-core 2.331.0 → 2.333.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/dist/cjs/assets/locale/messages_ar.json +11 -0
  2. package/dist/cjs/assets/locale/messages_bg.json +11 -0
  3. package/dist/cjs/assets/locale/messages_ca.json +11 -0
  4. package/dist/cjs/assets/locale/messages_cs.json +11 -0
  5. package/dist/cjs/assets/locale/messages_da.json +11 -0
  6. package/dist/cjs/assets/locale/messages_de.json +11 -0
  7. package/dist/cjs/assets/locale/messages_el.json +11 -0
  8. package/dist/cjs/assets/locale/messages_en.json +11 -0
  9. package/dist/cjs/assets/locale/messages_es.json +11 -0
  10. package/dist/cjs/assets/locale/messages_fi.json +11 -0
  11. package/dist/cjs/assets/locale/messages_fr.json +11 -0
  12. package/dist/cjs/assets/locale/messages_he.json +11 -0
  13. package/dist/cjs/assets/locale/messages_hi.json +11 -0
  14. package/dist/cjs/assets/locale/messages_hu.json +11 -0
  15. package/dist/cjs/assets/locale/messages_id.json +11 -0
  16. package/dist/cjs/assets/locale/messages_it.json +11 -0
  17. package/dist/cjs/assets/locale/messages_ja.json +11 -0
  18. package/dist/cjs/assets/locale/messages_ko.json +11 -0
  19. package/dist/cjs/assets/locale/messages_lt.json +11 -0
  20. package/dist/cjs/assets/locale/messages_ms.json +11 -0
  21. package/dist/cjs/assets/locale/messages_nl.json +11 -0
  22. package/dist/cjs/assets/locale/messages_no.json +11 -0
  23. package/dist/cjs/assets/locale/messages_pl.json +11 -0
  24. package/dist/cjs/assets/locale/messages_pt.json +11 -0
  25. package/dist/cjs/assets/locale/messages_ro.json +11 -0
  26. package/dist/cjs/assets/locale/messages_ru.json +11 -0
  27. package/dist/cjs/assets/locale/messages_sk.json +11 -0
  28. package/dist/cjs/assets/locale/messages_sl.json +11 -0
  29. package/dist/cjs/assets/locale/messages_sv.json +11 -0
  30. package/dist/cjs/assets/locale/messages_th.json +11 -0
  31. package/dist/cjs/assets/locale/messages_tl.json +11 -0
  32. package/dist/cjs/assets/locale/messages_tr.json +11 -0
  33. package/dist/cjs/assets/locale/messages_uk.json +11 -0
  34. package/dist/cjs/assets/locale/messages_vi.json +11 -0
  35. package/dist/cjs/assets/locale/messages_zh.json +11 -0
  36. package/dist/cjs/assets/locale/version_file +1 -1
  37. package/dist/cjs/assets/visual-e2e-locale/messages_visuale2e.json +29 -2
  38. package/dist/cjs/hooks/index.js +0 -6
  39. package/dist/cjs/hooks/index.js.map +1 -1
  40. package/dist/cjs/hooks/useImport.js +3 -13
  41. package/dist/cjs/hooks/useImport.js.map +1 -1
  42. package/dist/cjs/index.js +0 -7
  43. package/dist/cjs/index.js.map +1 -1
  44. package/dist/cjs/services/translations.js.map +1 -1
  45. package/dist/cjs/state/CustomFieldModalState.js +12 -0
  46. package/dist/cjs/state/CustomFieldModalState.js.map +1 -1
  47. package/dist/cjs/state/ImportState/ImportState.js +401 -133
  48. package/dist/cjs/state/ImportState/ImportState.js.map +1 -1
  49. package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js +671 -62
  50. package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -1
  51. package/dist/cjs/state/ImportState/detectFieldType.js +23 -0
  52. package/dist/cjs/state/ImportState/detectFieldType.js.map +1 -1
  53. package/dist/cjs/state/ImportState/fieldEdit.js +49 -0
  54. package/dist/cjs/state/ImportState/fieldEdit.js.map +1 -1
  55. package/dist/cjs/state/TagsState/TagsManagementState.js +17 -44
  56. package/dist/cjs/state/TagsState/TagsManagementState.js.map +1 -1
  57. package/dist/cjs/state/TagsState/TagsSearchState.js +0 -6
  58. package/dist/cjs/state/TagsState/TagsSearchState.js.map +1 -1
  59. package/dist/cjs/testkit/createInMemorySchemaSource.js +186 -0
  60. package/dist/cjs/testkit/createInMemorySchemaSource.js.map +1 -0
  61. package/dist/cjs/testkit/hubJobImportMocks.js +18 -2
  62. package/dist/cjs/testkit/hubJobImportMocks.js.map +1 -1
  63. package/dist/cjs/testkit/index.js +6 -0
  64. package/dist/cjs/testkit/index.js.map +1 -1
  65. package/dist/cjs/types/FieldsSource.js.map +1 -1
  66. package/dist/cjs/types/Schema.js.map +1 -1
  67. package/dist/cjs/types/SchemaSource.js +2 -1
  68. package/dist/cjs/types/SchemaSource.js.map +1 -1
  69. package/dist/esm/assets/locale/messages_ar.json +11 -0
  70. package/dist/esm/assets/locale/messages_bg.json +11 -0
  71. package/dist/esm/assets/locale/messages_ca.json +11 -0
  72. package/dist/esm/assets/locale/messages_cs.json +11 -0
  73. package/dist/esm/assets/locale/messages_da.json +11 -0
  74. package/dist/esm/assets/locale/messages_de.json +11 -0
  75. package/dist/esm/assets/locale/messages_el.json +11 -0
  76. package/dist/esm/assets/locale/messages_en.json +11 -0
  77. package/dist/esm/assets/locale/messages_es.json +11 -0
  78. package/dist/esm/assets/locale/messages_fi.json +11 -0
  79. package/dist/esm/assets/locale/messages_fr.json +11 -0
  80. package/dist/esm/assets/locale/messages_he.json +11 -0
  81. package/dist/esm/assets/locale/messages_hi.json +11 -0
  82. package/dist/esm/assets/locale/messages_hu.json +11 -0
  83. package/dist/esm/assets/locale/messages_id.json +11 -0
  84. package/dist/esm/assets/locale/messages_it.json +11 -0
  85. package/dist/esm/assets/locale/messages_ja.json +11 -0
  86. package/dist/esm/assets/locale/messages_ko.json +11 -0
  87. package/dist/esm/assets/locale/messages_lt.json +11 -0
  88. package/dist/esm/assets/locale/messages_ms.json +11 -0
  89. package/dist/esm/assets/locale/messages_nl.json +11 -0
  90. package/dist/esm/assets/locale/messages_no.json +11 -0
  91. package/dist/esm/assets/locale/messages_pl.json +11 -0
  92. package/dist/esm/assets/locale/messages_pt.json +11 -0
  93. package/dist/esm/assets/locale/messages_ro.json +11 -0
  94. package/dist/esm/assets/locale/messages_ru.json +11 -0
  95. package/dist/esm/assets/locale/messages_sk.json +11 -0
  96. package/dist/esm/assets/locale/messages_sl.json +11 -0
  97. package/dist/esm/assets/locale/messages_sv.json +11 -0
  98. package/dist/esm/assets/locale/messages_th.json +11 -0
  99. package/dist/esm/assets/locale/messages_tl.json +11 -0
  100. package/dist/esm/assets/locale/messages_tr.json +11 -0
  101. package/dist/esm/assets/locale/messages_uk.json +11 -0
  102. package/dist/esm/assets/locale/messages_vi.json +11 -0
  103. package/dist/esm/assets/locale/messages_zh.json +11 -0
  104. package/dist/esm/assets/locale/version_file +1 -1
  105. package/dist/esm/assets/visual-e2e-locale/messages_visuale2e.json +29 -2
  106. package/dist/esm/hooks/index.js +0 -1
  107. package/dist/esm/hooks/index.js.map +1 -1
  108. package/dist/esm/hooks/useImport.js +2 -7
  109. package/dist/esm/hooks/useImport.js.map +1 -1
  110. package/dist/esm/index.js +0 -1
  111. package/dist/esm/index.js.map +1 -1
  112. package/dist/esm/state/CustomFieldModalState.js +8 -0
  113. package/dist/esm/state/CustomFieldModalState.js.map +1 -1
  114. package/dist/esm/state/ImportState/ImportState.js +299 -108
  115. package/dist/esm/state/ImportState/ImportState.js.map +1 -1
  116. package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js +442 -55
  117. package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -1
  118. package/dist/esm/state/ImportState/detectFieldType.js +19 -0
  119. package/dist/esm/state/ImportState/detectFieldType.js.map +1 -1
  120. package/dist/esm/state/ImportState/fieldEdit.js +44 -0
  121. package/dist/esm/state/ImportState/fieldEdit.js.map +1 -1
  122. package/dist/esm/state/TagsState/TagsManagementState.js +17 -33
  123. package/dist/esm/state/TagsState/TagsManagementState.js.map +1 -1
  124. package/dist/esm/state/TagsState/TagsSearchState.js +0 -6
  125. package/dist/esm/state/TagsState/TagsSearchState.js.map +1 -1
  126. package/dist/esm/testkit/createInMemorySchemaSource.js +140 -0
  127. package/dist/esm/testkit/createInMemorySchemaSource.js.map +1 -0
  128. package/dist/esm/testkit/hubJobImportMocks.js +17 -1
  129. package/dist/esm/testkit/hubJobImportMocks.js.map +1 -1
  130. package/dist/esm/testkit/index.js +1 -0
  131. package/dist/esm/testkit/index.js.map +1 -1
  132. package/dist/esm/types/SchemaSource.js.map +1 -1
  133. package/dist/types/hooks/index.d.ts +0 -1
  134. package/dist/types/hooks/index.d.ts.map +1 -1
  135. package/dist/types/hooks/useImport.d.ts +3 -18
  136. package/dist/types/hooks/useImport.d.ts.map +1 -1
  137. package/dist/types/index.d.ts +0 -1
  138. package/dist/types/index.d.ts.map +1 -1
  139. package/dist/types/services/translations.d.ts +20 -1
  140. package/dist/types/services/translations.d.ts.map +1 -1
  141. package/dist/types/state/CustomFieldModalState.d.ts +9 -0
  142. package/dist/types/state/CustomFieldModalState.d.ts.map +1 -1
  143. package/dist/types/state/ImportState/ImportState.d.ts +201 -62
  144. package/dist/types/state/ImportState/ImportState.d.ts.map +1 -1
  145. package/dist/types/state/ImportState/detectFieldType.d.ts +2 -0
  146. package/dist/types/state/ImportState/detectFieldType.d.ts.map +1 -1
  147. package/dist/types/state/ImportState/fieldEdit.d.ts +6 -0
  148. package/dist/types/state/ImportState/fieldEdit.d.ts.map +1 -1
  149. package/dist/types/state/TagsState/TagsManagementState.d.ts.map +1 -1
  150. package/dist/types/state/TagsState/TagsSearchState.d.ts.map +1 -1
  151. package/dist/types/testkit/WixPatternsContainer.driver.d.ts +4 -0
  152. package/dist/types/testkit/WixPatternsContainer.driver.d.ts.map +1 -1
  153. package/dist/types/testkit/createInMemorySchemaSource.d.ts +41 -0
  154. package/dist/types/testkit/createInMemorySchemaSource.d.ts.map +1 -0
  155. package/dist/types/testkit/hubJobImportMocks.d.ts +11 -1
  156. package/dist/types/testkit/hubJobImportMocks.d.ts.map +1 -1
  157. package/dist/types/testkit/index.d.ts +1 -0
  158. package/dist/types/testkit/index.d.ts.map +1 -1
  159. package/dist/types/types/FieldsSource.d.ts +5 -1
  160. package/dist/types/types/FieldsSource.d.ts.map +1 -1
  161. package/dist/types/types/Schema.d.ts +6 -0
  162. package/dist/types/types/Schema.d.ts.map +1 -1
  163. package/dist/types/types/SchemaSource.d.ts +6 -4
  164. package/dist/types/types/SchemaSource.d.ts.map +1 -1
  165. package/package.json +4 -4
  166. package/dist/cjs/hooks/useImportConfig.js +0 -10
  167. package/dist/cjs/hooks/useImportConfig.js.map +0 -1
  168. package/dist/esm/hooks/useImportConfig.js +0 -6
  169. package/dist/esm/hooks/useImportConfig.js.map +0 -1
  170. package/dist/types/hooks/useImportConfig.d.ts +0 -21
  171. package/dist/types/hooks/useImportConfig.d.ts.map +0 -1
@@ -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)
@@ -72,6 +86,23 @@ var _fieldEdit = require("./fieldEdit");
72
86
  * the consumer performs one schema mutation (e.g. `updateSchema(collectionId,
73
87
  * ...addField(key, config))`). Its presence is also the opt-in for the "create
74
88
  * new field" default — see `getDefaultMapping`.
89
+ *
90
+ * Optionally returns the collection's updated fields map, mirroring the schema's
91
+ * `FieldActions`. When it does, the import merges that map into the table's field
92
+ * source on success so the new columns appear with no manual reload (CAIRO-4400).
93
+ * Returning `void` still imports fine — the columns just wait for the next schema
94
+ * load.
95
+ */
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.
75
106
  */
76
107
 
77
108
  /**
@@ -85,10 +116,31 @@ var _fieldEdit = require("./fieldEdit");
85
116
  * - `null` — don't import (skip)
86
117
  */
87
118
 
88
- // How long to keep the completed progress bar (100%) on screen before closing
89
- // the modal on a clean import. Without it, an import of very few rows finishes
90
- // near-instantly and the modal appears to vanish without ever showing the bar
91
- // reaching completion.
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
+
131
+ /**
132
+ * The import inputs a consumer supplies. Satisfied both by a plain object (the
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.
138
+ */
139
+
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.
92
144
  const COMPLETION_LINGER_MS = 600;
93
145
 
94
146
  // The Wix Data collection's id field key. A CSV column mapped to this field lets
@@ -131,18 +183,15 @@ function uniqueFieldNameAndKey(header, usedNames, usedKeys) {
131
183
  }
132
184
  class ImportState {
133
185
  constructor(props) {
134
- (0, _defineProperty2.default)(this, "fields", void 0);
135
- (0, _defineProperty2.default)(this, "collectionId", void 0);
136
- (0, _defineProperty2.default)(this, "destination", void 0);
137
- (0, _defineProperty2.default)(this, "backup", void 0);
138
- (0, _defineProperty2.default)(this, "updateSchema", void 0);
186
+ // The import inputs, held by reference so a live schema-backed config's getters
187
+ // stay live (never spread). The public getters below read from it on demand.
188
+ (0, _defineProperty2.default)(this, "config", void 0);
139
189
  (0, _defineProperty2.default)(this, "collection", void 0);
140
190
  (0, _defineProperty2.default)(this, "importModalState", void 0);
141
191
  (0, _defineProperty2.default)(this, "translate", void 0);
142
192
  (0, _defineProperty2.default)(this, "errorHandler", void 0);
143
193
  (0, _defineProperty2.default)(this, "showToast", void 0);
144
194
  (0, _defineProperty2.default)(this, "downloadCsv", void 0);
145
- (0, _defineProperty2.default)(this, "onJobError", void 0);
146
195
  (0, _defineProperty2.default)(this, "hubJobClient", void 0);
147
196
  (0, _defineProperty2.default)(this, "_detectColumnType", void 0);
148
197
  (0, _defineProperty2.default)(this, "discardChangesModal", new _ConditionalModalState.ConditionalModalState());
@@ -174,7 +223,9 @@ class ImportState {
174
223
  (0, _defineProperty2.default)(this, "totalCount", 0);
175
224
  (0, _defineProperty2.default)(this, "result", {
176
225
  succeeded: 0,
177
- failed: 0
226
+ failed: 0,
227
+ created: 0,
228
+ updated: 0
178
229
  });
179
230
  (0, _defineProperty2.default)(this, "isBackingUp", false);
180
231
  (0, _defineProperty2.default)(this, "isRestoring", false);
@@ -184,22 +235,22 @@ class ImportState {
184
235
  // Id of the running data-movement job, set once submitted — used to cancel it
185
236
  // (`terminateJob`) if the user closes mid-import.
186
237
  (0, _defineProperty2.default)(this, "_jobId", null);
187
- // The column whose pending new field the field editor is currently editing.
188
- // Not observable transient wiring between `beginFieldEdit` and the editor's
189
- // `applyFieldEdit` callback, never rendered.
190
- (0, _defineProperty2.default)(this, "_editingHeader", null);
191
- this.fields = props.fields ?? [];
192
- this.collectionId = props.collectionId;
193
- this.destination = props.destination;
194
- this.backup = props.backup;
195
- this.updateSchema = props.updateSchema;
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);
243
+ // Holds `updateSchema`'s returned fields map between field creation (pre-upload)
244
+ // and success. Kept private/non-observable so it can't update columns mid-import;
245
+ // it's promoted to the observable `createdFieldsMap` only on success.
246
+ (0, _defineProperty2.default)(this, "_pendingFieldsMap", void 0);
247
+ this.config = props.config;
196
248
  this.collection = props.collection;
197
249
  this.importModalState = props.importModalState;
198
250
  this.translate = props.translate;
199
251
  this.errorHandler = props.errorHandler;
200
252
  this.showToast = props.showToast;
201
253
  this.downloadCsv = props.downloadCsv;
202
- this.onJobError = props.onJobError;
203
254
  this._detectColumnType = props.detectColumnType ?? _detectFieldType.detectColumnType;
204
255
  this.hubJobClient = (0, _hubJobClient.createHubJobClient)({
205
256
  httpClient: props.httpClient,
@@ -223,7 +274,12 @@ class ImportState {
223
274
  hasMappings: _mobx.computed,
224
275
  hasNoMappedData: _mobx.computed,
225
276
  selectedFieldIds: _mobx.computed,
277
+ missingRequiredFields: _mobx.computed,
278
+ hasMissingRequiredFields: _mobx.computed,
226
279
  idMappedHeader: _mobx.computed,
280
+ editingHeader: _mobx.observable.ref,
281
+ editingMapping: _mobx.computed,
282
+ endFieldEdit: _mobx.action.bound,
227
283
  setWritePolicy: _mobx.action.bound,
228
284
  onUploadContinue: _mobx.action.bound,
229
285
  goToUpload: _mobx.action.bound,
@@ -232,12 +288,63 @@ class ImportState {
232
288
  setNewFieldConfig: _mobx.action.bound,
233
289
  beginFieldEdit: _mobx.action.bound,
234
290
  applyFieldEdit: _mobx.action.bound,
291
+ applyDataExtensionFieldEdit: _mobx.action.bound,
235
292
  close: _mobx.action.bound,
293
+ done: _mobx.action.bound,
294
+ downloadFailedRows: _mobx.action.bound,
236
295
  onFileSelected: _mobx.action.bound,
237
296
  startImport: _mobx.action.bound,
238
297
  reset: _mobx.action.bound
239
298
  });
240
299
  }
300
+
301
+ // Import inputs read live from `config` — a live schema-backed config exposes
302
+ // these as getters, a manual config as plain values; both read the same way.
303
+
304
+ /** Existing collection fields, for column auto-mapping. */
305
+ get fields() {
306
+ return this.config.fields;
307
+ }
308
+
309
+ /** Wix Data collection the import writes into (unset for platformized). */
310
+ get collectionId() {
311
+ const ds = this.config.dataSource;
312
+ return 'collectionId' in ds ? ds.collectionId : undefined;
313
+ }
314
+ get destination() {
315
+ const ds = this.config.dataSource;
316
+ return 'destination' in ds ? ds.destination : undefined;
317
+ }
318
+ get backup() {
319
+ return this.config.backup;
320
+ }
321
+
322
+ /** CMS create-fields callback, present only on the `collectionId` branch. */
323
+ get updateSchema() {
324
+ const ds = this.config.dataSource;
325
+ return 'collectionId' in ds ? ds.updateSchema : undefined;
326
+ }
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;
341
+ }
342
+
343
+ // The collection's id-field key; a schema-backed config may report a non-`_id`
344
+ // identifier. Defaults to the Wix Data `_id`.
345
+ get idFieldKey() {
346
+ return this.config.idFieldKey ?? ID_FIELD_KEY;
347
+ }
241
348
  init() {
242
349
  return () => {
243
350
  // Unmounting mid-import stops the poll loop (it checks `_cancelled`) so it
@@ -279,6 +386,27 @@ class ImportState {
279
386
  return new Set(Object.values(this.mappingByHeader).filter(m => (m == null ? void 0 : m.kind) === 'existing').map(m => m.fieldId));
280
387
  }
281
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
+
282
410
  /**
283
411
  * The CSV header currently mapped to the collection's `_id` field, or `null`
284
412
  * when no column targets it. Drives both the "When items share the same ID"
@@ -286,7 +414,8 @@ class ImportState {
286
414
  * with no `_id` mapped, every row is a fresh insert and collisions can't occur.
287
415
  */
288
416
  get idMappedHeader() {
289
- const entry = Object.entries(this.mappingByHeader).find(([, mapping]) => (mapping == null ? void 0 : mapping.kind) === 'existing' && mapping.fieldId === ID_FIELD_KEY);
417
+ const idFieldKey = this.idFieldKey;
418
+ const entry = Object.entries(this.mappingByHeader).find(([, mapping]) => (mapping == null ? void 0 : mapping.kind) === 'existing' && mapping.fieldId === idFieldKey);
290
419
  return entry ? entry[0] : null;
291
420
  }
292
421
  setWritePolicy(policy) {
@@ -295,12 +424,13 @@ class ImportState {
295
424
 
296
425
  /**
297
426
  * Whether columns may be mapped to a brand-new field created during import.
298
- * Driven purely by the presence of the `updateSchema` callback — providing the
299
- * capability is the opt-in (see `getDefaultMapping`). Never changes after
300
- * construction, so it isn't an observable.
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`).
301
431
  */
302
432
  get canCreateNewField() {
303
- return this.updateSchema != null;
433
+ return this.updateSchema != null || this.createDataExtensionFields != null;
304
434
  }
305
435
 
306
436
  /**
@@ -333,8 +463,11 @@ class ImportState {
333
463
  this.totalCount = 0;
334
464
  this.result = {
335
465
  succeeded: 0,
336
- failed: 0
466
+ failed: 0,
467
+ created: 0,
468
+ updated: 0
337
469
  };
470
+ this._pendingFieldsMap = undefined;
338
471
  this.isBackingUp = false;
339
472
  this.isRestoring = false;
340
473
  this.restoreFn = null;
@@ -343,11 +476,10 @@ class ImportState {
343
476
  }
344
477
  async close() {
345
478
  // While on the progress step we treat close as a cancel — terminate the job
346
- // and restore the backup — UNLESS the import already succeeded. On the
347
- // success path `startImport` lingers ~600ms on the completed bar with the
348
- // step still 'progress'; closing during that window must NOT restore (it
349
- // would undo a clean import). A successful close just closes the modal and
350
- // 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.
351
483
  if (this.currentStep === 'progress' && !this.importStatus.isSuccess) {
352
484
  var _this$_abortControlle2;
353
485
  this._cancelled = true;
@@ -371,6 +503,14 @@ class ImportState {
371
503
  }
372
504
  this.importModalState.close();
373
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
+ }
374
514
  async _restoreBackup() {
375
515
  const restoreFn = this.restoreFn;
376
516
  if (!restoreFn) {
@@ -541,51 +681,44 @@ class ImportState {
541
681
  }
542
682
  });
543
683
  }
544
- _showPartialSuccessToast() {
545
- var _this$showToast3;
546
- (_this$showToast3 = this.showToast) == null || _this$showToast3.call(this, {
547
- type: 'WARNING',
548
- message: this.translate('cairo.import.partialSuccessToast', {
549
- failed: String(this.result.failed),
550
- imported: String(this.result.succeeded)
551
- }),
552
- action: {
553
- text: this.translate('cairo.import.downloadFailedRows'),
554
- onClick: () => this._downloadFailedRows(),
555
- removeToastOnClick: true
556
- }
557
- });
558
- }
559
684
 
560
685
  /**
561
- * Builds a plain, re-importable CSV of the rows the server rejected the
562
- * original headers plus just the failed rows and hands it to `downloadCsv`.
563
- * The failed rows come from the job's movement logs (which row failed), mapped
564
- * 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.
565
691
  */
566
- async _downloadFailedRows() {
692
+ async downloadFailedRows() {
567
693
  var _this$downloadCsv;
568
694
  const jobId = this._jobId;
569
695
  if (!jobId) {
570
696
  return;
571
697
  }
572
- let indices;
698
+ let failures;
573
699
  try {
574
- indices = await this._fetchFailedRowIndices(jobId);
700
+ failures = await this._fetchFailedRows(jobId);
575
701
  } catch (error) {
576
702
  console.error('Fetching failed rows report failed', error);
577
703
  return;
578
704
  }
579
- const failedRows = indices.map(index => this.csvRows[index]).filter(row => row != null);
580
- 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) {
581
713
  return;
582
714
  }
583
- 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);
584
717
  (_this$downloadCsv = this.downloadCsv) == null || _this$downloadCsv.call(this, content, `failed-${this.fileName || 'import.csv'}`);
585
718
  }
586
719
 
587
- // Pages through the job's movement logs and returns the `csvRows` indices of
588
- // 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
589
722
  // (wix-data-hub: packages/wix-data-hub-executor-c/src/plugins/formats/csv/
590
723
  // csv-binary-decoder.ts) parses with the `csv-parse` library (`columns: true`)
591
724
  // and sets `sourceItemId = "row ${++index + 1}"`, incrementing once **per
@@ -594,8 +727,8 @@ class ImportState {
594
727
  // `csvRows[0]`, i.e. `csvRows[N - 2]`. Because `csv-parse` is RFC-4180,
595
728
  // quoted cells containing embedded newlines are a single record, so they do
596
729
  // NOT shift the numbering. Out-of-range indices are guarded below.
597
- async _fetchFailedRowIndices(jobId) {
598
- const indices = [];
730
+ async _fetchFailedRows(jobId) {
731
+ const failures = [];
599
732
  let cursor;
600
733
  do {
601
734
  var _pagingMetadata$curso;
@@ -613,12 +746,15 @@ class ImportState {
613
746
  const fileLine = Number((_exec = /\d+/.exec(log.sourceItemId)) == null ? void 0 : _exec[0]);
614
747
  const index = fileLine - 2;
615
748
  if (Number.isInteger(index) && index >= 0 && index < this.csvRows.length) {
616
- indices.push(index);
749
+ failures.push({
750
+ index,
751
+ reason: log.failure.description ?? ''
752
+ });
617
753
  }
618
754
  }
619
755
  cursor = (pagingMetadata == null ? void 0 : pagingMetadata.hasNext) && ((_pagingMetadata$curso = pagingMetadata.cursors) == null ? void 0 : _pagingMetadata$curso.next) || undefined;
620
756
  } while (cursor);
621
- return indices;
757
+ return failures;
622
758
  }
623
759
  setMapping(header, fieldId) {
624
760
  this.mappingByHeader = {
@@ -668,24 +804,56 @@ class ImportState {
668
804
  }
669
805
 
670
806
  /**
671
- * Marks which column's pending new field the field editor is currently
672
- * editing, so a later `applyFieldEdit` (the editor's `onSubmit`) knows where to
673
- * 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.
674
810
  */
675
811
  beginFieldEdit(header) {
676
- 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;
821
+ }
822
+
823
+ /**
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;
677
833
  }
678
834
 
679
835
  /**
680
- * `onSubmit` handler for the field editor: maps the edited field spec into the
681
- * column being edited (set via `beginFieldEdit`). No-op if nothing is being
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
682
838
  * edited.
683
839
  */
684
840
  applyFieldEdit(field) {
685
- if (this._editingHeader == null) {
841
+ if (this.editingHeader == null) {
686
842
  return;
687
843
  }
688
- this.setNewFieldConfig(this._editingHeader, (0, _fieldEdit.toFieldEdit)(field));
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) {
854
+ return;
855
+ }
856
+ this.setNewFieldConfig(this.editingHeader, (0, _fieldEdit.toDataExtensionFieldEdit)(draft));
689
857
  }
690
858
 
691
859
  /**
@@ -733,15 +901,15 @@ class ImportState {
733
901
  // The job ran server-side, so a failure arrives on a 200 (the poll
734
902
  // succeeded; the job didn't) with no HTTP error to carry the application
735
903
  // error. Build a synthetic, HTTP-error-shaped error (the error handler
736
- // reads the code from `response.data.details.applicationError`) and hand
737
- // the throwing operation to the consumer's `onJobError` so THEY wrap it
738
- // with their own `withErrorHandler` + error-code map — `job.error` is the
739
- // consumer's application error (their import business logic), so only they
740
- // can map its codes, and the map stays coupled to the `withErrorHandler`
741
- // call as everywhere else (e.g. `fetchData`). The map is attached to the
742
- // error object, which the progress step resolves via `getResolvedError`.
743
- // Without a handler the same error still throws, falling back to the
744
- // 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.
745
913
  const error = Object.assign(new Error(`ImportJob.${((_job$error = job.error) == null ? void 0 : _job$error.code) ?? 'failed'}`), {
746
914
  response: {
747
915
  data: {
@@ -751,11 +919,17 @@ class ImportState {
751
919
  }
752
920
  }
753
921
  });
754
- const run = () => Promise.reject(error);
755
- if (this.onJobError) {
756
- // The consumer wraps `run` with `withErrorHandler`; it re-rejects with
757
- // `error` (now carrying their map), which propagates from here.
758
- 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);
759
933
  }
760
934
  throw error;
761
935
  }
@@ -783,10 +957,16 @@ class ImportState {
783
957
  };
784
958
  }
785
959
  _applyJobProgress(job) {
960
+ var _progress$itemsSucces, _progress$itemsSucces2;
786
961
  const progress = job.progress;
787
962
  const succeeded = (progress == null ? void 0 : progress.itemsSuccessful) ?? 0;
788
963
  const failed = (progress == null ? void 0 : progress.itemsRejected) ?? 0;
789
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;
790
970
 
791
971
  // `totalCount` is fixed up front in `startImport` from the CSV row count
792
972
  // (one uploaded row per CSV row); polling only advances `processedCount`.
@@ -794,10 +974,15 @@ class ImportState {
794
974
  this.processedCount = Math.min(current, this.totalCount);
795
975
  this.result = {
796
976
  succeeded,
797
- failed
977
+ failed,
978
+ created,
979
+ updated
798
980
  };
799
981
  });
800
982
  }
983
+ _lingerOnCompletion() {
984
+ return new Promise(resolve => setTimeout(resolve, COMPLETION_LINGER_MS));
985
+ }
801
986
  async _runBackup() {
802
987
  const backup = this.backup;
803
988
  if (!backup) {
@@ -829,9 +1014,6 @@ class ImportState {
829
1014
  });
830
1015
  return false;
831
1016
  }
832
- _lingerOnCompletion() {
833
- return new Promise(resolve => setTimeout(resolve, COMPLETION_LINGER_MS));
834
- }
835
1017
 
836
1018
  /**
837
1019
  * Resolves every column to the final destination key used in the upload
@@ -842,18 +1024,21 @@ class ImportState {
842
1024
  */
843
1025
  async _resolveFieldMapping() {
844
1026
  const resolved = {};
845
- 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 = [];
846
1030
  // Seed with existing field ids/display names so derived keys and names never
847
1031
  // collide with them or with each other.
848
1032
  const usedKeys = new Set(this.fields.map(field => field.id).filter(id => !!id));
849
1033
  const usedNames = new Set(this.fields.map(field => field.header).filter(header => !!header));
1034
+ const canCreate = this.canCreateNewField;
850
1035
  for (const header of this.csvHeaders) {
851
1036
  const mapping = this.mappingByHeader[header];
852
1037
  if (mapping == null) {
853
1038
  resolved[header] = null;
854
1039
  } else if (mapping.kind === 'existing') {
855
1040
  resolved[header] = mapping.fieldId;
856
- } else if (this.updateSchema) {
1041
+ } else if (canCreate) {
857
1042
  var _mapping$displayName;
858
1043
  // The user's name override (from the field editor) seeds the unique
859
1044
  // name/key derivation; otherwise fall back to the column header.
@@ -864,29 +1049,79 @@ class ImportState {
864
1049
  } = uniqueFieldNameAndKey(baseName, usedNames, usedKeys);
865
1050
  usedKeys.add(key);
866
1051
  usedNames.add(displayName);
867
- newFields.push({
1052
+ pending.push({
868
1053
  key,
869
- type: mapping.fieldType,
870
1054
  displayName,
871
- ...(mapping.config && {
872
- config: mapping.config
873
- })
1055
+ mapping
874
1056
  });
875
1057
  resolved[header] = key;
876
1058
  } else {
877
- // "new" mapping without an `updateSchema` callback — nothing to map into.
1059
+ // "new" mapping with no create callback on this source — nothing to map into.
878
1060
  resolved[header] = null;
879
1061
  }
880
1062
  }
881
- if (newFields.length > 0 && this.updateSchema) {
882
- await this.updateSchema(newFields);
1063
+ if (pending.length > 0) {
1064
+ // Stash the returned fields map to merge into the table on success; kept off
1065
+ // the observable until then so columns don't update mid-import.
1066
+ this._pendingFieldsMap = await this._createPendingFields(pending);
883
1067
  }
884
1068
  return resolved;
885
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
+ }
886
1107
  async startImport() {
887
1108
  if (!this.hasMappings) {
888
1109
  return;
889
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
+ }
890
1125
  this._cancelled = false;
891
1126
  this._abortController = new AbortController();
892
1127
 
@@ -905,9 +1140,14 @@ class ImportState {
905
1140
  };
906
1141
  this.result = {
907
1142
  succeeded: 0,
908
- failed: 0
1143
+ failed: 0,
1144
+ created: 0,
1145
+ updated: 0
909
1146
  };
910
1147
  this._jobId = null;
1148
+ // Drop any fields map from a prior attempt so a no-new-fields "Try again"
1149
+ // can't merge stale fields.
1150
+ this._pendingFieldsMap = undefined;
911
1151
  });
912
1152
  const backupFailed = await this._runBackup();
913
1153
  if (backupFailed) {
@@ -940,6 +1180,12 @@ class ImportState {
940
1180
  ..._model.queryStatus.loading,
941
1181
  promise
942
1182
  };
1183
+
1184
+ // Assigned only on the success path below, so the post-import merge (after the
1185
+ // try/catch) fires only for a completed import — without re-checking status
1186
+ // there. Reading `_pendingFieldsMap` directly wouldn't do: it's set pre-upload,
1187
+ // so it stays truthy even when the job later fails.
1188
+ let createdFields;
943
1189
  try {
944
1190
  await promise;
945
1191
  if (this._cancelled) {
@@ -950,25 +1196,32 @@ class ImportState {
950
1196
  ..._model.queryStatus.success,
951
1197
  data: this.result
952
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;
953
1204
  });
954
1205
 
955
- // On a clean import, hold the completed (100%) progress bar on screen
956
- // briefly so the completion is perceptible even for very few rows. Skip
957
- // the wait when there are failed rows that path closes and surfaces a
958
- // toast instead.
959
- if (this.result.failed === 0) {
960
- await this._lingerOnCompletion();
961
- if (this._cancelled) {
962
- return;
963
- }
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;
964
1212
  }
1213
+
1214
+ // Success path only. `_pendingFieldsMap` holds this run's returned map — it's
1215
+ // cleared at the start of `startImport` (the stale-guard) and set by
1216
+ // `_resolveFieldMapping`; snapshotting here keeps the merge success-gated.
1217
+ createdFields = this._pendingFieldsMap;
965
1218
  (0, _mobx.runInAction)(() => {
966
- // Rows the server rejected are reported via a toast (the downloadable
967
- // failed-rows report is added by the failed-rows-report task).
968
- if (this.result.failed > 0) {
969
- this._showPartialSuccessToast();
970
- }
971
- 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';
972
1225
  this.collection.clearResultAndMoveToStart({
973
1226
  force: true
974
1227
  });
@@ -981,6 +1234,21 @@ class ImportState {
981
1234
  };
982
1235
  });
983
1236
  }
1237
+
1238
+ // Merge the created fields into the table so the new columns appear with no
1239
+ // manual reload (CAIRO-4400). Called imperatively here, outside the try/catch
1240
+ // above, so a merge failure can't flip a completed import into the error state
1241
+ // (whose "Try again" re-imports every row). `createdFields` is set only on the
1242
+ // success path and only when fields were created.
1243
+ if (createdFields) {
1244
+ try {
1245
+ var _this$config$mergeFie, _this$config;
1246
+ (_this$config$mergeFie = (_this$config = this.config).mergeFields) == null || _this$config$mergeFie.call(_this$config, createdFields);
1247
+ } catch (error) {
1248
+ // eslint-disable-next-line no-console
1249
+ console.error('ImportState config.mergeFields failed', error);
1250
+ }
1251
+ }
984
1252
  }
985
1253
  }
986
1254
  exports.ImportState = ImportState;