@wix/bex-core 2.328.0 → 2.330.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 (254) 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 +5 -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/index.js +5 -2
  42. package/dist/cjs/index.js.map +1 -1
  43. package/dist/cjs/services/DataExtensionFieldOperationsService.js +3 -3
  44. package/dist/cjs/services/DataExtensionFieldOperationsService.js.map +1 -1
  45. package/dist/cjs/services/translations.js.map +1 -1
  46. package/dist/cjs/state/ArchiveFieldModalState.js +5 -8
  47. package/dist/cjs/state/ArchiveFieldModalState.js.map +1 -1
  48. package/dist/cjs/state/CustomFieldModalBiReporter.js +5 -9
  49. package/dist/cjs/state/CustomFieldModalBiReporter.js.map +1 -1
  50. package/dist/cjs/state/CustomFieldModalState.js +32 -71
  51. package/dist/cjs/state/CustomFieldModalState.js.map +1 -1
  52. package/dist/cjs/state/DataExtensionState.js +0 -12
  53. package/dist/cjs/state/DataExtensionState.js.map +1 -1
  54. package/dist/cjs/state/DeleteFieldModalState.js +9 -11
  55. package/dist/cjs/state/DeleteFieldModalState.js.map +1 -1
  56. package/dist/cjs/state/{DataExtensionStateBIReporter.js → FieldOperationsBIReporter.js} +7 -32
  57. package/dist/cjs/state/FieldOperationsBIReporter.js.map +1 -0
  58. package/dist/cjs/state/ImportState/ImportState.js +383 -109
  59. package/dist/cjs/state/ImportState/ImportState.js.map +1 -1
  60. package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js +610 -0
  61. package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -0
  62. package/dist/cjs/state/ImportState/__tests__/hubJobClient.spec.js +209 -0
  63. package/dist/cjs/state/ImportState/__tests__/hubJobClient.spec.js.map +1 -0
  64. package/dist/cjs/state/ImportState/__tests__/importJobPoller.spec.js +86 -0
  65. package/dist/cjs/state/ImportState/__tests__/importJobPoller.spec.js.map +1 -0
  66. package/dist/cjs/state/ImportState/__tests__/parseCsv.spec.js +52 -0
  67. package/dist/cjs/state/ImportState/__tests__/parseCsv.spec.js.map +1 -0
  68. package/dist/cjs/state/ImportState/__tests__/rewriteCsv.spec.js +81 -0
  69. package/dist/cjs/state/ImportState/__tests__/rewriteCsv.spec.js.map +1 -0
  70. package/dist/cjs/state/ImportState/fieldEdit.js +46 -0
  71. package/dist/cjs/state/ImportState/fieldEdit.js.map +1 -0
  72. package/dist/cjs/state/ImportState/hubJobClient.js +101 -0
  73. package/dist/cjs/state/ImportState/hubJobClient.js.map +1 -0
  74. package/dist/cjs/state/ImportState/importJobPoller.js +44 -0
  75. package/dist/cjs/state/ImportState/importJobPoller.js.map +1 -0
  76. package/dist/cjs/state/ImportState/index.js +12 -0
  77. package/dist/cjs/state/ImportState/index.js.map +1 -1
  78. package/dist/cjs/state/ImportState/parseCsv.js +39 -10
  79. package/dist/cjs/state/ImportState/parseCsv.js.map +1 -1
  80. package/dist/cjs/state/ImportState/rewriteCsv.js +46 -0
  81. package/dist/cjs/state/ImportState/rewriteCsv.js.map +1 -0
  82. package/dist/cjs/state/formatPermissionsForBi.js +36 -0
  83. package/dist/cjs/state/formatPermissionsForBi.js.map +1 -0
  84. package/dist/cjs/testkit/WixPatternsContainer.driver.js +4 -1
  85. package/dist/cjs/testkit/WixPatternsContainer.driver.js.map +1 -1
  86. package/dist/cjs/testkit/client.js +6 -0
  87. package/dist/cjs/testkit/client.js.map +1 -1
  88. package/dist/cjs/testkit/hubJobImportMocks.js +166 -0
  89. package/dist/cjs/testkit/hubJobImportMocks.js.map +1 -0
  90. package/dist/cjs/testkit/index.js +6 -0
  91. package/dist/cjs/testkit/index.js.map +1 -1
  92. package/dist/cjs/types/FieldsSource.js +4 -0
  93. package/dist/cjs/types/FieldsSource.js.map +1 -0
  94. package/dist/cjs/types/Schema.js.map +1 -1
  95. package/dist/cjs/types/SchemaSource.js +81 -0
  96. package/dist/cjs/types/SchemaSource.js.map +1 -0
  97. package/dist/esm/assets/locale/messages_ar.json +12 -0
  98. package/dist/esm/assets/locale/messages_bg.json +12 -0
  99. package/dist/esm/assets/locale/messages_ca.json +12 -0
  100. package/dist/esm/assets/locale/messages_cs.json +12 -0
  101. package/dist/esm/assets/locale/messages_da.json +17 -5
  102. package/dist/esm/assets/locale/messages_de.json +12 -0
  103. package/dist/esm/assets/locale/messages_el.json +12 -0
  104. package/dist/esm/assets/locale/messages_en.json +12 -0
  105. package/dist/esm/assets/locale/messages_es.json +16 -4
  106. package/dist/esm/assets/locale/messages_fi.json +12 -0
  107. package/dist/esm/assets/locale/messages_fr.json +12 -0
  108. package/dist/esm/assets/locale/messages_he.json +12 -0
  109. package/dist/esm/assets/locale/messages_hi.json +17 -5
  110. package/dist/esm/assets/locale/messages_hu.json +12 -0
  111. package/dist/esm/assets/locale/messages_id.json +17 -5
  112. package/dist/esm/assets/locale/messages_it.json +17 -5
  113. package/dist/esm/assets/locale/messages_ja.json +12 -0
  114. package/dist/esm/assets/locale/messages_ko.json +17 -5
  115. package/dist/esm/assets/locale/messages_lt.json +12 -0
  116. package/dist/esm/assets/locale/messages_ms.json +12 -0
  117. package/dist/esm/assets/locale/messages_nl.json +16 -4
  118. package/dist/esm/assets/locale/messages_no.json +12 -0
  119. package/dist/esm/assets/locale/messages_pl.json +12 -0
  120. package/dist/esm/assets/locale/messages_pt.json +16 -4
  121. package/dist/esm/assets/locale/messages_ro.json +12 -0
  122. package/dist/esm/assets/locale/messages_ru.json +12 -0
  123. package/dist/esm/assets/locale/messages_sk.json +12 -0
  124. package/dist/esm/assets/locale/messages_sl.json +12 -0
  125. package/dist/esm/assets/locale/messages_sv.json +17 -5
  126. package/dist/esm/assets/locale/messages_th.json +17 -5
  127. package/dist/esm/assets/locale/messages_tl.json +12 -0
  128. package/dist/esm/assets/locale/messages_tr.json +17 -5
  129. package/dist/esm/assets/locale/messages_uk.json +12 -0
  130. package/dist/esm/assets/locale/messages_vi.json +17 -5
  131. package/dist/esm/assets/locale/messages_zh.json +12 -0
  132. package/dist/esm/assets/locale/version_file +1 -1
  133. package/dist/esm/assets/visual-e2e-locale/messages_visuale2e.json +5 -1
  134. package/dist/esm/hooks/useImport.js +5 -4
  135. package/dist/esm/hooks/useImport.js.map +1 -1
  136. package/dist/esm/hooks/useImportConfig.js.map +1 -1
  137. package/dist/esm/index.js +1 -0
  138. package/dist/esm/index.js.map +1 -1
  139. package/dist/esm/services/DataExtensionFieldOperationsService.js +3 -3
  140. package/dist/esm/services/DataExtensionFieldOperationsService.js.map +1 -1
  141. package/dist/esm/state/ArchiveFieldModalState.js +4 -7
  142. package/dist/esm/state/ArchiveFieldModalState.js.map +1 -1
  143. package/dist/esm/state/CustomFieldModalBiReporter.js +5 -8
  144. package/dist/esm/state/CustomFieldModalBiReporter.js.map +1 -1
  145. package/dist/esm/state/CustomFieldModalState.js +28 -52
  146. package/dist/esm/state/CustomFieldModalState.js.map +1 -1
  147. package/dist/esm/state/DataExtensionState.js +0 -10
  148. package/dist/esm/state/DataExtensionState.js.map +1 -1
  149. package/dist/esm/state/DeleteFieldModalState.js +7 -10
  150. package/dist/esm/state/DeleteFieldModalState.js.map +1 -1
  151. package/dist/esm/state/{DataExtensionStateBIReporter.js → FieldOperationsBIReporter.js} +5 -25
  152. package/dist/esm/state/FieldOperationsBIReporter.js.map +1 -0
  153. package/dist/esm/state/ImportState/ImportState.js +314 -101
  154. package/dist/esm/state/ImportState/ImportState.js.map +1 -1
  155. package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js +434 -0
  156. package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -0
  157. package/dist/esm/state/ImportState/__tests__/hubJobClient.spec.js +139 -0
  158. package/dist/esm/state/ImportState/__tests__/hubJobClient.spec.js.map +1 -0
  159. package/dist/esm/state/ImportState/__tests__/importJobPoller.spec.js +62 -0
  160. package/dist/esm/state/ImportState/__tests__/importJobPoller.spec.js.map +1 -0
  161. package/dist/esm/state/ImportState/__tests__/parseCsv.spec.js +59 -0
  162. package/dist/esm/state/ImportState/__tests__/parseCsv.spec.js.map +1 -0
  163. package/dist/esm/state/ImportState/__tests__/rewriteCsv.spec.js +68 -0
  164. package/dist/esm/state/ImportState/__tests__/rewriteCsv.spec.js.map +1 -0
  165. package/dist/esm/state/ImportState/fieldEdit.js +33 -0
  166. package/dist/esm/state/ImportState/fieldEdit.js.map +1 -0
  167. package/dist/esm/state/ImportState/hubJobClient.js +43 -0
  168. package/dist/esm/state/ImportState/hubJobClient.js.map +1 -0
  169. package/dist/esm/state/ImportState/importJobPoller.js +34 -0
  170. package/dist/esm/state/ImportState/importJobPoller.js.map +1 -0
  171. package/dist/esm/state/ImportState/index.js +2 -0
  172. package/dist/esm/state/ImportState/index.js.map +1 -1
  173. package/dist/esm/state/ImportState/parseCsv.js +34 -10
  174. package/dist/esm/state/ImportState/parseCsv.js.map +1 -1
  175. package/dist/esm/state/ImportState/rewriteCsv.js +39 -0
  176. package/dist/esm/state/ImportState/rewriteCsv.js.map +1 -0
  177. package/dist/esm/state/formatPermissionsForBi.js +26 -0
  178. package/dist/esm/state/formatPermissionsForBi.js.map +1 -0
  179. package/dist/esm/testkit/WixPatternsContainer.driver.js +4 -1
  180. package/dist/esm/testkit/WixPatternsContainer.driver.js.map +1 -1
  181. package/dist/esm/testkit/client.js +1 -0
  182. package/dist/esm/testkit/client.js.map +1 -1
  183. package/dist/esm/testkit/hubJobImportMocks.js +173 -0
  184. package/dist/esm/testkit/hubJobImportMocks.js.map +1 -0
  185. package/dist/esm/testkit/index.js +1 -0
  186. package/dist/esm/testkit/index.js.map +1 -1
  187. package/dist/esm/types/FieldsSource.js +2 -0
  188. package/dist/esm/types/FieldsSource.js.map +1 -0
  189. package/dist/esm/types/SchemaSource.js +52 -0
  190. package/dist/esm/types/SchemaSource.js.map +1 -0
  191. package/dist/types/hooks/useImport.d.ts +5 -3
  192. package/dist/types/hooks/useImport.d.ts.map +1 -1
  193. package/dist/types/hooks/useImportConfig.d.ts +2 -3
  194. package/dist/types/hooks/useImportConfig.d.ts.map +1 -1
  195. package/dist/types/index.d.ts +4 -1
  196. package/dist/types/index.d.ts.map +1 -1
  197. package/dist/types/services/translations.d.ts +1 -1
  198. package/dist/types/services/translations.d.ts.map +1 -1
  199. package/dist/types/state/ArchiveFieldModalState.d.ts +7 -5
  200. package/dist/types/state/ArchiveFieldModalState.d.ts.map +1 -1
  201. package/dist/types/state/CustomFieldModalBiReporter.d.ts +4 -3
  202. package/dist/types/state/CustomFieldModalBiReporter.d.ts.map +1 -1
  203. package/dist/types/state/CustomFieldModalState.d.ts +12 -6
  204. package/dist/types/state/CustomFieldModalState.d.ts.map +1 -1
  205. package/dist/types/state/DataExtensionState.d.ts +0 -2
  206. package/dist/types/state/DataExtensionState.d.ts.map +1 -1
  207. package/dist/types/state/DeleteFieldModalState.d.ts +8 -5
  208. package/dist/types/state/DeleteFieldModalState.d.ts.map +1 -1
  209. package/dist/types/state/{DataExtensionStateBIReporter.d.ts → FieldOperationsBIReporter.d.ts} +4 -5
  210. package/dist/types/state/FieldOperationsBIReporter.d.ts.map +1 -0
  211. package/dist/types/state/ImportState/ImportState.d.ts +120 -20
  212. package/dist/types/state/ImportState/ImportState.d.ts.map +1 -1
  213. package/dist/types/state/ImportState/__tests__/ImportState.engine.spec.d.ts +2 -0
  214. package/dist/types/state/ImportState/__tests__/ImportState.engine.spec.d.ts.map +1 -0
  215. package/dist/types/state/ImportState/__tests__/hubJobClient.spec.d.ts +2 -0
  216. package/dist/types/state/ImportState/__tests__/hubJobClient.spec.d.ts.map +1 -0
  217. package/dist/types/state/ImportState/__tests__/importJobPoller.spec.d.ts +2 -0
  218. package/dist/types/state/ImportState/__tests__/importJobPoller.spec.d.ts.map +1 -0
  219. package/dist/types/state/ImportState/__tests__/parseCsv.spec.d.ts +2 -0
  220. package/dist/types/state/ImportState/__tests__/parseCsv.spec.d.ts.map +1 -0
  221. package/dist/types/state/ImportState/__tests__/rewriteCsv.spec.d.ts +2 -0
  222. package/dist/types/state/ImportState/__tests__/rewriteCsv.spec.d.ts.map +1 -0
  223. package/dist/types/state/ImportState/fieldEdit.d.ts +10 -0
  224. package/dist/types/state/ImportState/fieldEdit.d.ts.map +1 -0
  225. package/dist/types/state/ImportState/hubJobClient.d.ts +47 -0
  226. package/dist/types/state/ImportState/hubJobClient.d.ts.map +1 -0
  227. package/dist/types/state/ImportState/importJobPoller.d.ts +20 -0
  228. package/dist/types/state/ImportState/importJobPoller.d.ts.map +1 -0
  229. package/dist/types/state/ImportState/index.d.ts +2 -0
  230. package/dist/types/state/ImportState/index.d.ts.map +1 -1
  231. package/dist/types/state/ImportState/parseCsv.d.ts +7 -0
  232. package/dist/types/state/ImportState/parseCsv.d.ts.map +1 -1
  233. package/dist/types/state/ImportState/rewriteCsv.d.ts +25 -0
  234. package/dist/types/state/ImportState/rewriteCsv.d.ts.map +1 -0
  235. package/dist/types/state/formatPermissionsForBi.d.ts +7 -0
  236. package/dist/types/state/formatPermissionsForBi.d.ts.map +1 -0
  237. package/dist/types/testkit/WixPatternsContainer.driver.d.ts +22 -0
  238. package/dist/types/testkit/WixPatternsContainer.driver.d.ts.map +1 -1
  239. package/dist/types/testkit/client.d.ts +1 -0
  240. package/dist/types/testkit/client.d.ts.map +1 -1
  241. package/dist/types/testkit/hubJobImportMocks.d.ts +40 -0
  242. package/dist/types/testkit/hubJobImportMocks.d.ts.map +1 -0
  243. package/dist/types/testkit/index.d.ts +1 -0
  244. package/dist/types/testkit/index.d.ts.map +1 -1
  245. package/dist/types/types/FieldsSource.d.ts +116 -0
  246. package/dist/types/types/FieldsSource.d.ts.map +1 -0
  247. package/dist/types/types/Schema.d.ts +28 -11
  248. package/dist/types/types/Schema.d.ts.map +1 -1
  249. package/dist/types/types/SchemaSource.d.ts +60 -0
  250. package/dist/types/types/SchemaSource.d.ts.map +1 -0
  251. package/package.json +4 -4
  252. package/dist/cjs/state/DataExtensionStateBIReporter.js.map +0 -1
  253. package/dist/esm/state/DataExtensionStateBIReporter.js.map +0 -1
  254. package/dist/types/state/DataExtensionStateBIReporter.d.ts.map +0 -1
