@wix/bex-core 2.327.0 → 2.329.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 (184) hide show
  1. package/dist/cjs/assets/locale/messages_ar.json +12 -0
  2. package/dist/cjs/assets/locale/messages_bg.json +12 -0
  3. package/dist/cjs/assets/locale/messages_ca.json +12 -0
  4. package/dist/cjs/assets/locale/messages_cs.json +12 -0
  5. package/dist/cjs/assets/locale/messages_da.json +17 -5
  6. package/dist/cjs/assets/locale/messages_de.json +12 -0
  7. package/dist/cjs/assets/locale/messages_el.json +12 -0
  8. package/dist/cjs/assets/locale/messages_en.json +12 -0
  9. package/dist/cjs/assets/locale/messages_es.json +16 -4
  10. package/dist/cjs/assets/locale/messages_fi.json +12 -0
  11. package/dist/cjs/assets/locale/messages_fr.json +12 -0
  12. package/dist/cjs/assets/locale/messages_he.json +12 -0
  13. package/dist/cjs/assets/locale/messages_hi.json +17 -5
  14. package/dist/cjs/assets/locale/messages_hu.json +12 -0
  15. package/dist/cjs/assets/locale/messages_id.json +17 -5
  16. package/dist/cjs/assets/locale/messages_it.json +17 -5
  17. package/dist/cjs/assets/locale/messages_ja.json +12 -0
  18. package/dist/cjs/assets/locale/messages_ko.json +17 -5
  19. package/dist/cjs/assets/locale/messages_lt.json +12 -0
  20. package/dist/cjs/assets/locale/messages_ms.json +12 -0
  21. package/dist/cjs/assets/locale/messages_nl.json +16 -4
  22. package/dist/cjs/assets/locale/messages_no.json +12 -0
  23. package/dist/cjs/assets/locale/messages_pl.json +12 -0
  24. package/dist/cjs/assets/locale/messages_pt.json +16 -4
  25. package/dist/cjs/assets/locale/messages_ro.json +12 -0
  26. package/dist/cjs/assets/locale/messages_ru.json +12 -0
  27. package/dist/cjs/assets/locale/messages_sk.json +12 -0
  28. package/dist/cjs/assets/locale/messages_sl.json +12 -0
  29. package/dist/cjs/assets/locale/messages_sv.json +17 -5
  30. package/dist/cjs/assets/locale/messages_th.json +17 -5
  31. package/dist/cjs/assets/locale/messages_tl.json +12 -0
  32. package/dist/cjs/assets/locale/messages_tr.json +17 -5
  33. package/dist/cjs/assets/locale/messages_uk.json +12 -0
  34. package/dist/cjs/assets/locale/messages_vi.json +17 -5
  35. package/dist/cjs/assets/locale/messages_zh.json +12 -0
  36. package/dist/cjs/assets/locale/version_file +1 -1
  37. package/dist/cjs/assets/visual-e2e-locale/messages_visuale2e.json +4 -1
  38. package/dist/cjs/hooks/useImport.js +7 -5
  39. package/dist/cjs/hooks/useImport.js.map +1 -1
  40. package/dist/cjs/hooks/useImportConfig.js.map +1 -1
  41. package/dist/cjs/services/translations.js.map +1 -1
  42. package/dist/cjs/state/ImportState/ImportState.js +363 -109
  43. package/dist/cjs/state/ImportState/ImportState.js.map +1 -1
  44. package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js +536 -0
  45. package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -0
  46. package/dist/cjs/state/ImportState/__tests__/hubJobClient.spec.js +209 -0
  47. package/dist/cjs/state/ImportState/__tests__/hubJobClient.spec.js.map +1 -0
  48. package/dist/cjs/state/ImportState/__tests__/importJobPoller.spec.js +86 -0
  49. package/dist/cjs/state/ImportState/__tests__/importJobPoller.spec.js.map +1 -0
  50. package/dist/cjs/state/ImportState/__tests__/parseCsv.spec.js +52 -0
  51. package/dist/cjs/state/ImportState/__tests__/parseCsv.spec.js.map +1 -0
  52. package/dist/cjs/state/ImportState/__tests__/rewriteCsv.spec.js +81 -0
  53. package/dist/cjs/state/ImportState/__tests__/rewriteCsv.spec.js.map +1 -0
  54. package/dist/cjs/state/ImportState/fieldEdit.js +46 -0
  55. package/dist/cjs/state/ImportState/fieldEdit.js.map +1 -0
  56. package/dist/cjs/state/ImportState/hubJobClient.js +101 -0
  57. package/dist/cjs/state/ImportState/hubJobClient.js.map +1 -0
  58. package/dist/cjs/state/ImportState/importJobPoller.js +44 -0
  59. package/dist/cjs/state/ImportState/importJobPoller.js.map +1 -0
  60. package/dist/cjs/state/ImportState/index.js +12 -0
  61. package/dist/cjs/state/ImportState/index.js.map +1 -1
  62. package/dist/cjs/state/ImportState/parseCsv.js +39 -10
  63. package/dist/cjs/state/ImportState/parseCsv.js.map +1 -1
  64. package/dist/cjs/state/ImportState/rewriteCsv.js +46 -0
  65. package/dist/cjs/state/ImportState/rewriteCsv.js.map +1 -0
  66. package/dist/cjs/testkit/WixPatternsContainer.driver.js +4 -1
  67. package/dist/cjs/testkit/WixPatternsContainer.driver.js.map +1 -1
  68. package/dist/cjs/testkit/client.js +6 -0
  69. package/dist/cjs/testkit/client.js.map +1 -1
  70. package/dist/cjs/testkit/hubJobImportMocks.js +166 -0
  71. package/dist/cjs/testkit/hubJobImportMocks.js.map +1 -0
  72. package/dist/cjs/testkit/index.js +6 -0
  73. package/dist/cjs/testkit/index.js.map +1 -1
  74. package/dist/esm/assets/locale/messages_ar.json +12 -0
  75. package/dist/esm/assets/locale/messages_bg.json +12 -0
  76. package/dist/esm/assets/locale/messages_ca.json +12 -0
  77. package/dist/esm/assets/locale/messages_cs.json +12 -0
  78. package/dist/esm/assets/locale/messages_da.json +17 -5
  79. package/dist/esm/assets/locale/messages_de.json +12 -0
  80. package/dist/esm/assets/locale/messages_el.json +12 -0
  81. package/dist/esm/assets/locale/messages_en.json +12 -0
  82. package/dist/esm/assets/locale/messages_es.json +16 -4
  83. package/dist/esm/assets/locale/messages_fi.json +12 -0
  84. package/dist/esm/assets/locale/messages_fr.json +12 -0
  85. package/dist/esm/assets/locale/messages_he.json +12 -0
  86. package/dist/esm/assets/locale/messages_hi.json +17 -5
  87. package/dist/esm/assets/locale/messages_hu.json +12 -0
  88. package/dist/esm/assets/locale/messages_id.json +17 -5
  89. package/dist/esm/assets/locale/messages_it.json +17 -5
  90. package/dist/esm/assets/locale/messages_ja.json +12 -0
  91. package/dist/esm/assets/locale/messages_ko.json +17 -5
  92. package/dist/esm/assets/locale/messages_lt.json +12 -0
  93. package/dist/esm/assets/locale/messages_ms.json +12 -0
  94. package/dist/esm/assets/locale/messages_nl.json +16 -4
  95. package/dist/esm/assets/locale/messages_no.json +12 -0
  96. package/dist/esm/assets/locale/messages_pl.json +12 -0
  97. package/dist/esm/assets/locale/messages_pt.json +16 -4
  98. package/dist/esm/assets/locale/messages_ro.json +12 -0
  99. package/dist/esm/assets/locale/messages_ru.json +12 -0
  100. package/dist/esm/assets/locale/messages_sk.json +12 -0
  101. package/dist/esm/assets/locale/messages_sl.json +12 -0
  102. package/dist/esm/assets/locale/messages_sv.json +17 -5
  103. package/dist/esm/assets/locale/messages_th.json +17 -5
  104. package/dist/esm/assets/locale/messages_tl.json +12 -0
  105. package/dist/esm/assets/locale/messages_tr.json +17 -5
  106. package/dist/esm/assets/locale/messages_uk.json +12 -0
  107. package/dist/esm/assets/locale/messages_vi.json +17 -5
  108. package/dist/esm/assets/locale/messages_zh.json +12 -0
  109. package/dist/esm/assets/locale/version_file +1 -1
  110. package/dist/esm/assets/visual-e2e-locale/messages_visuale2e.json +4 -1
  111. package/dist/esm/hooks/useImport.js +5 -4
  112. package/dist/esm/hooks/useImport.js.map +1 -1
  113. package/dist/esm/hooks/useImportConfig.js.map +1 -1
  114. package/dist/esm/state/ImportState/ImportState.js +293 -101
  115. package/dist/esm/state/ImportState/ImportState.js.map +1 -1
  116. package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js +385 -0
  117. package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -0
  118. package/dist/esm/state/ImportState/__tests__/hubJobClient.spec.js +139 -0
  119. package/dist/esm/state/ImportState/__tests__/hubJobClient.spec.js.map +1 -0
  120. package/dist/esm/state/ImportState/__tests__/importJobPoller.spec.js +62 -0
  121. package/dist/esm/state/ImportState/__tests__/importJobPoller.spec.js.map +1 -0
  122. package/dist/esm/state/ImportState/__tests__/parseCsv.spec.js +59 -0
  123. package/dist/esm/state/ImportState/__tests__/parseCsv.spec.js.map +1 -0
  124. package/dist/esm/state/ImportState/__tests__/rewriteCsv.spec.js +68 -0
  125. package/dist/esm/state/ImportState/__tests__/rewriteCsv.spec.js.map +1 -0
  126. package/dist/esm/state/ImportState/fieldEdit.js +33 -0
  127. package/dist/esm/state/ImportState/fieldEdit.js.map +1 -0
  128. package/dist/esm/state/ImportState/hubJobClient.js +43 -0
  129. package/dist/esm/state/ImportState/hubJobClient.js.map +1 -0
  130. package/dist/esm/state/ImportState/importJobPoller.js +34 -0
  131. package/dist/esm/state/ImportState/importJobPoller.js.map +1 -0
  132. package/dist/esm/state/ImportState/index.js +2 -0
  133. package/dist/esm/state/ImportState/index.js.map +1 -1
  134. package/dist/esm/state/ImportState/parseCsv.js +34 -10
  135. package/dist/esm/state/ImportState/parseCsv.js.map +1 -1
  136. package/dist/esm/state/ImportState/rewriteCsv.js +39 -0
  137. package/dist/esm/state/ImportState/rewriteCsv.js.map +1 -0
  138. package/dist/esm/testkit/WixPatternsContainer.driver.js +4 -1
  139. package/dist/esm/testkit/WixPatternsContainer.driver.js.map +1 -1
  140. package/dist/esm/testkit/client.js +1 -0
  141. package/dist/esm/testkit/client.js.map +1 -1
  142. package/dist/esm/testkit/hubJobImportMocks.js +173 -0
  143. package/dist/esm/testkit/hubJobImportMocks.js.map +1 -0
  144. package/dist/esm/testkit/index.js +1 -0
  145. package/dist/esm/testkit/index.js.map +1 -1
  146. package/dist/types/hooks/useImport.d.ts +5 -3
  147. package/dist/types/hooks/useImport.d.ts.map +1 -1
  148. package/dist/types/hooks/useImportConfig.d.ts +2 -3
  149. package/dist/types/hooks/useImportConfig.d.ts.map +1 -1
  150. package/dist/types/services/translations.d.ts +1 -1
  151. package/dist/types/services/translations.d.ts.map +1 -1
  152. package/dist/types/state/ImportState/ImportState.d.ts +118 -20
  153. package/dist/types/state/ImportState/ImportState.d.ts.map +1 -1
  154. package/dist/types/state/ImportState/__tests__/ImportState.engine.spec.d.ts +2 -0
  155. package/dist/types/state/ImportState/__tests__/ImportState.engine.spec.d.ts.map +1 -0
  156. package/dist/types/state/ImportState/__tests__/hubJobClient.spec.d.ts +2 -0
  157. package/dist/types/state/ImportState/__tests__/hubJobClient.spec.d.ts.map +1 -0
  158. package/dist/types/state/ImportState/__tests__/importJobPoller.spec.d.ts +2 -0
  159. package/dist/types/state/ImportState/__tests__/importJobPoller.spec.d.ts.map +1 -0
  160. package/dist/types/state/ImportState/__tests__/parseCsv.spec.d.ts +2 -0
  161. package/dist/types/state/ImportState/__tests__/parseCsv.spec.d.ts.map +1 -0
  162. package/dist/types/state/ImportState/__tests__/rewriteCsv.spec.d.ts +2 -0
  163. package/dist/types/state/ImportState/__tests__/rewriteCsv.spec.d.ts.map +1 -0
  164. package/dist/types/state/ImportState/fieldEdit.d.ts +10 -0
  165. package/dist/types/state/ImportState/fieldEdit.d.ts.map +1 -0
  166. package/dist/types/state/ImportState/hubJobClient.d.ts +47 -0
  167. package/dist/types/state/ImportState/hubJobClient.d.ts.map +1 -0
  168. package/dist/types/state/ImportState/importJobPoller.d.ts +20 -0
  169. package/dist/types/state/ImportState/importJobPoller.d.ts.map +1 -0
  170. package/dist/types/state/ImportState/index.d.ts +2 -0
  171. package/dist/types/state/ImportState/index.d.ts.map +1 -1
  172. package/dist/types/state/ImportState/parseCsv.d.ts +7 -0
  173. package/dist/types/state/ImportState/parseCsv.d.ts.map +1 -1
  174. package/dist/types/state/ImportState/rewriteCsv.d.ts +25 -0
  175. package/dist/types/state/ImportState/rewriteCsv.d.ts.map +1 -0
  176. package/dist/types/testkit/WixPatternsContainer.driver.d.ts +22 -0
  177. package/dist/types/testkit/WixPatternsContainer.driver.d.ts.map +1 -1
  178. package/dist/types/testkit/client.d.ts +1 -0
  179. package/dist/types/testkit/client.d.ts.map +1 -1
  180. package/dist/types/testkit/hubJobImportMocks.d.ts +40 -0
  181. package/dist/types/testkit/hubJobImportMocks.d.ts.map +1 -0
  182. package/dist/types/testkit/index.d.ts +1 -0
  183. package/dist/types/testkit/index.d.ts.map +1 -1
  184. package/package.json +4 -4
