@wix/bex-core 2.332.0 → 2.336.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/dist/cjs/assets/locale/messages_ar.json +13 -2
  2. package/dist/cjs/assets/locale/messages_bg.json +13 -2
  3. package/dist/cjs/assets/locale/messages_ca.json +13 -2
  4. package/dist/cjs/assets/locale/messages_cs.json +11 -0
  5. package/dist/cjs/assets/locale/messages_da.json +11 -0
  6. package/dist/cjs/assets/locale/messages_de.json +11 -0
  7. package/dist/cjs/assets/locale/messages_el.json +13 -2
  8. package/dist/cjs/assets/locale/messages_en.json +13 -2
  9. package/dist/cjs/assets/locale/messages_es.json +11 -0
  10. package/dist/cjs/assets/locale/messages_fi.json +13 -2
  11. package/dist/cjs/assets/locale/messages_fr.json +11 -0
  12. package/dist/cjs/assets/locale/messages_he.json +13 -2
  13. package/dist/cjs/assets/locale/messages_hi.json +11 -0
  14. package/dist/cjs/assets/locale/messages_hu.json +13 -2
  15. package/dist/cjs/assets/locale/messages_id.json +11 -0
  16. package/dist/cjs/assets/locale/messages_it.json +11 -0
  17. package/dist/cjs/assets/locale/messages_ja.json +11 -0
  18. package/dist/cjs/assets/locale/messages_ko.json +11 -0
  19. package/dist/cjs/assets/locale/messages_lt.json +13 -2
  20. package/dist/cjs/assets/locale/messages_ms.json +13 -2
  21. package/dist/cjs/assets/locale/messages_nl.json +11 -0
  22. package/dist/cjs/assets/locale/messages_no.json +11 -0
  23. package/dist/cjs/assets/locale/messages_pl.json +11 -0
  24. package/dist/cjs/assets/locale/messages_pt.json +11 -0
  25. package/dist/cjs/assets/locale/messages_ro.json +13 -2
  26. package/dist/cjs/assets/locale/messages_ru.json +11 -0
  27. package/dist/cjs/assets/locale/messages_sk.json +13 -2
  28. package/dist/cjs/assets/locale/messages_sl.json +13 -2
  29. package/dist/cjs/assets/locale/messages_sv.json +11 -0
  30. package/dist/cjs/assets/locale/messages_th.json +11 -0
  31. package/dist/cjs/assets/locale/messages_tl.json +13 -2
  32. package/dist/cjs/assets/locale/messages_tr.json +11 -0
  33. package/dist/cjs/assets/locale/messages_uk.json +11 -0
  34. package/dist/cjs/assets/locale/messages_vi.json +11 -0
  35. package/dist/cjs/assets/locale/messages_zh.json +11 -0
  36. package/dist/cjs/assets/locale/version_file +1 -1
  37. package/dist/cjs/assets/visual-e2e-locale/messages_visuale2e.json +29 -2
  38. package/dist/cjs/hooks/index.js +6 -6
  39. package/dist/cjs/hooks/index.js.map +1 -1
  40. package/dist/cjs/hooks/useImport.js +4 -0
  41. package/dist/cjs/hooks/useImport.js.map +1 -1
  42. package/dist/cjs/hooks/useSchemaSource.js +11 -0
  43. package/dist/cjs/hooks/useSchemaSource.js.map +1 -0
  44. package/dist/cjs/index.js +4 -9
  45. package/dist/cjs/index.js.map +1 -1
  46. package/dist/cjs/services/translations.js.map +1 -1
  47. package/dist/cjs/state/CustomFieldModalState.js +12 -0
  48. package/dist/cjs/state/CustomFieldModalState.js.map +1 -1
  49. package/dist/cjs/state/ImportState/ImportState.js +415 -136
  50. package/dist/cjs/state/ImportState/ImportState.js.map +1 -1
  51. package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js +417 -82
  52. package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -1
  53. package/dist/cjs/state/ImportState/__tests__/buildImportConfig.spec.js +156 -0
  54. package/dist/cjs/state/ImportState/__tests__/buildImportConfig.spec.js.map +1 -0
  55. package/dist/cjs/state/ImportState/detectFieldType.js +41 -0
  56. package/dist/cjs/state/ImportState/detectFieldType.js.map +1 -1
  57. package/dist/cjs/state/ImportState/fieldEdit.js +49 -0
  58. package/dist/cjs/state/ImportState/fieldEdit.js.map +1 -1
  59. package/dist/cjs/state/SourceState.js +150 -0
  60. package/dist/cjs/state/SourceState.js.map +1 -0
  61. package/dist/cjs/state/index.js +7 -0
  62. package/dist/cjs/state/index.js.map +1 -1
  63. package/dist/cjs/testkit/createInMemorySchemaSource.js +190 -0
  64. package/dist/cjs/testkit/createInMemorySchemaSource.js.map +1 -0
  65. package/dist/cjs/testkit/hubJobImportMocks.js +18 -2
  66. package/dist/cjs/testkit/hubJobImportMocks.js.map +1 -1
  67. package/dist/cjs/testkit/index.js +6 -0
  68. package/dist/cjs/testkit/index.js.map +1 -1
  69. package/dist/cjs/types/Field.js.map +1 -1
  70. package/dist/cjs/types/Schema.js.map +1 -1
  71. package/dist/cjs/types/SchemaSource.js +31 -21
  72. package/dist/cjs/types/SchemaSource.js.map +1 -1
  73. package/dist/esm/assets/locale/messages_ar.json +13 -2
  74. package/dist/esm/assets/locale/messages_bg.json +13 -2
  75. package/dist/esm/assets/locale/messages_ca.json +13 -2
  76. package/dist/esm/assets/locale/messages_cs.json +11 -0
  77. package/dist/esm/assets/locale/messages_da.json +11 -0
  78. package/dist/esm/assets/locale/messages_de.json +11 -0
  79. package/dist/esm/assets/locale/messages_el.json +13 -2
  80. package/dist/esm/assets/locale/messages_en.json +13 -2
  81. package/dist/esm/assets/locale/messages_es.json +11 -0
  82. package/dist/esm/assets/locale/messages_fi.json +13 -2
  83. package/dist/esm/assets/locale/messages_fr.json +11 -0
  84. package/dist/esm/assets/locale/messages_he.json +13 -2
  85. package/dist/esm/assets/locale/messages_hi.json +11 -0
  86. package/dist/esm/assets/locale/messages_hu.json +13 -2
  87. package/dist/esm/assets/locale/messages_id.json +11 -0
  88. package/dist/esm/assets/locale/messages_it.json +11 -0
  89. package/dist/esm/assets/locale/messages_ja.json +11 -0
  90. package/dist/esm/assets/locale/messages_ko.json +11 -0
  91. package/dist/esm/assets/locale/messages_lt.json +13 -2
  92. package/dist/esm/assets/locale/messages_ms.json +13 -2
  93. package/dist/esm/assets/locale/messages_nl.json +11 -0
  94. package/dist/esm/assets/locale/messages_no.json +11 -0
  95. package/dist/esm/assets/locale/messages_pl.json +11 -0
  96. package/dist/esm/assets/locale/messages_pt.json +11 -0
  97. package/dist/esm/assets/locale/messages_ro.json +13 -2
  98. package/dist/esm/assets/locale/messages_ru.json +11 -0
  99. package/dist/esm/assets/locale/messages_sk.json +13 -2
  100. package/dist/esm/assets/locale/messages_sl.json +13 -2
  101. package/dist/esm/assets/locale/messages_sv.json +11 -0
  102. package/dist/esm/assets/locale/messages_th.json +11 -0
  103. package/dist/esm/assets/locale/messages_tl.json +13 -2
  104. package/dist/esm/assets/locale/messages_tr.json +11 -0
  105. package/dist/esm/assets/locale/messages_uk.json +11 -0
  106. package/dist/esm/assets/locale/messages_vi.json +11 -0
  107. package/dist/esm/assets/locale/messages_zh.json +11 -0
  108. package/dist/esm/assets/locale/version_file +1 -1
  109. package/dist/esm/assets/visual-e2e-locale/messages_visuale2e.json +29 -2
  110. package/dist/esm/hooks/index.js +1 -1
  111. package/dist/esm/hooks/index.js.map +1 -1
  112. package/dist/esm/hooks/useImport.js +3 -1
  113. package/dist/esm/hooks/useImport.js.map +1 -1
  114. package/dist/esm/hooks/useSchemaSource.js +7 -0
  115. package/dist/esm/hooks/useSchemaSource.js.map +1 -0
  116. package/dist/esm/index.js +1 -2
  117. package/dist/esm/index.js.map +1 -1
  118. package/dist/esm/state/CustomFieldModalState.js +8 -0
  119. package/dist/esm/state/CustomFieldModalState.js.map +1 -1
  120. package/dist/esm/state/ImportState/ImportState.js +306 -113
  121. package/dist/esm/state/ImportState/ImportState.js.map +1 -1
  122. package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js +297 -70
  123. package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -1
  124. package/dist/esm/state/ImportState/__tests__/buildImportConfig.spec.js +100 -0
  125. package/dist/esm/state/ImportState/__tests__/buildImportConfig.spec.js.map +1 -0
  126. package/dist/esm/state/ImportState/detectFieldType.js +34 -15
  127. package/dist/esm/state/ImportState/detectFieldType.js.map +1 -1
  128. package/dist/esm/state/ImportState/fieldEdit.js +44 -0
  129. package/dist/esm/state/ImportState/fieldEdit.js.map +1 -1
  130. package/dist/esm/state/SourceState.js +131 -0
  131. package/dist/esm/state/SourceState.js.map +1 -0
  132. package/dist/esm/state/index.js +1 -0
  133. package/dist/esm/state/index.js.map +1 -1
  134. package/dist/esm/testkit/createInMemorySchemaSource.js +141 -0
  135. package/dist/esm/testkit/createInMemorySchemaSource.js.map +1 -0
  136. package/dist/esm/testkit/hubJobImportMocks.js +17 -1
  137. package/dist/esm/testkit/hubJobImportMocks.js.map +1 -1
  138. package/dist/esm/testkit/index.js +1 -0
  139. package/dist/esm/testkit/index.js.map +1 -1
  140. package/dist/esm/types/SchemaSource.js +21 -14
  141. package/dist/esm/types/SchemaSource.js.map +1 -1
  142. package/dist/types/hooks/index.d.ts +1 -1
  143. package/dist/types/hooks/index.d.ts.map +1 -1
  144. package/dist/types/hooks/useImport.d.ts +9 -3
  145. package/dist/types/hooks/useImport.d.ts.map +1 -1
  146. package/dist/types/hooks/useSchemaSource.d.ts +4 -0
  147. package/dist/types/hooks/useSchemaSource.d.ts.map +1 -0
  148. package/dist/types/index.d.ts +2 -3
  149. package/dist/types/index.d.ts.map +1 -1
  150. package/dist/types/services/translations.d.ts +7 -1
  151. package/dist/types/services/translations.d.ts.map +1 -1
  152. package/dist/types/state/CustomFieldModalState.d.ts +9 -0
  153. package/dist/types/state/CustomFieldModalState.d.ts.map +1 -1
  154. package/dist/types/state/ImportState/ImportState.d.ts +208 -65
  155. package/dist/types/state/ImportState/ImportState.d.ts.map +1 -1
  156. package/dist/types/state/ImportState/__tests__/buildImportConfig.spec.d.ts +2 -0
  157. package/dist/types/state/ImportState/__tests__/buildImportConfig.spec.d.ts.map +1 -0
  158. package/dist/types/state/ImportState/detectFieldType.d.ts +5 -0
  159. package/dist/types/state/ImportState/detectFieldType.d.ts.map +1 -1
  160. package/dist/types/state/ImportState/fieldEdit.d.ts +6 -0
  161. package/dist/types/state/ImportState/fieldEdit.d.ts.map +1 -1
  162. package/dist/types/state/SourceState.d.ts +39 -0
  163. package/dist/types/state/SourceState.d.ts.map +1 -0
  164. package/dist/types/state/index.d.ts +1 -0
  165. package/dist/types/state/index.d.ts.map +1 -1
  166. package/dist/types/testkit/WixPatternsContainer.driver.d.ts +4 -0
  167. package/dist/types/testkit/WixPatternsContainer.driver.d.ts.map +1 -1
  168. package/dist/types/testkit/createInMemorySchemaSource.d.ts +43 -0
  169. package/dist/types/testkit/createInMemorySchemaSource.d.ts.map +1 -0
  170. package/dist/types/testkit/hubJobImportMocks.d.ts +11 -1
  171. package/dist/types/testkit/hubJobImportMocks.d.ts.map +1 -1
  172. package/dist/types/testkit/index.d.ts +1 -0
  173. package/dist/types/testkit/index.d.ts.map +1 -1
  174. package/dist/types/types/Field.d.ts +3 -0
  175. package/dist/types/types/Field.d.ts.map +1 -1
  176. package/dist/types/types/Schema.d.ts +10 -6
  177. package/dist/types/types/Schema.d.ts.map +1 -1
  178. package/dist/types/types/SchemaSource.d.ts +30 -13
  179. package/dist/types/types/SchemaSource.d.ts.map +1 -1
  180. package/package.json +4 -4
  181. package/dist/cjs/hooks/useImportConfig.js +0 -12
  182. package/dist/cjs/hooks/useImportConfig.js.map +0 -1
  183. package/dist/esm/hooks/useImportConfig.js +0 -6
  184. package/dist/esm/hooks/useImportConfig.js.map +0 -1
  185. package/dist/types/hooks/useImportConfig.d.ts +0 -5
  186. package/dist/types/hooks/useImportConfig.d.ts.map +0 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