@@ -1,22 +1,36 @@
1
1
  import { action, computed, makeObservable, observable, runInAction, } from 'mobx';
2
2
  import { queryStatus, } from '../../model';
3
3
  import { ConditionalModalState } from '../ConditionalModalState';
4
+ import { JobStatus, WritePolicy, } from '@wix/bex-utils/@wix/ambassador-hub-v1-job/types';
4
5
  import { parseCsv, unparseCsv } from './parseCsv';
6
+ import { rewriteParsedCsvForImport } from './rewriteCsv';
7
+ import { createHubJobClient } from './hubJobClient';
8
+ import { pollImportJob } from './importJobPoller';
5
9
  import { detectColumnType, TYPE_SAMPLE_SIZE, } from './detectFieldType';
6
- import set from 'lodash/set';
7
- const DEFAULT_BATCH_SIZE = 100;
10
+ import { toFieldEdit } from './fieldEdit';
11
+ // `NewFieldType`, `detectColumnType`, etc. are re-exported from the package via
12
+ // the `ImportState` barrel (`./index`), which also exports `./detectFieldType`.
13
+ // Re-export the write-policy enum so consumers (and the cairo UI) reference it
14
+ // from `@wix/bex-core` rather than the bundled ambassador path.
15
+ export { WritePolicy } from '@wix/bex-utils/@wix/ambassador-hub-v1-job/types';
8
16
  // How long to keep the completed progress bar (100%) on screen before closing