@@ -7,16 +7,60 @@ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/de
7
7
  var _mobx = require("mobx");
8
8
  var _model = require("../../model");
9
9
  var _ConditionalModalState = require("../ConditionalModalState");
10
+ var _types = require("@wix/bex-utils/@wix/ambassador-hub-v1-job/types");
11
+ exports.WritePolicy = _types.WritePolicy;
10
12
  var _parseCsv = require("./parseCsv");
13
+ var _rewriteCsv = require("./rewriteCsv");
14
+ var _hubJobClient = require("./hubJobClient");
15
+ var _importJobPoller = require("./importJobPoller");
11
16
  var _detectFieldType = require("./detectFieldType");
12
- var _set = _interopRequireDefault(require("lodash/set"));
17
+ var _fieldEdit = require("./fieldEdit");
18
+ /** Outcome of a server-side import job, derived from the job's progress. */
19
+
20
+ /**
21
+ * Optional snapshot taken before the import runs, returning a `restore` used to
22
+ * undo the import on cancel. See {@link BackupResult.restore} for the revert
23
+ * contract under upsert.
24
+ */
25
+
26
+ /**
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 error — and 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`):
36
+ *
37
+ * onJobError: (run) =>
38
+ * errorHandler.withErrorHandler(run, {
39
+ * [jobErrorCode]: (error) => ({ message: translate(...) }),
40
+ * })
41
+ *
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.
45
+ */
46
+
13
47
  // `NewFieldType`, `detectColumnType`, etc. are re-exported from the package via