4
  exports.__esModule = true;
5
- exports.ImportState = void 0;
5
+ exports.buildImportConfig = exports.ImportState = void 0;
6
6
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
7
7
  var _mobx = require("mobx");
8
8
  var _model = require("../../model");
@@ -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 builds its own field shape from them, so no
103
+ * 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,117 @@ 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
+
131
+ /**
132
+ * The effective import config. Satisfied both by a plain object (a fully
133
+ * hand-built config) and by the object `buildImportConfig` assembles, whose
134
+ * members are getters read on demand — so the async schema load needn't resolve
135
+ * before `ImportState` is built. `ImportState` holds it by reference (never
136
+ * spreads it) so those getters stay live.
137
+ */
138
+
94
139
  /**
95
- * 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.
140
+ * The inputs a consumer passes to `<ImportButton config={...} />`. Everything the
141
+ * table's source provides the collection, its fields, create-new-field and
142
+ * merge-back is read live off the `fieldsSource`, so only what isn't derivable
143
+ * lives here. Omit `destination` to import into the table's own collection (Wix
144
+ * Data); set it to write to a platformized target instead.
100
145
  */
101
146
 
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.
147
+ // Builds the Field to create from a pending new column: import decides the key,
148
+ // and the detected NewFieldType maps to the schema's PatternsFieldType. Import
149
+ // only ever creates non-reference fields, so the cast is safe.
150
+ const toField = def => {
151
+ var _def$config, _def$config2, _def$config3;
152
+ return {
153
+ id: def.key,
154
+ displayName: def.displayName,
155
+ type: (0, _detectFieldType.toPatternsFieldType)(def.type),
156
+ ...(((_def$config = def.config) == null ? void 0 : _def$config.validation) && {
157
+ validation: def.config.validation
158
+ }),
159
+ ...(((_def$config2 = def.config) == null ? void 0 : _def$config2.isPii) && {
160
+ isPii: true
161
+ }),
162
+ ...(((_def$config3 = def.config) == null ? void 0 : _def$config3.helpText) && {
163
+ helpText: def.config.helpText
164
+ })
165
+ };
166
+ };
167
+
168
+ /**
169
+ * Assembles a `ResolvedImportConfig` from the table's live `fieldsSource` and the
170
+ * consumer `inputs`. The schema-derived members are getters so they read the
171
+ * source live — fields import creates and fields added through the
172
+ * field-management panel show up immediately. Empty before a source attaches;
173
+ * nothing imports until then.
174
+ *
175
+ * Exported for tests.
176
+ */
177
+ const buildImportConfig = (fieldsSource, collection, inputs = {}) => {
178
+ const {
179
+ destination,
180
+ fields = [],
181
+ backup,
182
+ idFieldKey,
183
+ jobApplicationErrorMap
184
+ } = inputs;
185
+ return {
186
+ get fields() {
187
+ const seen = new Set(fields.map(f => f.id));
188
+ const live = ((fieldsSource == null ? void 0 : fieldsSource.fields) ?? []).filter(f => !seen.has(f.id)).map(f => {
189
+ var _f$validation;
190
+ return {
191
+ id: f.id,
192
+ header: f.displayName,
193
+ required: (_f$validation = f.validation) == null ? void 0 : _f$validation.required
194
+ };
195
+ });
196
+ return [...fields, ...live];
197
+ },
198
+ get dataSource() {
199
+ var _fieldsSource$fieldMa, _fieldsSource$fieldAc;
200
+ if (destination) {
201
+ return {
202
+ destination
203
+ };
204
+ }
205
+ const add = fieldsSource != null && (_fieldsSource$fieldMa = fieldsSource.fieldManagement) != null && _fieldsSource$fieldMa.canAddFields ? (_fieldsSource$fieldAc = fieldsSource.fieldActions) == null ? void 0 : _fieldsSource$fieldAc.addFields : undefined;
206
+ return {
207
+ collectionId: collection.queryName,
208
+ ...(add && {
209
+ updateSchema: defs => add(defs.map(toField))
210
+ })
211
+ };
212
+ },
213
+ mergeFields: f => fieldsSource == null || fieldsSource.mergeFields == null ? void 0 : fieldsSource.mergeFields(f),
214
+ ...(backup && {
215
+ backup
216
+ }),
217
+ ...(idFieldKey && {
218
+ idFieldKey
219
+ }),
220
+ ...(jobApplicationErrorMap && {
221
+ jobApplicationErrorMap
222
+ })
223
+ };
224
+ };
225
+ exports.buildImportConfig = buildImportConfig;
226
+ // How long to hold the completed (100%) progress bar on screen before moving to
227
+ // the summary step (CAIRO-4476). The job reports counts only when it finishes,
228
+ // so the bar sits at 0 for the whole import and would otherwise flip straight to
229
+ // the summary without the user ever seeing it reach completion.
106
230
  const COMPLETION_LINGER_MS = 600;