9
17
  // the modal on a clean import. Without it, an import of very few rows finishes
10
18
  // near-instantly and the modal appears to vanish without ever showing the bar
11
19
  // reaching completion.
12
20
  const COMPLETION_LINGER_MS = 600;
21
+ // The Wix Data collection's id field key. A CSV column mapped to this field lets
22
+ // the user choose how rows that collide on `_id` are written (writePolicy).
23
+ const ID_FIELD_KEY = '_id';
13
24
  function normalize(value) {
14
25
  return value.trim().toLowerCase();
15
26
  }
16
27
  // camelCase key for a field created from a column header
17
28
  // (e.g. "Product Name" -> "productName").
18
29
  function toFieldKey(header) {
19
- const words = header.trim().split(/[^a-zA-Z0-9]+/).filter(Boolean);
30
+ const words = header
31
+ .trim()
32
+ .split(/[^a-zA-Z0-9]+/)
33
+ .filter(Boolean);
20
34
  if (words.length === 0) {
21
35
  return 'field';
22
36
  }
@@ -44,19 +58,6 @@ function uniqueFieldNameAndKey(header, usedNames, usedKeys) {
44
58
  displayName = `${header}-${suffix}`;
45
59
  }
46
60
  }
47
- function toMappedRows(rows, headers, mappingByHeader) {
48
- return rows.map((row) => {
49
- const mapped = {};
50
- headers.forEach((header, index) => {
51
- const mappedField = mappingByHeader[header];
52
- if (mappedField == null) {
53
- return;
54
- }
55
- set(mapped, mappedField, row[index] ?? '');
56
- });
57
- return mapped;
58
- });
59
- }
60
61
  export class ImportState {
61
62
  constructor(props) {
62
63
  this.discardChangesModal = new ConditionalModalState();
@@ -67,26 +68,43 @@ export class ImportState {
67
68
  this.fileName = '';
68
69
  this.csvHeaders = [];
69
70
  this.csvRows = [];
71
+ // Header display order for the configuration step, snapshotted when the file
72
+ // is parsed. The column auto-mapped to `_id` floats to the top (CAIRO-4418);
73
+ // the order then stays fixed while the user edits mappings, so clearing or
74
+ // remapping the `_id` column can't reorder the row under the user's cursor.
75
+ // Recomputed only on a new file (`applyParsedCsv`) or a full `reset()`.
76
+ this.orderedHeaders = [];
70
77
  this.mappingByHeader = {};
78
+ // How rows that collide on `_id` are written. Only meaningful (and only sent
79
+ // to the server) when a CSV column is mapped to the `_id` field — see
80
+ // `idMappedHeader` and `_destination`. Defaults to the server default
81
+ // (`OVERWRITE` = upsert by `_id`).
82
+ //
83
+ // Retention: the choice is deliberately kept while editing mappings within a
84
+ // file — remapping `_id` away and back keeps the user's selection rather than
85
+ // silently reverting it. It resets to the default only on a *new file* upload
86
+ // (`applyParsedCsv`) or a full `reset()`.
87
+ this.writePolicy = WritePolicy.OVERWRITE;
71
88
  this.processedCount = 0;
72
89
  this.totalCount = 0;
73
90
  this.result = {
74
91
  succeeded: 0,
75
92
  failed: 0,
76
- errors: [],
77
93
  };
78
94
  this.isBackingUp = false;
79
95
  this.isRestoring = false;
80
96
  this.restoreFn = null;
81
97
  this._cancelled = false;
82
98
  this._abortController = null;
83
- // Global indices (into `csvRows`) of every row that was not imported: rows
84
- // the server rejected via a batch's returned `errors`, plus when a batch
85
- // throws after some rows succeeded — the throwing batch and all un-attempted
86
- // rows. Offered to the user as a downloadable CSV via the partial toast.
87
- this._failedRowIndices = [];
99
+ // Id of the running data-movement job, set once submitted used to cancel it
100
+ // (`terminateJob`) if the user closes mid-import.
101
+ this._jobId = null;
102
+ // The column whose pending new field the field editor is currently editing.
103
+ // Not observable — transient wiring between `beginFieldEdit` and the editor's
104
+ // `applyFieldEdit` callback, never rendered.
105
+ this._editingHeader = null;
88
106
  this.fields = (props.fields ?? []);
89
- this.bulkUploadItems = props.bulkUploadItems;
107
+ this.collectionId = props.collectionId;
90
108
  this.backup = props.backup;
91
109
  this.updateSchema = props.updateSchema;
92
110
  this.collection = props.collection;
@@ -95,15 +113,21 @@ export class ImportState {
95
113
  this.errorHandler = props.errorHandler;
96
114
  this.showToast = props.showToast;
97
115
  this.downloadCsv = props.downloadCsv;
98
- this.batchSize = props.batchSize ?? DEFAULT_BATCH_SIZE;
116
+ this.onJobError = props.onJobError;
99
117
  this._detectColumnType = props.detectColumnType ?? detectColumnType;
118
+ this.hubJobClient = createHubJobClient({
119
+ httpClient: props.httpClient,
120
+ errorHandler: props.errorHandler,
121
+ });
100
122
  makeObservable(this, {
101
123
  currentStep: observable.ref,
102
124
  importStatus: observable.ref,
103
125
  fileName: observable.ref,
104
126
  csvHeaders: observable.ref,
105
127
  csvRows: observable.ref,
128
+ orderedHeaders: observable.ref,
106
129
  mappingByHeader: observable.ref,
130
+ writePolicy: observable.ref,
107
131
  processedCount: observable.ref,
108
132
  totalCount: observable.ref,
109
133
  result: observable.ref,
@@ -113,10 +137,15 @@ export class ImportState {
113
137
  hasMappings: computed,
114
138
  hasNoMappedData: computed,
115
139
  selectedFieldIds: computed,
140
+ idMappedHeader: computed,
141
+ setWritePolicy: action.bound,
116
142
  onUploadContinue: action.bound,
117
143
  goToUpload: action.bound,
118
144
  setMapping: action.bound,
119
145
  setNewFieldMapping: action.bound,
146
+ setNewFieldConfig: action.bound,
147
+ beginFieldEdit: action.bound,
148
+ applyFieldEdit: action.bound,
120
149
  close: action.bound,
121
150
  onFileSelected: action.bound,
122
151
  startImport: action.bound,
@@ -124,7 +153,15 @@ export class ImportState {
124
153
  });
125
154
  }
126
155
  init() {
127
- return () => undefined;
156
+ return () => {
157
+ // Unmounting mid-import stops the poll loop (it checks `_cancelled`) so it
158
+ // doesn't run to terminal holding references. We deliberately do NOT abort
159
+ // the AbortController: during a cancel-triggered restore it holds the
160
+ // restore controller, and aborting it would cancel an in-progress restore.
161
+ // The server-side job is also left running — an unmount isn't an explicit
162
+ // user cancel.
163
+ this._cancelled = true;
164
+ };
128
165
  }
129
166
  get hasParsedRows() {
130
167
  return this.csvHeaders.length > 0 && this.csvRows.length > 0;
@@ -156,6 +193,19 @@ export class ImportState {
156
193
  .filter((m) => m?.kind === 'existing')
157
194
  .map((m) => m.fieldId));
158
195
  }
196
+ /**
197
+ * The CSV header currently mapped to the collection's `_id` field, or `null`
198
+ * when no column targets it. Drives both the "When items share the same ID"
199
+ * control (only shown on this row) and whether `writePolicy` is sent at all —
200
+ * with no `_id` mapped, every row is a fresh insert and collisions can't occur.
201
+ */
202
+ get idMappedHeader() {
203
+ const entry = Object.entries(this.mappingByHeader).find(([, mapping]) => mapping?.kind === 'existing' && mapping.fieldId === ID_FIELD_KEY);
204
+ return entry ? entry[0] : null;
205
+ }
206
+ setWritePolicy(policy) {
207
+ this.writePolicy = policy;
208
+ }
159
209
  /**
160
210
  * Whether columns may be mapped to a brand-new field created during import.
161
211
  * Driven purely by the presence of the `updateSchema` callback — providing the
@@ -175,24 +225,41 @@ export class ImportState {
175
225
  this.fileName = '';
176
226
  this.csvHeaders = [];
177
227
  this.csvRows = [];
228
+ this.orderedHeaders = [];
178
229
  this.mappingByHeader = {};
230
+ this.writePolicy = WritePolicy.OVERWRITE;
179
231
  this.processedCount = 0;
180
232
  this.totalCount = 0;
181
233
  this.result = {
182
234
  succeeded: 0,
183
235
  failed: 0,
184
- errors: [],
185
236
  };
186
237
  this.isBackingUp = false;
187
238
  this.isRestoring = false;
188
239
  this.restoreFn = null;
189
240
  this._cancelled = false;
190
- this._failedRowIndices = [];
241
+ this._jobId = null;
191
242
  }
192
243
  async close() {
193
- if (this.currentStep === 'progress') {
244
+ // While on the progress step we treat close as a cancel — terminate the job
245
+ // and restore the backup — UNLESS the import already succeeded. On the
246
+ // success path `startImport` lingers ~600ms on the completed bar with the
247
+ // step still 'progress'; closing during that window must NOT restore (it
248
+ // would undo a clean import). A successful close just closes the modal and
249
+ // lets the success path do its collection refresh.
250
+ if (this.currentStep === 'progress' && !this.importStatus.isSuccess) {
194
251
  this._cancelled = true;
195
252
  this._abortController?.abort();
253
+ // Stop the server-side job. It does not roll back rows already written —
254
+ // that's what the backup restore below is for.
255
+ if (this._jobId) {
256
+ try {
257
+ await this.hubJobClient.terminateJob(this._jobId);
258
+ }
259
+ catch (error) {
260
+ console.error('Terminate import job failed', error);
261
+ }
262
+ }
196
263
  if (!this.isBackingUp) {
197
264
  await this._restoreBackup();
198
265
  }
@@ -278,11 +345,27 @@ export class ImportState {
278
345
  .slice(0, TYPE_SAMPLE_SIZE)
279
346
  .map((row) => row[columnIndex] ?? '');
280
347
  }
348
+ // Puts the `_id`-mapped column first (CAIRO-4418), preserving CSV order for the
349
+ // rest. Returns a fresh array so the snapshot doesn't alias `csvHeaders`.
350
+ _orderHeadersWithIdFirst(headers) {
351
+ const idHeader = this.idMappedHeader;
352
+ return idHeader != null
353
+ ? [idHeader, ...headers.filter((header) => header !== idHeader)]
354
+ : [...headers];
355
+ }
281
356
  applyParsedCsv(fileName, parsedCsv) {
282
357
  this.fileName = fileName;
283
358
  this.csvHeaders = parsedCsv.headers;
284
359
  this.csvRows = parsedCsv.rows;
285
360
  this.mappingByHeader = this.getDefaultMapping(parsedCsv.headers);
361
+ // Snapshot the row order for this file with the auto-mapped `_id` column
362
+ // first. Reads `idMappedHeader`, so it must run after `mappingByHeader` is
363
+ // set. Kept stable until the next file so mapping edits don't reorder rows.
364
+ this.orderedHeaders = this._orderHeadersWithIdFirst(parsedCsv.headers);
365
+ // A new file is a fresh import: start from the default policy so a prior
366
+ // file's Skip choice doesn't silently carry over (only the new-file path
367
+ // resets — remapping `_id` within the same file keeps the user's choice).
368
+ this.writePolicy = WritePolicy.OVERWRITE;
286
369
  this.currentStep = 'configuration';
287
370
  }
288
371
  async onFileSelected(file) {
@@ -334,6 +417,7 @@ export class ImportState {
334
417
  this.fileName = file.name;
335
418
  this.csvHeaders = [];
336
419
  this.csvRows = [];
420
+ this.orderedHeaders = [];
337
421
  this.mappingByHeader = {};
338
422
  this.currentStep = 'upload';
339
423
  });
@@ -354,12 +438,11 @@ export class ImportState {
354
438
  });
355
439
  }
356
440
  _showPartialSuccessToast() {
357
- const failed = this._failedRowIndices.length;
358
441
  this.showToast?.({
359
442
  type: 'WARNING',
360
443
  message: this.translate('cairo.import.partialSuccessToast', {
361
- failed: String(failed),
362
- imported: String(this.totalCount - failed),
444
+ failed: String(this.result.failed),
445
+ imported: String(this.result.succeeded),
363
446
  }),
364
447
  action: {
365
448
  text: this.translate('cairo.import.downloadFailedRows'),
@@ -368,11 +451,66 @@ export class ImportState {
368
451
  },
369
452
  });
370
453
  }
371
- _downloadFailedRows() {
372
- const failedRows = this._failedRowIndices.map((index) => this.csvRows[index]);
454
+ /**
455
+ * Builds a plain, re-importable CSV of the rows the server rejected — the
456
+ * original headers plus just the failed rows — and hands it to `downloadCsv`.
457
+ * The failed rows come from the job's movement logs (which row failed), mapped
458
+ * back to the parsed `csvRows` we still hold in memory.
459
+ */
460
+ async _downloadFailedRows() {
461
+ const jobId = this._jobId;
462
+ if (!jobId) {
463
+ return;
464
+ }
465
+ let indices;
466
+ try {
467
+ indices = await this._fetchFailedRowIndices(jobId);
468
+ }
469
+ catch (error) {
470
+ console.error('Fetching failed rows report failed', error);
471
+ return;
472
+ }
473
+ const failedRows = indices
474
+ .map((index) => this.csvRows[index])
475
+ .filter((row) => row != null);
476
+ if (failedRows.length === 0) {
477
+ return;
478
+ }
373
479
  const content = unparseCsv(this.csvHeaders, failedRows);
374
480
  this.downloadCsv?.(content, `failed-${this.fileName || 'import.csv'}`);
375
481
  }
482
+ // Pages through the job's movement logs and returns the `csvRows` indices of
483
+ // the rows that failed. data-hub's CSV decoder
484
+ // (wix-data-hub: packages/wix-data-hub-executor-c/src/plugins/formats/csv/
485
+ // csv-binary-decoder.ts) parses with the `csv-parse` library (`columns: true`)
486
+ // and sets `sourceItemId = "row ${++index + 1}"`, incrementing once **per
487
+ // record** — not per physical line. So N is the 1-based record number with
488
+ // the header consumed as columns: the first data record is `"row 2"` →
489
+ // `csvRows[0]`, i.e. `csvRows[N - 2]`. Because `csv-parse` is RFC-4180,
490
+ // quoted cells containing embedded newlines are a single record, so they do
491
+ // NOT shift the numbering. Out-of-range indices are guarded below.
492
+ async _fetchFailedRowIndices(jobId) {
493
+ const indices = [];
494
+ let cursor;
495
+ do {
496
+ const { logs, pagingMetadata } = await this.hubJobClient.listMovementLogs(jobId, cursor ? { cursor } : undefined);
497
+ for (const log of logs ?? []) {
498
+ if (!log.failure || log.sourceItemId == null) {
499
+ continue;
500
+ }
501
+ const fileLine = Number(/\d+/.exec(log.sourceItemId)?.[0]);
502
+ const index = fileLine - 2;
503
+ if (Number.isInteger(index) &&
504
+ index >= 0 &&
505
+ index < this.csvRows.length) {
506
+ indices.push(index);
507
+ }
508
+ }
509
+ cursor =
510
+ (pagingMetadata?.hasNext && pagingMetadata.cursors?.next) || undefined;
511
+ } while (cursor);
512
+ return indices;
513
+ }
376
514
  setMapping(header, fieldId) {
377
515
  this.mappingByHeader = {
378
516
  ...this.mappingByHeader,
@@ -387,53 +525,133 @@ export class ImportState {
387
525
  [header]: { kind: 'new', fieldType },
388
526
  };
389
527
  }
390
- async _uploadBatches(batches) {
391
- let processedRows = 0;
392
- let receivedOkResponse = false;
393
- const failedRowIndices = [];
394
- for (const batch of batches) {
395
- if (this._cancelled) {
396
- break;
397
- }
398
- const batchStart = processedRows;
399
- try {
400
- const batchResult = await this.bulkUploadItems(batch);
401
- receivedOkResponse = true;
402
- this.result = {
403
- succeeded: this.result.succeeded + batchResult.succeeded,
404
- failed: this.result.failed + batchResult.failed,
405
- errors: [...this.result.errors, ...batchResult.errors],
406
- };
407
- // Rows the server rejected within an otherwise-OK response. `rowIndex`
408
- // is relative to the batch, so offset it to a global `csvRows` index.
409
- for (const { rowIndex } of batchResult.errors) {
410
- failedRowIndices.push(batchStart + rowIndex);
411
- }
412
- }
413
- catch (error) {
414
- // Full batch failure: a batch threw before any OK response came back.
415
- // Surface it as an error state with a "Try again" action by rejecting
416
- // up to `startImport`, instead of reporting a partial success.
417
- if (!receivedOkResponse) {
418
- throw error;
419
- }
420
- // A batch threw after some rows were already imported. Every row from
421
- // the throwing batch onward (including the batches we won't attempt)
422
- // counts as failed, on top of any rows already rejected via returned
423
- // errors in earlier batches.
424
- for (let index = batchStart; index < this.totalCount; index++) {
425
- failedRowIndices.push(index);
426
- }
427
- break;
528
+ /**
529
+ * Persists the result of the "Edit field" editor for a column's pending new
530
+ * field: the (possibly overridden) type, an optional display-name override,
531
+ * and the per-type configuration. No-op if the column isn't currently mapped
532
+ * to a new field.
533
+ */
534
+ setNewFieldConfig(header, edit) {
535
+ const mapping = this.mappingByHeader[header];
536
+ if (mapping?.kind !== 'new') {
537
+ return;
538
+ }
539
+ this.mappingByHeader = {
540
+ ...this.mappingByHeader,
541
+ [header]: {
542
+ kind: 'new',
543
+ fieldType: edit.fieldType,
544
+ ...(edit.displayName != null && { displayName: edit.displayName }),
545
+ ...(edit.config && { config: edit.config }),
546
+ },
547
+ };
548
+ }
549
+ /**
550
+ * Marks which column's pending new field the field editor is currently
551
+ * editing, so a later `applyFieldEdit` (the editor's `onSubmit`) knows where to
552
+ * write the result.
553
+ */
554
+ beginFieldEdit(header) {
555
+ this._editingHeader = header;
556
+ }
557
+ /**
558
+ * `onSubmit` handler for the field editor: maps the edited field spec into the
559
+ * column being edited (set via `beginFieldEdit`). No-op if nothing is being
560
+ * edited.
561
+ */
562
+ applyFieldEdit(field) {
563
+ if (this._editingHeader == null) {
564
+ return;
565
+ }
566
+ this.setNewFieldConfig(this._editingHeader, toFieldEdit(field));
567
+ }
568
+ /**
569
+ * Runs the server-side import: align the CSV to the resolved field keys,
570
+ * upload it, submit a data-movement job, then poll until terminal. Throws on
571
+ * a failed job so `startImport` surfaces the error state; partial success is
572
+ * reported via `result.failed` and the partial toast.
573
+ */
574
+ async _runImportJob(resolvedMapping) {
575
+ const csv = rewriteParsedCsvForImport(this.csvHeaders, this.csvRows, resolvedMapping);
576
+ const file = new File([csv], this.fileName || 'import.csv', {
577
+ type: 'text/csv',
578
+ });
579
+ const fileId = await this.hubJobClient.uploadFile(file);
580
+ if (this._cancelled) {
581
+ return this.result;
582
+ }
583
+ const jobId = await this.hubJobClient.submitImportJob({
584
+ fileId,
585
+ name: this.fileName,
586
+ destination: this._destination(),
587
+ });
588
+ runInAction(() => {
589
+ this._jobId = jobId;
590
+ });
591
+ if (this._cancelled) {
592
+ // The user cancelled while `submitImportJob` was in flight, so `close()`
593
+ // saw no `_jobId` and couldn't terminate it. Terminate it here, otherwise
594
+ // the job keeps importing server-side after cancel (and would overwrite a
595
+ // restored backup).
596
+ await this.hubJobClient.terminateJob(jobId).catch(() => undefined);
597
+ return this.result;
598
+ }
599
+ const job = await pollImportJob((id) => this.hubJobClient.getJob(id), jobId, {
600
+ isCancelled: () => this._cancelled,
601
+ onProgress: (polled) => this._applyJobProgress(polled),
602
+ });
603
+ // `null` means cancelled mid-poll — leave whatever progress we have.
604
+ if (!job) {
605
+ return this.result;
606
+ }
607
+ if (job.status === JobStatus.FAILED) {
608
+ // The job ran server-side, so a failure arrives on a 200 (the poll
609
+ // succeeded; the job didn't) with no HTTP error to carry the application
610
+ // error. Build a synthetic, HTTP-error-shaped error (the error handler
611
+ // reads the code from `response.data.details.applicationError`) and hand
612
+ // the throwing operation to the consumer's `onJobError` so THEY wrap it
613
+ // with their own `withErrorHandler` + error-code map — `job.error` is the
614
+ // consumer's application error (their import business logic), so only they
615
+ // can map its codes, and the map stays coupled to the `withErrorHandler`
616
+ // call as everywhere else (e.g. `fetchData`). The map is attached to the
617
+ // error object, which the progress step resolves via `getResolvedError`.
618
+ // Without a handler the same error still throws, falling back to the
619
+ // generic message.
620
+ const error = Object.assign(new Error(`ImportJob.${job.error?.code ?? 'failed'}`), { response: { data: { details: { applicationError: job.error } } } });
621
+ const run = () => Promise.reject(error);
622
+ if (this.onJobError) {
623
+ // The consumer wraps `run` with `withErrorHandler`; it re-rejects with
624
+ // `error` (now carrying their map), which propagates from here.
625
+ await this.onJobError(run);
428
626
  }
429
- processedRows += batch.length;
430
- runInAction(() => {
431
- this.processedCount = Math.min(processedRows, this.totalCount);
432
- });
627
+ throw error;
433
628
  }
434
- this._failedRowIndices = failedRowIndices;
629
+ this._applyJobProgress(job);
435
630
  return this.result;
436
631
  }
632
+ // Destination: the consumer's Wix Data collection. `writePolicy` is sent only
633
+ // when a CSV column is mapped to `_id` (the only case collisions can occur);
634
+ // otherwise it's omitted and the server applies its default (upsert by `_id`).
635
+ _destination() {
636
+ return {
637
+ wixDataCollection: {
638
+ collectionId: this.collectionId,
639
+ ...(this.idMappedHeader != null && { writePolicy: this.writePolicy }),
640
+ },
641
+ };
642
+ }
643
+ _applyJobProgress(job) {
644
+ const progress = job.progress;
645
+ const succeeded = progress?.itemsSuccessful ?? 0;
646
+ const failed = progress?.itemsRejected ?? 0;
647
+ const current = progress?.current ?? succeeded + failed;
648
+ // `totalCount` is fixed up front in `startImport` from the CSV row count
649
+ // (one uploaded row per CSV row); polling only advances `processedCount`.
650
+ runInAction(() => {
651
+ this.processedCount = Math.min(current, this.totalCount);
652
+ this.result = { succeeded, failed };
653
+ });
654
+ }
437
655
  async _runBackup() {
438
656
  const backup = this.backup;
439
657
  if (!backup) {
@@ -494,10 +712,18 @@ export class ImportState {
494
712
  resolved[header] = mapping.fieldId;
495
713
  }
496
714
  else if (this.updateSchema) {
497
- const { displayName, key } = uniqueFieldNameAndKey(header, usedNames, usedKeys);
715
+ // The user's name override (from the field editor) seeds the unique
716
+ // name/key derivation; otherwise fall back to the column header.
717
+ const baseName = mapping.displayName?.trim() || header;
718
+ const { displayName, key } = uniqueFieldNameAndKey(baseName, usedNames, usedKeys);
498
719
  usedKeys.add(key);
499
720
  usedNames.add(displayName);
500
- newFields.push({ key, type: mapping.fieldType, displayName });
721
+ newFields.push({
722
+ key,
723
+ type: mapping.fieldType,
724
+ displayName,
725
+ ...(mapping.config && { config: mapping.config }),
726
+ });
501
727
  resolved[header] = key;
502
728
  }
503
729
  else {
@@ -510,23 +736,14 @@ export class ImportState {
510
736
  }
511
737
  return resolved;
512
738
  }
513
- _createRowsBatches(rows) {
514
- return rows.reduce((acc, row, index) => {
515
- if (index % this.batchSize === 0) {
516
- acc.push([]);
517
- }
518
- acc[acc.length - 1].push(row);
519
- return acc;
520
- }, []);
521
- }
522
739
  async startImport() {
523
740
  if (!this.hasMappings) {
524
741
  return;
525
742
  }
526
743
  this._cancelled = false;
527
744
  this._abortController = new AbortController();
528
- // One mapped row is produced per CSV row, so the total is known up front and
529
- // doesn't depend on field creation below.
745
+ // One uploaded row per CSV row (the rewrite drops columns, not rows), so the
746
+ // total is known up front from the parsed CSV and stays fixed through polling.
530
747
  const totalCount = this.csvRows.length;
531
748
  runInAction(() => {
532
749
  this.currentStep = 'progress';
@@ -539,9 +756,8 @@ export class ImportState {
539
756
  this.result = {
540
757
  succeeded: 0,
541
758
  failed: 0,
542
- errors: [],
543
759
  };
544
- this._failedRowIndices = [];
760
+ this._jobId = null;
545
761
  });
546
762
  const backupFailed = await this._runBackup();
547
763
  if (backupFailed) {
@@ -551,8 +767,8 @@ export class ImportState {
551
767
  return;
552
768
  }
553
769
  // Create any new fields and resolve every column to its destination key
554
- // before building the payload. A creation failure surfaces as the error
555
- // state with a "Try again" CTA, like a first-batch upload failure.
770
+ // before uploading. A creation failure surfaces as the error state with a
771
+ // "Try again" CTA, like an upload/job failure.
556
772
  let resolvedMapping;
557
773
  try {
558
774
  resolvedMapping = await this._resolveFieldMapping();
@@ -569,9 +785,7 @@ export class ImportState {
569
785
  if (this._cancelled) {
570
786
  return;
571
787
  }
572
- const rows = toMappedRows(this.csvRows, this.csvHeaders, resolvedMapping);
573
- const batches = this._createRowsBatches(rows);
574
- const promise = this._uploadBatches(batches);
788
+ const promise = this._runImportJob(resolvedMapping);
575
789
  this.importStatus = {
576
790
  ...queryStatus.loading,
577
791
  promise,
@@ -591,17 +805,16 @@ export class ImportState {
591
805
  // briefly so the completion is perceptible even for very few rows. Skip
592
806
  // the wait when there are failed rows — that path closes and surfaces a
593
807
  // toast instead.
594
- if (this._failedRowIndices.length === 0) {
808
+ if (this.result.failed === 0) {
595
809
  await this._lingerOnCompletion();
596
810
  if (this._cancelled) {
597
811
  return;
598
812
  }
599
813
  }
600
814
  runInAction(() => {
601
- // Any rows that didn't make it in whether rejected via a batch's
602
- // returned `errors` or left unimported when a batch threw — are
603
- // reported via a toast that offers them as a downloadable CSV.
604
- if (this._failedRowIndices.length > 0) {
815
+ // Rows the server rejected are reported via a toast (the downloadable
816
+ // failed-rows report is added by the failed-rows-report task).
817
+ if (this.result.failed > 0) {
605
818
  this._showPartialSuccessToast();
606
819
  }
607
820
  this.importModalState.close();