14
48
  // the `ImportState` barrel (`./index`), which also exports `./detectFieldType`.
15
49
 
50
+ // Re-export the write-policy enum so consumers (and the cairo UI) reference it
51
+ // from `@wix/bex-core` rather than the bundled ambassador path.
52
+
53
+ // Per-type configuration for a new field, captured from the "Edit field" editor
54
+ // (CAIRO-4398). All optional — an unedited field carries none and is created
55
+ // from detected defaults only. `displayName` is intentionally not here: the
56
+ // (possibly user-overridden) name is deduped and surfaced as
57
+ // `NewFieldDefinition.displayName`.
58
+
16
59
  // A new field to add to the collection schema during import. `key` is decided by
17
- // Cairo (derived from the column header, deduped) and is also the key the
18
- // column's values are written under, so the consumer must add the field under
19
- // exactly this key.
60
+ // Cairo (derived from the column header or the user's name override — deduped)
61
+ // and is also the key the column's values are written under, so the consumer
62
+ // must add the field under exactly this key. `config` carries any per-type
63
+ // configuration the field editor produced; create the field with it.
20
64
 
21
65
  /**
22
66
  * Consumer-supplied callback that adds the given new fields to the collection
@@ -30,17 +74,23 @@ var _set = _interopRequireDefault(require("lodash/set"));
30
74
  /**
31
75
  * How a CSV column is mapped:
32
76
  * - `existing` — into an existing collection field by id
33
- * - `new` — into a new field created during import (always text for now)
77
+ * - `new` — into a new field created during import. `fieldType` is the detected
78
+ * type, or the type the user picked in the field editor. `displayName` and
79
+ * `config` are present only once the user edited the pending field via the
80
+ * "Edit field" editor — `displayName` overrides the header-derived name and
81
+ * `config` carries the per-type configuration.
34
82
  * - `null` — don't import (skip)
35
83
  */