107
231
 
108
232
  // The Wix Data collection's id field key. A CSV column mapped to this field lets
@@ -185,7 +309,9 @@ class ImportState {
185
309
  (0, _defineProperty2.default)(this, "totalCount", 0);
186
310
  (0, _defineProperty2.default)(this, "result", {
187
311
  succeeded: 0,
188
- failed: 0
312
+ failed: 0,
313
+ created: 0,
314
+ updated: 0
189
315
  });
190
316
  (0, _defineProperty2.default)(this, "isBackingUp", false);
191
317
  (0, _defineProperty2.default)(this, "isRestoring", false);
@@ -195,16 +321,19 @@ class ImportState {
195
321
  // Id of the running data-movement job, set once submitted — used to cancel it
196
322
  // (`terminateJob`) if the user closes mid-import.
197
323
  (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);
324
+ // The column whose pending new field the field editor is currently editing, or
325
+ // `null` when the editor is closed. Observable so the source's field-editor
326
+ // component (rendered via `renderFieldEditor`) opens/prefills itself when this
327
+ // is set and closes when it clears — the editor is decoupled from the layout.
328
+ (0, _defineProperty2.default)(this, "editingHeader", null);
202
329
  // Holds `updateSchema`'s returned fields map between field creation (pre-upload)
203
330
  // and success. Kept private/non-observable so it can't update columns mid-import;
204
331
  // it's promoted to the observable `createdFieldsMap` only on success.
205
332
  (0, _defineProperty2.default)(this, "_pendingFieldsMap", void 0);
206
- this.config = props.config;
207
333
  this.collection = props.collection;
334
+ // A pre-built config wins (hand-built / tests); otherwise assemble it from
335
+ // the live source + inputs.
336
+ this.config = props.config ?? buildImportConfig(props.fieldsSource, props.collection, props.importInputs);
208
337
  this.importModalState = props.importModalState;
209
338
  this.translate = props.translate;
210
339
  this.errorHandler = props.errorHandler;
@@ -233,7 +362,12 @@ class ImportState {
233
362
  hasMappings: _mobx.computed,
234
363
  hasNoMappedData: _mobx.computed,
235
364
  selectedFieldIds: _mobx.computed,
365
+ missingRequiredFields: _mobx.computed,
366
+ hasMissingRequiredFields: _mobx.computed,
236
367
  idMappedHeader: _mobx.computed,
368
+ editingHeader: _mobx.observable.ref,
369
+ editingMapping: _mobx.computed,
370
+ endFieldEdit: _mobx.action.bound,
237
371
  setWritePolicy: _mobx.action.bound,
238
372
  onUploadContinue: _mobx.action.bound,
239
373
  goToUpload: _mobx.action.bound,
@@ -242,7 +376,10 @@ class ImportState {
242
376
  setNewFieldConfig: _mobx.action.bound,
243
377
  beginFieldEdit: _mobx.action.bound,
244
378
  applyFieldEdit: _mobx.action.bound,
379
+ applyDataExtensionFieldEdit: _mobx.action.bound,
245
380
  close: _mobx.action.bound,
381
+ done: _mobx.action.bound,
382
+ downloadFailedRows: _mobx.action.bound,
246
383
  onFileSelected: _mobx.action.bound,
247
384
  startImport: _mobx.action.bound,
248
385
  reset: _mobx.action.bound
@@ -259,19 +396,36 @@ class ImportState {
259
396
 
260
397
  /** Wix Data collection the import writes into (unset for platformized). */
261
398
  get collectionId() {
262
- return this.config.collectionId;
399
+ const ds = this.config.dataSource;
400
+ return 'collectionId' in ds ? ds.collectionId : undefined;
263
401
  }
264
402
  get destination() {
265
- return this.config.destination;
403
+ const ds = this.config.dataSource;
404
+ return 'destination' in ds ? ds.destination : undefined;
266
405
  }
267
406
  get backup() {
268
407
  return this.config.backup;
269
408
  }
409
+
410
+ /** CMS create-fields callback, present only on the `collectionId` branch. */
270
411
  get updateSchema() {
271
- return this.config.updateSchema;
412
+ const ds = this.config.dataSource;
413
+ return 'collectionId' in ds ? ds.updateSchema : undefined;
272
414
  }
273
- get onJobError() {
274
- return this.config.onJobError;
415
+
416
+ /** DataExtension create-fields callback, present only on the `destination` branch. */
417
+ get createDataExtensionFields() {
418
+ const ds = this.config.dataSource;
419
+ return 'destination' in ds ? ds.createFields : undefined;
420
+ }
421
+
422
+ /** DataExtension field-management for the editor, on the `destination` branch. */
423
+ get dataExtensionFieldManagement() {
424
+ const ds = this.config.dataSource;
425
+ return 'destination' in ds ? ds.fieldManagement : undefined;
426
+ }
427
+ get jobApplicationErrorMap() {
428
+ return this.config.jobApplicationErrorMap;
275
429
  }
276
430
 
277
431
  // The collection's id-field key; a schema-backed config may report a non-`_id`
@@ -320,6 +474,27 @@ class ImportState {
320
474
  return new Set(Object.values(this.mappingByHeader).filter(m => (m == null ? void 0 : m.kind) === 'existing').map(m => m.fieldId));
321
475
  }
322
476
 
477
+ /**
478
+ * Required destination fields that no CSV column is mapped to (CAIRO-4452).
479
+ * A required field is an existing collection field, so only an `existing`
480
+ * mapping can satisfy it — a "create new field" column can't. Covers both
481
+ * ways a required field ends up unmapped: the CSV has no matching column at
482
+ * all, or the user set its column to "don't import". Drives the footer
483
+ * indicator and blocks `startImport`.
484
+ */
485
+ get missingRequiredFields() {
486
+ return this.fields.filter(field => field.required && field.id != null && !this.selectedFieldIds.has(field.id));
487
+ }
488
+ get hasMissingRequiredFields() {
489
+ return this.missingRequiredFields.length > 0;
490
+ }
491
+
492
+ // Comma-separated display names of the missing required fields, for the block
493
+ // toast. Falls back to the field id when a field carries no header.
494
+ _missingRequiredFieldNames() {
495
+ return this.missingRequiredFields.map(field => field.header || field.id || '').filter(Boolean).join(', ');
496
+ }
497
+
323
498
  /**
324
499
  * The CSV header currently mapped to the collection's `_id` field, or `null`
325
500
  * when no column targets it. Drives both the "When items share the same ID"
@@ -337,12 +512,13 @@ class ImportState {
337
512
 
338
513
  /**
339
514
  * 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`).
515
+ * The opt-in is the presence of the active `dataSource` branch's create
516
+ * callback `updateSchema` (CMS) or `createFields` (DataExtension). A source
517
+ * that can't create fields (e.g. no create permission) supplies neither, so
518
+ * columns fall back to "don't import" (see `getDefaultMapping`).
343
519
  */
344
520
  get canCreateNewField() {
345
- return this.config.canAddFields ?? this.config.updateSchema != null;
521
+ return this.updateSchema != null || this.createDataExtensionFields != null;
346
522
  }
347
523
 
348
524
  /**
@@ -375,7 +551,9 @@ class ImportState {
375
551
  this.totalCount = 0;
376
552
  this.result = {
377
553
  succeeded: 0,
378
- failed: 0
554
+ failed: 0,
555
+ created: 0,
556
+ updated: 0
379
557
  };
380
558
  this._pendingFieldsMap = undefined;
381
559
  this.isBackingUp = false;
@@ -386,11 +564,10 @@ class ImportState {
386
564
  }
387
565
  async close() {
388
566
  // 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.
567
+ // and restore the backup — UNLESS the import already succeeded. On success
568
+ // `startImport` moves straight to the 'summary' step, so a close from there
569
+ // just closes the modal (the summary's own `done` is the normal exit); this
570
+ // guard covers only closing mid-import.
394
571
  if (this.currentStep === 'progress' && !this.importStatus.isSuccess) {
395
572
  var _this$_abortControlle2;
396
573
  this._cancelled = true;
@@ -414,6 +591,14 @@ class ImportState {
414
591
  }
415
592
  this.importModalState.close();
416
593
  }
594
+
595
+ /**
596
+ * Closes the summary step at the end of a completed import. The collection was
597
+ * already refreshed when the summary opened, so this only dismisses the modal.
598
+ */
599
+ done() {
600
+ this.importModalState.close();
601
+ }
417
602
  async _restoreBackup() {
418
603
  const restoreFn = this.restoreFn;
419
604
  if (!restoreFn) {
@@ -584,51 +769,44 @@ class ImportState {
584
769
  }
585
770
  });
586
771
  }
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
772
 
603
773
  /**
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.
774
+ * Builds a re-importable CSV of the rows the server rejected and hands it to
775
+ * `downloadCsv`. Each failed row is prefixed with a leading "why it failed"
776
+ * column holding the server's error message, so the user can see and fix each
777
+ * reason. The failed rows (and their reasons) come from the job's movement
778
+ * logs, mapped back to the parsed `csvRows` we still hold in memory.
608
779
  */
609
- async _downloadFailedRows() {
780
+ async downloadFailedRows() {
610
781
  var _this$downloadCsv;
611
782
  const jobId = this._jobId;
612
783
  if (!jobId) {
613
784
  return;
614
785
  }
615
- let indices;
786
+ let failures;
616
787
  try {
617
- indices = await this._fetchFailedRowIndices(jobId);
788
+ failures = await this._fetchFailedRows(jobId);
618
789
  } catch (error) {
619
790
  console.error('Fetching failed rows report failed', error);
620
791
  return;
621
792
  }
622
- const failedRows = indices.map(index => this.csvRows[index]).filter(row => row != null);
623
- if (failedRows.length === 0) {
793
+ const rows = failures.map(({
794
+ index,
795
+ reason
796
+ }) => {
797
+ const row = this.csvRows[index];
798
+ return row != null ? [reason, ...row] : null;
799
+ }).filter(row => row != null);
800
+ if (rows.length === 0) {
624
801
  return;
625
802
  }
626
- const content = (0, _parseCsv.unparseCsv)(this.csvHeaders, failedRows);
803
+ const headers = [this.translate('cairo.import.failureReasonColumn'), ...this.csvHeaders];
804
+ const content = (0, _parseCsv.unparseCsv)(headers, rows);
627
805
  (_this$downloadCsv = this.downloadCsv) == null || _this$downloadCsv.call(this, content, `failed-${this.fileName || 'import.csv'}`);
628
806
  }
629
807
 
630
- // Pages through the job's movement logs and returns the `csvRows` indices of
631
- // the rows that failed. data-hub's CSV decoder
808
+ // Pages through the job's movement logs and returns, for each row that failed,
809
+ // its `csvRows` index and the server's failure reason. data-hub's CSV decoder
632
810
  // (wix-data-hub: packages/wix-data-hub-executor-c/src/plugins/formats/csv/
633
811
  // csv-binary-decoder.ts) parses with the `csv-parse` library (`columns: true`)
634
812
  // and sets `sourceItemId = "row ${++index + 1}"`, incrementing once **per
@@ -637,8 +815,8 @@ class ImportState {
637
815
  // `csvRows[0]`, i.e. `csvRows[N - 2]`. Because `csv-parse` is RFC-4180,
638
816
  // quoted cells containing embedded newlines are a single record, so they do
639
817
  // NOT shift the numbering. Out-of-range indices are guarded below.
640
- async _fetchFailedRowIndices(jobId) {
641
- const indices = [];
818
+ async _fetchFailedRows(jobId) {
819
+ const failures = [];
642
820
  let cursor;
643
821
  do {
644
822
  var _pagingMetadata$curso;
@@ -656,12 +834,15 @@ class ImportState {
656
834
  const fileLine = Number((_exec = /\d+/.exec(log.sourceItemId)) == null ? void 0 : _exec[0]);
657
835
  const index = fileLine - 2;
658
836
  if (Number.isInteger(index) && index >= 0 && index < this.csvRows.length) {
659
- indices.push(index);
837
+ failures.push({
838
+ index,
839
+ reason: log.failure.description ?? ''
840
+ });
660
841
  }
661
842
  }
662
843
  cursor = (pagingMetadata == null ? void 0 : pagingMetadata.hasNext) && ((_pagingMetadata$curso = pagingMetadata.cursors) == null ? void 0 : _pagingMetadata$curso.next) || undefined;
663
844
  } while (cursor);
664
- return indices;
845
+ return failures;
665
846
  }
666
847
  setMapping(header, fieldId) {
667
848
  this.mappingByHeader = {
@@ -711,24 +892,56 @@ class ImportState {
711
892
  }
712
893
 
713
894
  /**
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.
895
+ * Opens the field editor for a column's pending new field: sets
896
+ * `editingHeader`, which the source's editor component watches to open and
897
+ * prefill itself. No-op if the column isn't mapped to a new field.
717
898
  */
718
899
  beginFieldEdit(header) {
719
- this._editingHeader = header;
900
+ var _this$mappingByHeader;
901
+ if (((_this$mappingByHeader = this.mappingByHeader[header]) == null ? void 0 : _this$mappingByHeader.kind) === 'new') {
902
+ this.editingHeader = header;
903
+ }
904
+ }
905
+
906
+ /** Closes the field editor (clears `editingHeader`). */
907
+ endFieldEdit() {
908
+ this.editingHeader = null;
720
909
  }
721
910
 
722
911
  /**
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
912
+ * The `new` mapping currently being edited, or `null` when the editor is
913
+ * closed. The editor component reads this to prefill its form.
914
+ */
915
+ get editingMapping() {
916
+ if (this.editingHeader == null) {
917
+ return null;
918
+ }
919
+ const mapping = this.mappingByHeader[this.editingHeader];
920
+ return (mapping == null ? void 0 : mapping.kind) === 'new' ? mapping : null;
921
+ }
922
+
923
+ /**
924
+ * `onSubmit` handler for the CMS field editor: maps the edited field spec into
925
+ * the column being edited (set via `beginFieldEdit`). No-op if nothing is being
725
926
  * edited.
726
927
  */
727
928
  applyFieldEdit(field) {
728
- if (this._editingHeader == null) {
929
+ if (this.editingHeader == null) {
930
+ return;
931
+ }
932
+ this.setNewFieldConfig(this.editingHeader, (0, _fieldEdit.toFieldEdit)(field));
933
+ }
934
+
935
+ /**
936
+ * `onSubmit` handler for the DataExtension field editor: maps the edited
937
+ * DataExtension draft (which carries per-audience permissions) into the column
938
+ * being edited. The `destination`-branch counterpart to `applyFieldEdit`.
939
+ */
940
+ applyDataExtensionFieldEdit(draft) {
941
+ if (this.editingHeader == null) {
729
942
  return;
730
943
  }
731
- this.setNewFieldConfig(this._editingHeader, (0, _fieldEdit.toFieldEdit)(field));
944
+ this.setNewFieldConfig(this.editingHeader, (0, _fieldEdit.toDataExtensionFieldEdit)(draft));
732
945
  }
733
946
 
734
947
  /**
@@ -776,15 +989,15 @@ class ImportState {
776
989
  // The job ran server-side, so a failure arrives on a 200 (the poll
777
990
  // succeeded; the job didn't) with no HTTP error to carry the application
778
991
  // 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.
992
+ // reads the code from `response.data.details.applicationError`), then
993
+ // resolve it through the consumer's per-code map via `withErrorHandler`
994
+ // the same way as `fetchData` etc. `job.error` is the consumer's
995
+ // application error (their import business logic), so only they can map
996
+ // its codes; they supply just the code→message map and we do the
997
+ // wrapping. `withErrorHandler` attaches the map to the error and
998
+ // re-rejects it, so the progress step resolves a consumer-specific
999
+ // message via `getResolvedError`. Without a map the same error still
1000
+ // throws, falling back to the generic message.
788
1001
  const error = Object.assign(new Error(`ImportJob.${((_job$error = job.error) == null ? void 0 : _job$error.code) ?? 'failed'}`), {
789
1002
  response: {
790
1003
  data: {
@@ -794,11 +1007,17 @@ class ImportState {
794
1007
  }
795
1008
  }
796
1009
  });
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);
1010
+ const jobApplicationErrorMap = this.jobApplicationErrorMap;
1011
+ if (jobApplicationErrorMap) {
1012
+ // Adapt the consumer's code→message map into the error handler's
1013
+ // `errorCodesMap`: each entry, when its code matches the job's
1014
+ // application error, resolves with that error. `withErrorHandler`
1015
+ // attaches the resolved message to `error` and re-rejects it, so the
1016
+ // progress step surfaces it via `getResolvedError` — the consumer only
1017
+ // supplies the map, not the wrapping.
1018
+ const appError = job.error ?? {};
1019
+ const errorCodesMap = Object.fromEntries(Object.entries(jobApplicationErrorMap).map(([code, resolve]) => [code, () => resolve(appError)]));
1020
+ await this.errorHandler.withErrorHandler(() => Promise.reject(error), errorCodesMap);
802
1021
  }
803
1022
  throw error;
804
1023
  }
@@ -826,10 +1045,16 @@ class ImportState {
826
1045
  };
827
1046
  }
828
1047
  _applyJobProgress(job) {
1048
+ var _progress$itemsSucces, _progress$itemsSucces2;
829
1049
  const progress = job.progress;
830
1050
  const succeeded = (progress == null ? void 0 : progress.itemsSuccessful) ?? 0;
831
1051
  const failed = (progress == null ? void 0 : progress.itemsRejected) ?? 0;
832
1052
  const current = (progress == null ? void 0 : progress.current) ?? succeeded + failed;
1053
+ // The server splits successful items into created (new) and updated
1054
+ // (existing) — the summary step reports each. Absent on older jobs, in
1055
+ // which case both stay 0 and only the total `succeeded` is shown.
1056
+ const created = (progress == null || (_progress$itemsSucces = progress.itemsSuccessfulInfo) == null ? void 0 : _progress$itemsSucces.itemsCreated) ?? 0;
1057
+ const updated = (progress == null || (_progress$itemsSucces2 = progress.itemsSuccessfulInfo) == null ? void 0 : _progress$itemsSucces2.itemsUpdated) ?? 0;
833
1058
 
834
1059
  // `totalCount` is fixed up front in `startImport` from the CSV row count
835
1060
  // (one uploaded row per CSV row); polling only advances `processedCount`.
@@ -837,10 +1062,15 @@ class ImportState {
837
1062
  this.processedCount = Math.min(current, this.totalCount);
838
1063
  this.result = {
839
1064
  succeeded,
840
- failed
1065
+ failed,
1066
+ created,
1067
+ updated
841
1068
  };
842
1069
  });
843
1070
  }
1071
+ _lingerOnCompletion() {
1072
+ return new Promise(resolve => setTimeout(resolve, COMPLETION_LINGER_MS));
1073
+ }
844
1074
  async _runBackup() {
845
1075
  const backup = this.backup;
846
1076
  if (!backup) {
@@ -872,9 +1102,6 @@ class ImportState {
872
1102
  });
873
1103
  return false;
874
1104
  }
875
- _lingerOnCompletion() {
876
- return new Promise(resolve => setTimeout(resolve, COMPLETION_LINGER_MS));
877
- }
878
1105
 
879
1106
  /**
880
1107
  * Resolves every column to the final destination key used in the upload
@@ -885,18 +1112,21 @@ class ImportState {
885
1112
  */
886
1113
  async _resolveFieldMapping() {
887
1114
  const resolved = {};
888
- const newFields = [];
1115
+ // Pending new columns with their deduped key + display name. Source-neutral;
1116
+ // the per-source field def is built from these in `_createPendingFields`.
1117
+ const pending = [];
889
1118
  // Seed with existing field ids/display names so derived keys and names never
890
1119
  // collide with them or with each other.
891
1120
  const usedKeys = new Set(this.fields.map(field => field.id).filter(id => !!id));
892
1121
  const usedNames = new Set(this.fields.map(field => field.header).filter(header => !!header));
1122
+ const canCreate = this.canCreateNewField;
893
1123
  for (const header of this.csvHeaders) {
894
1124
  const mapping = this.mappingByHeader[header];
895
1125
  if (mapping == null) {
896
1126
  resolved[header] = null;
897
1127
  } else if (mapping.kind === 'existing') {
898
1128
  resolved[header] = mapping.fieldId;
899
- } else if (this.updateSchema) {
1129
+ } else if (canCreate) {
900
1130
  var _mapping$displayName;
901
1131
  // The user's name override (from the field editor) seeds the unique
902
1132
  // name/key derivation; otherwise fall back to the column header.
@@ -907,34 +1137,79 @@ class ImportState {
907
1137
  } = uniqueFieldNameAndKey(baseName, usedNames, usedKeys);
908
1138
  usedKeys.add(key);
909
1139
  usedNames.add(displayName);
910
- newFields.push({
1140
+ pending.push({
911
1141
  key,
912
- type: mapping.fieldType,
913
1142
  displayName,
914
- ...(mapping.config && {
915
- config: mapping.config
916
- })
1143
+ mapping
917
1144
  });
918
1145
  resolved[header] = key;
919
1146
  } else {
920
- // "new" mapping without an `updateSchema` callback — nothing to map into.
1147
+ // "new" mapping with no create callback on this source — nothing to map into.
921
1148
  resolved[header] = null;
922
1149
  }
923
1150
  }
924
- if (newFields.length > 0 && this.updateSchema) {
1151
+ if (pending.length > 0) {
925
1152
  // 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;
1153
+ // the observable until then so columns don't update mid-import.
1154
+ this._pendingFieldsMap = await this._createPendingFields(pending);
931
1155
  }
932
1156
  return resolved;
933
1157
  }
1158
+
1159
+ /**
1160
+ * Creates the pending new fields via the active `dataSource` branch's callback
1161
+ * — `updateSchema` (CMS) or `createFields` (DataExtension) — building the
1162
+ * source-specific field def from the shared, already-deduped pending columns.
1163
+ * The callback may throw; `startImport` surfaces that as the error state.
1164
+ */
1165
+ async _createPendingFields(pending) {
1166
+ const updateSchema = this.updateSchema;
1167
+ const createDataExtensionFields = this.createDataExtensionFields;
1168
+ let updated;
1169
+ if (updateSchema) {
1170
+ updated = await updateSchema(pending.map(({
1171
+ key,
1172
+ displayName,
1173
+ mapping
1174
+ }) => ({
1175
+ key,
1176
+ type: mapping.fieldType,
1177
+ displayName,
1178
+ ...(mapping.config && {
1179
+ config: mapping.config
1180
+ })
1181
+ })));
1182
+ } else if (createDataExtensionFields) {
1183
+ // The DataExtension source builds its own field shape from the neutral
1184
+ // pending columns — no DataExtension-specific def-building in core.
1185
+ updated = await createDataExtensionFields(pending);
1186
+ } else {
1187
+ return undefined;
1188
+ }
1189
+
1190
+ // Guard the empty map: SchemaState.mergeFields is a full replace, so merging
1191
+ // `{}` would wipe every column — treat a missing/empty return as "nothing to
1192
+ // merge".
1193
+ return updated && Object.keys(updated).length > 0 ? updated : undefined;
1194
+ }
934
1195
  async startImport() {
935
1196
  if (!this.hasMappings) {
936
1197
  return;
937
1198
  }
1199
+
1200
+ // Block import while any required destination field is unmapped and tell the
1201
+ // user which (CAIRO-4452). The footer already flags this; the Import button
1202
+ // stays enabled, so this is where the click is stopped.
1203
+ if (this.hasMissingRequiredFields) {
1204
+ var _this$showToast3;
1205
+ (_this$showToast3 = this.showToast) == null || _this$showToast3.call(this, {
1206
+ type: 'ERROR',
1207
+ message: this.translate('cairo.import.missingRequiredMessage', {
1208
+ fields: this._missingRequiredFieldNames()
1209
+ })
1210
+ });
1211
+ return;
1212
+ }
938
1213
  this._cancelled = false;
939
1214
  this._abortController = new AbortController();
940
1215
 
@@ -953,7 +1228,9 @@ class ImportState {
953
1228
  };
954
1229
  this.result = {
955
1230
  succeeded: 0,
956
- failed: 0
1231
+ failed: 0,
1232
+ created: 0,
1233
+ updated: 0
957
1234
  };
958
1235
  this._jobId = null;
959
1236
  // Drop any fields map from a prior attempt so a no-new-fields "Try again"
@@ -1007,17 +1284,19 @@ class ImportState {
1007
1284
  ..._model.queryStatus.success,
1008
1285
  data: this.result
1009
1286
  };
1287
+ // The job reports counts only when it finishes, so the bar sat at 0 for
1288
+ // the whole import (CAIRO-4476). Snap it to 100% and hold it (below)
1289
+ // before the summary step replaces the progress step, so the user sees
1290
+ // the import reach completion.
1291
+ this.processedCount = this.totalCount;
1010
1292
  });
1011
1293
 
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
- }
1294
+ // Keep the completed bar on screen briefly before moving on. `importStatus`
1295
+ // is already success, so a close during this window just dismisses the
1296
+ // modal (the `close()` guard doesn't treat it as a cancel).
1297
+ await this._lingerOnCompletion();
1298
+ if (this._cancelled) {
1299
+ return;
1021
1300
  }
1022
1301
 
1023
1302
  // Success path only. `_pendingFieldsMap` holds this run's returned map — it's
@@ -1025,12 +1304,12 @@ class ImportState {
1025
1304
  // `_resolveFieldMapping`; snapshotting here keeps the merge success-gated.
1026
1305
  createdFields = this._pendingFieldsMap;
1027
1306
  (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();
1307
+ // Import finished: show the summary step (created / updated / failed
1308
+ // counts, plus a "Download CSV" of the failed rows when any failed).
1309
+ // The modal stays open; the user dismisses it from the summary via
1310
+ // `done`. Refresh the collection now so the table behind the modal is
1311
+ // up to date by the time it closes.
1312
+ this.currentStep = 'summary';
1034
1313
  this.collection.clearResultAndMoveToStart({
1035
1314
  force: true
1036
1315
  });