36
84
 
37
- const DEFAULT_BATCH_SIZE = 100;
38
-
39
85
  // How long to keep the completed progress bar (100%) on screen before closing
40
86
  // the modal on a clean import. Without it, an import of very few rows finishes
41
87
  // near-instantly and the modal appears to vanish without ever showing the bar
42
88
  // reaching completion.
43
89
  const COMPLETION_LINGER_MS = 600;
90
+
91
+ // The Wix Data collection's id field key. A CSV column mapped to this field lets
92
+ // the user choose how rows that collide on `_id` are written (writePolicy).
93
+ const ID_FIELD_KEY = '_id';
44
94
  function normalize(value) {
45
95
  return value.trim().toLowerCase();
46
96
  }
@@ -76,23 +126,10 @@ function uniqueFieldNameAndKey(header, usedNames, usedKeys) {
76
126
  displayName = `${header}-${suffix}`;
77
127
  }
78
128
  }
79
- function toMappedRows(rows, headers, mappingByHeader) {
80
- return rows.map(row => {
81
- const mapped = {};
82
- headers.forEach((header, index) => {
83
- const mappedField = mappingByHeader[header];
84
- if (mappedField == null) {
85
- return;
86
- }
87
- (0, _set.default)(mapped, mappedField, row[index] ?? '');
88
- });
89
- return mapped;
90
- });
91
- }
92
129
  class ImportState {
93
130
  constructor(props) {
94
131
  (0, _defineProperty2.default)(this, "fields", void 0);
95
- (0, _defineProperty2.default)(this, "bulkUploadItems", void 0);
132
+ (0, _defineProperty2.default)(this, "collectionId", void 0);
96
133
  (0, _defineProperty2.default)(this, "backup", void 0);
97
134
  (0, _defineProperty2.default)(this, "updateSchema", void 0);
98
135
  (0, _defineProperty2.default)(this, "collection", void 0);
@@ -101,7 +138,8 @@ class ImportState {
101
138
  (0, _defineProperty2.default)(this, "errorHandler", void 0);
102
139
  (0, _defineProperty2.default)(this, "showToast", void 0);
103
140
  (0, _defineProperty2.default)(this, "downloadCsv", void 0);
104
- (0, _defineProperty2.default)(this, "batchSize", void 0);
141
+ (0, _defineProperty2.default)(this, "onJobError", void 0);
142
+ (0, _defineProperty2.default)(this, "hubJobClient", void 0);
105
143
  (0, _defineProperty2.default)(this, "_detectColumnType", void 0);
106
144
  (0, _defineProperty2.default)(this, "discardChangesModal", new _ConditionalModalState.ConditionalModalState());
107
145
  (0, _defineProperty2.default)(this, "currentStep", 'upload');
@@ -112,25 +150,36 @@ class ImportState {
112
150
  (0, _defineProperty2.default)(this, "csvHeaders", []);
113
151
  (0, _defineProperty2.default)(this, "csvRows", []);
114
152
  (0, _defineProperty2.default)(this, "mappingByHeader", {});
153
+ // How rows that collide on `_id` are written. Only meaningful (and only sent
154
+ // to the server) when a CSV column is mapped to the `_id` field — see
155
+ // `idMappedHeader` and `_destination`. Defaults to the server default
156
+ // (`OVERWRITE` = upsert by `_id`).
157
+ //
158
+ // Retention: the choice is deliberately kept while editing mappings within a
159
+ // file — remapping `_id` away and back keeps the user's selection rather than
160
+ // silently reverting it. It resets to the default only on a *new file* upload
161
+ // (`applyParsedCsv`) or a full `reset()`.
162
+ (0, _defineProperty2.default)(this, "writePolicy", _types.WritePolicy.OVERWRITE);
115
163
  (0, _defineProperty2.default)(this, "processedCount", 0);
116
164
  (0, _defineProperty2.default)(this, "totalCount", 0);
117
165
  (0, _defineProperty2.default)(this, "result", {
118
166
  succeeded: 0,
119
- failed: 0,
120
- errors: []
167
+ failed: 0
121
168
  });
122
169
  (0, _defineProperty2.default)(this, "isBackingUp", false);
123
170
  (0, _defineProperty2.default)(this, "isRestoring", false);
124
171
  (0, _defineProperty2.default)(this, "restoreFn", null);
125
172
  (0, _defineProperty2.default)(this, "_cancelled", false);
126
173
  (0, _defineProperty2.default)(this, "_abortController", null);
127
- // Global indices (into `csvRows`) of every row that was not imported: rows
128
- // the server rejected via a batch's returned `errors`, plus when a batch
129
- // throws after some rows succeeded — the throwing batch and all un-attempted
130
- // rows. Offered to the user as a downloadable CSV via the partial toast.
131
- (0, _defineProperty2.default)(this, "_failedRowIndices", []);
174
+ // Id of the running data-movement job, set once submitted used to cancel it
175
+ // (`terminateJob`) if the user closes mid-import.
176
+ (0, _defineProperty2.default)(this, "_jobId", null);
177
+ // The column whose pending new field the field editor is currently editing.
178
+ // Not observable — transient wiring between `beginFieldEdit` and the editor's
179
+ // `applyFieldEdit` callback, never rendered.
180
+ (0, _defineProperty2.default)(this, "_editingHeader", null);
132
181
  this.fields = props.fields ?? [];
133
- this.bulkUploadItems = props.bulkUploadItems;
182
+ this.collectionId = props.collectionId;
134
183
  this.backup = props.backup;
135
184
  this.updateSchema = props.updateSchema;
136
185
  this.collection = props.collection;
@@ -139,8 +188,12 @@ class ImportState {
139
188
  this.errorHandler = props.errorHandler;
140
189
  this.showToast = props.showToast;
141
190
  this.downloadCsv = props.downloadCsv;
142
- this.batchSize = props.batchSize ?? DEFAULT_BATCH_SIZE;
191
+ this.onJobError = props.onJobError;
143
192
  this._detectColumnType = props.detectColumnType ?? _detectFieldType.detectColumnType;
193
+ this.hubJobClient = (0, _hubJobClient.createHubJobClient)({
194
+ httpClient: props.httpClient,
195
+ errorHandler: props.errorHandler
196
+ });
144
197
  (0, _mobx.makeObservable)(this, {
145
198
  currentStep: _mobx.observable.ref,
146
199
  importStatus: _mobx.observable.ref,
@@ -148,6 +201,7 @@ class ImportState {
148
201
  csvHeaders: _mobx.observable.ref,
149
202
  csvRows: _mobx.observable.ref,
150
203
  mappingByHeader: _mobx.observable.ref,
204
+ writePolicy: _mobx.observable.ref,
151
205
  processedCount: _mobx.observable.ref,
152
206
  totalCount: _mobx.observable.ref,
153
207
  result: _mobx.observable.ref,
@@ -157,10 +211,15 @@ class ImportState {
157
211
  hasMappings: _mobx.computed,
158
212
  hasNoMappedData: _mobx.computed,
159
213
  selectedFieldIds: _mobx.computed,
214
+ idMappedHeader: _mobx.computed,
215
+ setWritePolicy: _mobx.action.bound,
160
216
  onUploadContinue: _mobx.action.bound,
161
217
  goToUpload: _mobx.action.bound,
162
218
  setMapping: _mobx.action.bound,
163
219
  setNewFieldMapping: _mobx.action.bound,
220
+ setNewFieldConfig: _mobx.action.bound,
221
+ beginFieldEdit: _mobx.action.bound,
222
+ applyFieldEdit: _mobx.action.bound,
164
223
  close: _mobx.action.bound,
165
224
  onFileSelected: _mobx.action.bound,
166
225
  startImport: _mobx.action.bound,
@@ -168,7 +227,15 @@ class ImportState {
168
227
  });
169
228
  }
170
229
  init() {
171
- return () => undefined;
230
+ return () => {
231
+ // Unmounting mid-import stops the poll loop (it checks `_cancelled`) so it
232
+ // doesn't run to terminal holding references. We deliberately do NOT abort
233
+ // the AbortController: during a cancel-triggered restore it holds the
234
+ // restore controller, and aborting it would cancel an in-progress restore.
235
+ // The server-side job is also left running — an unmount isn't an explicit
236
+ // user cancel.
237
+ this._cancelled = true;
238
+ };
172
239
  }
173
240
  get hasParsedRows() {
174
241
  return this.csvHeaders.length > 0 && this.csvRows.length > 0;
@@ -200,6 +267,20 @@ class ImportState {
200
267
  return new Set(Object.values(this.mappingByHeader).filter(m => (m == null ? void 0 : m.kind) === 'existing').map(m => m.fieldId));
201
268
  }
202
269
 
270
+ /**
271
+ * The CSV header currently mapped to the collection's `_id` field, or `null`
272
+ * when no column targets it. Drives both the "When items share the same ID"
273
+ * control (only shown on this row) and whether `writePolicy` is sent at all —
274
+ * with no `_id` mapped, every row is a fresh insert and collisions can't occur.
275
+ */
276
+ get idMappedHeader() {
277
+ const entry = Object.entries(this.mappingByHeader).find(([, mapping]) => (mapping == null ? void 0 : mapping.kind) === 'existing' && mapping.fieldId === ID_FIELD_KEY);
278
+ return entry ? entry[0] : null;
279
+ }
280
+ setWritePolicy(policy) {
281
+ this.writePolicy = policy;
282
+ }
283
+
203
284
  /**
204
285
  * Whether columns may be mapped to a brand-new field created during import.
205
286
  * Driven purely by the presence of the `updateSchema` callback — providing the
@@ -221,24 +302,40 @@ class ImportState {
221
302
  this.csvHeaders = [];
222
303
  this.csvRows = [];
223
304
  this.mappingByHeader = {};
305
+ this.writePolicy = _types.WritePolicy.OVERWRITE;
224
306
  this.processedCount = 0;
225
307
  this.totalCount = 0;
226
308
  this.result = {
227
309
  succeeded: 0,
228
- failed: 0,
229
- errors: []
310
+ failed: 0
230
311
  };
231
312
  this.isBackingUp = false;
232
313
  this.isRestoring = false;
233
314
  this.restoreFn = null;
234
315
  this._cancelled = false;
235
- this._failedRowIndices = [];
316
+ this._jobId = null;
236
317
  }
237
318
  async close() {
238
- if (this.currentStep === 'progress') {
319
+ // While on the progress step we treat close as a cancel — terminate the job
320
+ // and restore the backup — UNLESS the import already succeeded. On the
321
+ // success path `startImport` lingers ~600ms on the completed bar with the
322
+ // step still 'progress'; closing during that window must NOT restore (it
323
+ // would undo a clean import). A successful close just closes the modal and
324
+ // lets the success path do its collection refresh.
325
+ if (this.currentStep === 'progress' && !this.importStatus.isSuccess) {
239
326
  var _this$_abortControlle2;
240
327
  this._cancelled = true;
241
328
  (_this$_abortControlle2 = this._abortController) == null || _this$_abortControlle2.abort();
329
+
330
+ // Stop the server-side job. It does not roll back rows already written —
331
+ // that's what the backup restore below is for.
332
+ if (this._jobId) {
333
+ try {
334
+ await this.hubJobClient.terminateJob(this._jobId);
335
+ } catch (error) {
336
+ console.error('Terminate import job failed', error);
337
+ }
338
+ }
242
339
  if (!this.isBackingUp) {
243
340
  await this._restoreBackup();
244
341
  }
@@ -330,6 +427,10 @@ class ImportState {
330
427
  this.csvHeaders = parsedCsv.headers;
331
428
  this.csvRows = parsedCsv.rows;
332
429
  this.mappingByHeader = this.getDefaultMapping(parsedCsv.headers);
430
+ // A new file is a fresh import: start from the default policy so a prior
431
+ // file's Skip choice doesn't silently carry over (only the new-file path
432
+ // resets — remapping `_id` within the same file keeps the user's choice).
433
+ this.writePolicy = _types.WritePolicy.OVERWRITE;
333
434
  this.currentStep = 'configuration';
334
435
  }
335
436
  async onFileSelected(file) {
@@ -404,12 +505,11 @@ class ImportState {
404
505
  }
405
506
  _showPartialSuccessToast() {
406
507
  var _this$showToast3;
407
- const failed = this._failedRowIndices.length;
408
508
  (_this$showToast3 = this.showToast) == null || _this$showToast3.call(this, {
409
509
  type: 'WARNING',
410
510
  message: this.translate('cairo.import.partialSuccessToast', {
411
- failed: String(failed),
412
- imported: String(this.totalCount - failed)
511
+ failed: String(this.result.failed),
512
+ imported: String(this.result.succeeded)
413
513
  }),
414
514
  action: {
415
515
  text: this.translate('cairo.import.downloadFailedRows'),
@@ -418,12 +518,70 @@ class ImportState {
418
518
  }
419
519
  });
420
520
  }
421
- _downloadFailedRows() {
521
+
522
+ /**
523
+ * Builds a plain, re-importable CSV of the rows the server rejected — the
524
+ * original headers plus just the failed rows — and hands it to `downloadCsv`.
525
+ * The failed rows come from the job's movement logs (which row failed), mapped
526
+ * back to the parsed `csvRows` we still hold in memory.
527
+ */
528
+ async _downloadFailedRows() {
422
529
  var _this$downloadCsv;
423
- const failedRows = this._failedRowIndices.map(index => this.csvRows[index]);
530
+ const jobId = this._jobId;
531
+ if (!jobId) {
532
+ return;
533
+ }
534
+ let indices;
535
+ try {
536
+ indices = await this._fetchFailedRowIndices(jobId);
537
+ } catch (error) {
538
+ console.error('Fetching failed rows report failed', error);
539
+ return;
540
+ }
541
+ const failedRows = indices.map(index => this.csvRows[index]).filter(row => row != null);
542
+ if (failedRows.length === 0) {
543
+ return;
544
+ }
424
545
  const content = (0, _parseCsv.unparseCsv)(this.csvHeaders, failedRows);
425
546
  (_this$downloadCsv = this.downloadCsv) == null || _this$downloadCsv.call(this, content, `failed-${this.fileName || 'import.csv'}`);
426
547
  }
548
+
549
+ // Pages through the job's movement logs and returns the `csvRows` indices of
550
+ // the rows that failed. data-hub's CSV decoder
551
+ // (wix-data-hub: packages/wix-data-hub-executor-c/src/plugins/formats/csv/
552
+ // csv-binary-decoder.ts) parses with the `csv-parse` library (`columns: true`)
553
+ // and sets `sourceItemId = "row ${++index + 1}"`, incrementing once **per
554
+ // record** — not per physical line. So N is the 1-based record number with
555
+ // the header consumed as columns: the first data record is `"row 2"` →
556
+ // `csvRows[0]`, i.e. `csvRows[N - 2]`. Because `csv-parse` is RFC-4180,
557
+ // quoted cells containing embedded newlines are a single record, so they do
558
+ // NOT shift the numbering. Out-of-range indices are guarded below.
559
+ async _fetchFailedRowIndices(jobId) {
560
+ const indices = [];
561
+ let cursor;
562
+ do {
563
+ var _pagingMetadata$curso;
564
+ const {
565
+ logs,
566
+ pagingMetadata
567
+ } = await this.hubJobClient.listMovementLogs(jobId, cursor ? {
568
+ cursor
569
+ } : undefined);
570
+ for (const log of logs ?? []) {
571
+ var _exec;
572
+ if (!log.failure || log.sourceItemId == null) {
573
+ continue;
574
+ }
575
+ const fileLine = Number((_exec = /\d+/.exec(log.sourceItemId)) == null ? void 0 : _exec[0]);
576
+ const index = fileLine - 2;
577
+ if (Number.isInteger(index) && index >= 0 && index < this.csvRows.length) {
578
+ indices.push(index);
579
+ }
580
+ }
581
+ cursor = (pagingMetadata == null ? void 0 : pagingMetadata.hasNext) && ((_pagingMetadata$curso = pagingMetadata.cursors) == null ? void 0 : _pagingMetadata$curso.next) || undefined;
582
+ } while (cursor);
583
+ return indices;
584
+ }
427
585
  setMapping(header, fieldId) {
428
586
  this.mappingByHeader = {
429
587
  ...this.mappingByHeader,
@@ -444,56 +602,158 @@ class ImportState {
444
602
  }
445
603
  };
446
604
  }
447
- async _uploadBatches(batches) {
448
- let processedRows = 0;
449
- let receivedOkResponse = false;
450
- const failedRowIndices = [];
451
- for (const batch of batches) {
452
- if (this._cancelled) {
453
- break;
605
+
606
+ /**
607
+ * Persists the result of the "Edit field" editor for a column's pending new
608
+ * field: the (possibly overridden) type, an optional display-name override,
609
+ * and the per-type configuration. No-op if the column isn't currently mapped
610
+ * to a new field.
611
+ */
612
+ setNewFieldConfig(header, edit) {
613
+ const mapping = this.mappingByHeader[header];
614
+ if ((mapping == null ? void 0 : mapping.kind) !== 'new') {
615
+ return;
616
+ }
617
+ this.mappingByHeader = {
618
+ ...this.mappingByHeader,
619
+ [header]: {
620
+ kind: 'new',
621
+ fieldType: edit.fieldType,
622
+ ...(edit.displayName != null && {
623
+ displayName: edit.displayName
624
+ }),
625
+ ...(edit.config && {
626
+ config: edit.config
627
+ })
454
628
  }
455
- const batchStart = processedRows;
456
- try {
457
- const batchResult = await this.bulkUploadItems(batch);
458
- receivedOkResponse = true;
459
- this.result = {
460
- succeeded: this.result.succeeded + batchResult.succeeded,
461
- failed: this.result.failed + batchResult.failed,
462
- errors: [...this.result.errors, ...batchResult.errors]
463
- };
629
+ };
630
+ }
464
631
 
465
- // Rows the server rejected within an otherwise-OK response. `rowIndex`
466
- // is relative to the batch, so offset it to a global `csvRows` index.
467
- for (const {
468
- rowIndex
469
- } of batchResult.errors) {
470
- failedRowIndices.push(batchStart + rowIndex);
471
- }
472
- } catch (error) {
473
- // Full batch failure: a batch threw before any OK response came back.
474
- // Surface it as an error state with a "Try again" action by rejecting
475
- // up to `startImport`, instead of reporting a partial success.
476
- if (!receivedOkResponse) {
477
- throw error;
478
- }
632
+ /**
633
+ * Marks which column's pending new field the field editor is currently
634
+ * editing, so a later `applyFieldEdit` (the editor's `onSubmit`) knows where to
635
+ * write the result.
636
+ */
637
+ beginFieldEdit(header) {
638
+ this._editingHeader = header;
639
+ }
640
+
641
+ /**
642
+ * `onSubmit` handler for the field editor: maps the edited field spec into the
643
+ * column being edited (set via `beginFieldEdit`). No-op if nothing is being
644
+ * edited.
645
+ */
646
+ applyFieldEdit(field) {
647
+ if (this._editingHeader == null) {
648
+ return;
649
+ }
650
+ this.setNewFieldConfig(this._editingHeader, (0, _fieldEdit.toFieldEdit)(field));
651
+ }
652
+
653
+ /**
654
+ * Runs the server-side import: align the CSV to the resolved field keys,
655
+ * upload it, submit a data-movement job, then poll until terminal. Throws on
656
+ * a failed job so `startImport` surfaces the error state; partial success is
657
+ * reported via `result.failed` and the partial toast.
658
+ */
659
+ async _runImportJob(resolvedMapping) {
660
+ const csv = (0, _rewriteCsv.rewriteParsedCsvForImport)(this.csvHeaders, this.csvRows, resolvedMapping);
661
+ const file = new File([csv], this.fileName || 'import.csv', {
662
+ type: 'text/csv'
663
+ });
664
+ const fileId = await this.hubJobClient.uploadFile(file);
665
+ if (this._cancelled) {
666
+ return this.result;
667
+ }
668
+ const jobId = await this.hubJobClient.submitImportJob({
669
+ fileId,
670
+ name: this.fileName,
671
+ destination: this._destination()
672
+ });
673
+ (0, _mobx.runInAction)(() => {
674
+ this._jobId = jobId;
675
+ });
676
+ if (this._cancelled) {
677
+ // The user cancelled while `submitImportJob` was in flight, so `close()`
678
+ // saw no `_jobId` and couldn't terminate it. Terminate it here, otherwise
679
+ // the job keeps importing server-side after cancel (and would overwrite a
680
+ // restored backup).
681
+ await this.hubJobClient.terminateJob(jobId).catch(() => undefined);
682
+ return this.result;
683
+ }
684
+ const job = await (0, _importJobPoller.pollImportJob)(id => this.hubJobClient.getJob(id), jobId, {
685
+ isCancelled: () => this._cancelled,
686
+ onProgress: polled => this._applyJobProgress(polled)
687
+ });
479
688
 
480
- // A batch threw after some rows were already imported. Every row from
481
- // the throwing batch onward (including the batches we won't attempt)
482
- // counts as failed, on top of any rows already rejected via returned
483
- // errors in earlier batches.
484
- for (let index = batchStart; index < this.totalCount; index++) {
485
- failedRowIndices.push(index);
689
+ // `null` means cancelled mid-poll leave whatever progress we have.
690
+ if (!job) {
691
+ return this.result;
692
+ }
693
+ if (job.status === _types.JobStatus.FAILED) {
694
+ var _job$error;
695
+ // The job ran server-side, so a failure arrives on a 200 (the poll
696
+ // succeeded; the job didn't) with no HTTP error to carry the application
697
+ // error. Build a synthetic, HTTP-error-shaped error (the error handler
698
+ // reads the code from `response.data.details.applicationError`) and hand
699
+ // the throwing operation to the consumer's `onJobError` so THEY wrap it
700
+ // with their own `withErrorHandler` + error-code map — `job.error` is the
701
+ // consumer's application error (their import business logic), so only they
702
+ // can map its codes, and the map stays coupled to the `withErrorHandler`
703
+ // call as everywhere else (e.g. `fetchData`). The map is attached to the
704
+ // error object, which the progress step resolves via `getResolvedError`.
705
+ // Without a handler the same error still throws, falling back to the
706
+ // generic message.
707
+ const error = Object.assign(new Error(`ImportJob.${((_job$error = job.error) == null ? void 0 : _job$error.code) ?? 'failed'}`), {
708
+ response: {
709
+ data: {
710
+ details: {
711
+ applicationError: job.error
712
+ }
713
+ }
486
714
  }
487
- break;
488
- }
489
- processedRows += batch.length;
490
- (0, _mobx.runInAction)(() => {
491
- this.processedCount = Math.min(processedRows, this.totalCount);
492
715
  });
716
+ const run = () => Promise.reject(error);
717
+ if (this.onJobError) {
718
+ // The consumer wraps `run` with `withErrorHandler`; it re-rejects with
719
+ // `error` (now carrying their map), which propagates from here.
720
+ await this.onJobError(run);
721
+ }
722
+ throw error;
493
723
  }
494
- this._failedRowIndices = failedRowIndices;
724
+ this._applyJobProgress(job);
495
725
  return this.result;
496
726
  }
727
+
728
+ // Destination: the consumer's Wix Data collection. `writePolicy` is sent only
729
+ // when a CSV column is mapped to `_id` (the only case collisions can occur);
730
+ // otherwise it's omitted and the server applies its default (upsert by `_id`).
731
+ _destination() {
732
+ return {
733
+ wixDataCollection: {
734
+ collectionId: this.collectionId,
735
+ ...(this.idMappedHeader != null && {
736
+ writePolicy: this.writePolicy
737
+ })
738
+ }
739
+ };
740
+ }
741
+ _applyJobProgress(job) {
742
+ const progress = job.progress;
743
+ const succeeded = (progress == null ? void 0 : progress.itemsSuccessful) ?? 0;
744
+ const failed = (progress == null ? void 0 : progress.itemsRejected) ?? 0;
745
+ const current = (progress == null ? void 0 : progress.current) ?? succeeded + failed;
746
+
747
+ // `totalCount` is fixed up front in `startImport` from the CSV row count
748
+ // (one uploaded row per CSV row); polling only advances `processedCount`.
749
+ (0, _mobx.runInAction)(() => {
750
+ this.processedCount = Math.min(current, this.totalCount);
751
+ this.result = {
752
+ succeeded,
753
+ failed
754
+ };
755
+ });
756
+ }
497
757
  async _runBackup() {
498
758
  const backup = this.backup;
499
759
  if (!backup) {
@@ -550,16 +810,23 @@ class ImportState {
550
810
  } else if (mapping.kind === 'existing') {
551
811
  resolved[header] = mapping.fieldId;
552
812
  } else if (this.updateSchema) {
813
+ var _mapping$displayName;
814
+ // The user's name override (from the field editor) seeds the unique
815
+ // name/key derivation; otherwise fall back to the column header.
816
+ const baseName = ((_mapping$displayName = mapping.displayName) == null ? void 0 : _mapping$displayName.trim()) || header;
553
817
  const {
554
818
  displayName,
555
819
  key
556
- } = uniqueFieldNameAndKey(header, usedNames, usedKeys);
820
+ } = uniqueFieldNameAndKey(baseName, usedNames, usedKeys);
557
821
  usedKeys.add(key);
558
822
  usedNames.add(displayName);
559
823
  newFields.push({
560
824
  key,
561
825
  type: mapping.fieldType,
562
- displayName
826
+ displayName,
827
+ ...(mapping.config && {
828
+ config: mapping.config
829
+ })
563
830
  });
564
831
  resolved[header] = key;
565
832
  } else {
@@ -572,15 +839,6 @@ class ImportState {
572
839
  }
573
840
  return resolved;
574
841
  }
575
- _createRowsBatches(rows) {
576
- return rows.reduce((acc, row, index) => {
577
- if (index % this.batchSize === 0) {
578
- acc.push([]);
579
- }
580
- acc[acc.length - 1].push(row);
581
- return acc;
582
- }, []);
583
- }
584
842
  async startImport() {
585
843
  if (!this.hasMappings) {
586
844
  return;
@@ -588,8 +846,8 @@ class ImportState {
588
846
  this._cancelled = false;
589
847
  this._abortController = new AbortController();
590
848
 
591
- // One mapped row is produced per CSV row, so the total is known up front and
592
- // doesn't depend on field creation below.
849
+ // One uploaded row per CSV row (the rewrite drops columns, not rows), so the
850
+ // total is known up front from the parsed CSV and stays fixed through polling.
593
851
  const totalCount = this.csvRows.length;
594
852
  (0, _mobx.runInAction)(() => {
595
853
  this.currentStep = 'progress';
@@ -603,10 +861,9 @@ class ImportState {
603
861
  };
604
862
  this.result = {
605
863
  succeeded: 0,
606
- failed: 0,
607
- errors: []
864
+ failed: 0
608
865
  };
609
- this._failedRowIndices = [];
866
+ this._jobId = null;
610
867
  });
611
868
  const backupFailed = await this._runBackup();
612
869
  if (backupFailed) {
@@ -617,8 +874,8 @@ class ImportState {
617
874
  }
618
875
 
619
876
  // Create any new fields and resolve every column to its destination key
620
- // before building the payload. A creation failure surfaces as the error
621
- // state with a "Try again" CTA, like a first-batch upload failure.
877
+ // before uploading. A creation failure surfaces as the error state with a
878
+ // "Try again" CTA, like an upload/job failure.
622
879
  let resolvedMapping;
623
880
  try {
624
881
  resolvedMapping = await this._resolveFieldMapping();
@@ -634,9 +891,7 @@ class ImportState {
634
891
  if (this._cancelled) {
635
892
  return;
636
893
  }
637
- const rows = toMappedRows(this.csvRows, this.csvHeaders, resolvedMapping);
638
- const batches = this._createRowsBatches(rows);
639
- const promise = this._uploadBatches(batches);
894
+ const promise = this._runImportJob(resolvedMapping);
640
895
  this.importStatus = {
641
896
  ..._model.queryStatus.loading,
642
897
  promise
@@ -657,17 +912,16 @@ class ImportState {
657
912
  // briefly so the completion is perceptible even for very few rows. Skip
658
913
  // the wait when there are failed rows — that path closes and surfaces a
659
914
  // toast instead.
660
- if (this._failedRowIndices.length === 0) {
915
+ if (this.result.failed === 0) {
661
916
  await this._lingerOnCompletion();
662
917
  if (this._cancelled) {
663
918
  return;
664
919
  }
665
920
  }
666
921
  (0, _mobx.runInAction)(() => {
667
- // Any rows that didn't make it in whether rejected via a batch's
668
- // returned `errors` or left unimported when a batch threw — are
669
- // reported via a toast that offers them as a downloadable CSV.
670
- if (this._failedRowIndices.length > 0) {
922
+ // Rows the server rejected are reported via a toast (the downloadable
923
+ // failed-rows report is added by the failed-rows-report task).
924
+ if (this.result.failed > 0) {
671
925
  this._showPartialSuccessToast();
672
926
  }
673
927
  this.importModalState.close();