@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.
- package/dist/cjs/assets/locale/messages_ar.json +12 -0
- package/dist/cjs/assets/locale/messages_bg.json +12 -0
- package/dist/cjs/assets/locale/messages_ca.json +12 -0
- package/dist/cjs/assets/locale/messages_cs.json +12 -0
- package/dist/cjs/assets/locale/messages_da.json +17 -5
- package/dist/cjs/assets/locale/messages_de.json +12 -0
- package/dist/cjs/assets/locale/messages_el.json +12 -0
- package/dist/cjs/assets/locale/messages_en.json +12 -0
- package/dist/cjs/assets/locale/messages_es.json +16 -4
- package/dist/cjs/assets/locale/messages_fi.json +12 -0
- package/dist/cjs/assets/locale/messages_fr.json +12 -0
- package/dist/cjs/assets/locale/messages_he.json +12 -0
- package/dist/cjs/assets/locale/messages_hi.json +17 -5
- package/dist/cjs/assets/locale/messages_hu.json +12 -0
- package/dist/cjs/assets/locale/messages_id.json +17 -5
- package/dist/cjs/assets/locale/messages_it.json +17 -5
- package/dist/cjs/assets/locale/messages_ja.json +12 -0
- package/dist/cjs/assets/locale/messages_ko.json +17 -5
- package/dist/cjs/assets/locale/messages_lt.json +12 -0
- package/dist/cjs/assets/locale/messages_ms.json +12 -0
- package/dist/cjs/assets/locale/messages_nl.json +16 -4
- package/dist/cjs/assets/locale/messages_no.json +12 -0
- package/dist/cjs/assets/locale/messages_pl.json +12 -0
- package/dist/cjs/assets/locale/messages_pt.json +16 -4
- package/dist/cjs/assets/locale/messages_ro.json +12 -0
- package/dist/cjs/assets/locale/messages_ru.json +12 -0
- package/dist/cjs/assets/locale/messages_sk.json +12 -0
- package/dist/cjs/assets/locale/messages_sl.json +12 -0
- package/dist/cjs/assets/locale/messages_sv.json +17 -5
- package/dist/cjs/assets/locale/messages_th.json +17 -5
- package/dist/cjs/assets/locale/messages_tl.json +12 -0
- package/dist/cjs/assets/locale/messages_tr.json +17 -5
- package/dist/cjs/assets/locale/messages_uk.json +12 -0
- package/dist/cjs/assets/locale/messages_vi.json +17 -5
- package/dist/cjs/assets/locale/messages_zh.json +12 -0
- package/dist/cjs/assets/locale/version_file +1 -1
- package/dist/cjs/assets/visual-e2e-locale/messages_visuale2e.json +4 -1
- package/dist/cjs/hooks/useImport.js +7 -5
- package/dist/cjs/hooks/useImport.js.map +1 -1
- package/dist/cjs/hooks/useImportConfig.js.map +1 -1
- package/dist/cjs/services/translations.js.map +1 -1
- package/dist/cjs/state/ImportState/ImportState.js +363 -109
- package/dist/cjs/state/ImportState/ImportState.js.map +1 -1
- package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js +536 -0
- package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -0
- package/dist/cjs/state/ImportState/__tests__/hubJobClient.spec.js +209 -0
- package/dist/cjs/state/ImportState/__tests__/hubJobClient.spec.js.map +1 -0
- package/dist/cjs/state/ImportState/__tests__/importJobPoller.spec.js +86 -0
- package/dist/cjs/state/ImportState/__tests__/importJobPoller.spec.js.map +1 -0
- package/dist/cjs/state/ImportState/__tests__/parseCsv.spec.js +52 -0
- package/dist/cjs/state/ImportState/__tests__/parseCsv.spec.js.map +1 -0
- package/dist/cjs/state/ImportState/__tests__/rewriteCsv.spec.js +81 -0
- package/dist/cjs/state/ImportState/__tests__/rewriteCsv.spec.js.map +1 -0
- package/dist/cjs/state/ImportState/fieldEdit.js +46 -0
- package/dist/cjs/state/ImportState/fieldEdit.js.map +1 -0
- package/dist/cjs/state/ImportState/hubJobClient.js +101 -0
- package/dist/cjs/state/ImportState/hubJobClient.js.map +1 -0
- package/dist/cjs/state/ImportState/importJobPoller.js +44 -0
- package/dist/cjs/state/ImportState/importJobPoller.js.map +1 -0
- package/dist/cjs/state/ImportState/index.js +12 -0
- package/dist/cjs/state/ImportState/index.js.map +1 -1
- package/dist/cjs/state/ImportState/parseCsv.js +39 -10
- package/dist/cjs/state/ImportState/parseCsv.js.map +1 -1
- package/dist/cjs/state/ImportState/rewriteCsv.js +46 -0
- package/dist/cjs/state/ImportState/rewriteCsv.js.map +1 -0
- package/dist/cjs/testkit/WixPatternsContainer.driver.js +4 -1
- package/dist/cjs/testkit/WixPatternsContainer.driver.js.map +1 -1
- package/dist/cjs/testkit/client.js +6 -0
- package/dist/cjs/testkit/client.js.map +1 -1
- package/dist/cjs/testkit/hubJobImportMocks.js +166 -0
- package/dist/cjs/testkit/hubJobImportMocks.js.map +1 -0
- package/dist/cjs/testkit/index.js +6 -0
- package/dist/cjs/testkit/index.js.map +1 -1
- package/dist/esm/assets/locale/messages_ar.json +12 -0
- package/dist/esm/assets/locale/messages_bg.json +12 -0
- package/dist/esm/assets/locale/messages_ca.json +12 -0
- package/dist/esm/assets/locale/messages_cs.json +12 -0
- package/dist/esm/assets/locale/messages_da.json +17 -5
- package/dist/esm/assets/locale/messages_de.json +12 -0
- package/dist/esm/assets/locale/messages_el.json +12 -0
- package/dist/esm/assets/locale/messages_en.json +12 -0
- package/dist/esm/assets/locale/messages_es.json +16 -4
- package/dist/esm/assets/locale/messages_fi.json +12 -0
- package/dist/esm/assets/locale/messages_fr.json +12 -0
- package/dist/esm/assets/locale/messages_he.json +12 -0
- package/dist/esm/assets/locale/messages_hi.json +17 -5
- package/dist/esm/assets/locale/messages_hu.json +12 -0
- package/dist/esm/assets/locale/messages_id.json +17 -5
- package/dist/esm/assets/locale/messages_it.json +17 -5
- package/dist/esm/assets/locale/messages_ja.json +12 -0
- package/dist/esm/assets/locale/messages_ko.json +17 -5
- package/dist/esm/assets/locale/messages_lt.json +12 -0
- package/dist/esm/assets/locale/messages_ms.json +12 -0
- package/dist/esm/assets/locale/messages_nl.json +16 -4
- package/dist/esm/assets/locale/messages_no.json +12 -0
- package/dist/esm/assets/locale/messages_pl.json +12 -0
- package/dist/esm/assets/locale/messages_pt.json +16 -4
- package/dist/esm/assets/locale/messages_ro.json +12 -0
- package/dist/esm/assets/locale/messages_ru.json +12 -0
- package/dist/esm/assets/locale/messages_sk.json +12 -0
- package/dist/esm/assets/locale/messages_sl.json +12 -0
- package/dist/esm/assets/locale/messages_sv.json +17 -5
- package/dist/esm/assets/locale/messages_th.json +17 -5
- package/dist/esm/assets/locale/messages_tl.json +12 -0
- package/dist/esm/assets/locale/messages_tr.json +17 -5
- package/dist/esm/assets/locale/messages_uk.json +12 -0
- package/dist/esm/assets/locale/messages_vi.json +17 -5
- package/dist/esm/assets/locale/messages_zh.json +12 -0
- package/dist/esm/assets/locale/version_file +1 -1
- package/dist/esm/assets/visual-e2e-locale/messages_visuale2e.json +4 -1
- package/dist/esm/hooks/useImport.js +5 -4
- package/dist/esm/hooks/useImport.js.map +1 -1
- package/dist/esm/hooks/useImportConfig.js.map +1 -1
- package/dist/esm/state/ImportState/ImportState.js +293 -101
- package/dist/esm/state/ImportState/ImportState.js.map +1 -1
- package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js +385 -0
- package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -0
- package/dist/esm/state/ImportState/__tests__/hubJobClient.spec.js +139 -0
- package/dist/esm/state/ImportState/__tests__/hubJobClient.spec.js.map +1 -0
- package/dist/esm/state/ImportState/__tests__/importJobPoller.spec.js +62 -0
- package/dist/esm/state/ImportState/__tests__/importJobPoller.spec.js.map +1 -0
- package/dist/esm/state/ImportState/__tests__/parseCsv.spec.js +59 -0
- package/dist/esm/state/ImportState/__tests__/parseCsv.spec.js.map +1 -0
- package/dist/esm/state/ImportState/__tests__/rewriteCsv.spec.js +68 -0
- package/dist/esm/state/ImportState/__tests__/rewriteCsv.spec.js.map +1 -0
- package/dist/esm/state/ImportState/fieldEdit.js +33 -0
- package/dist/esm/state/ImportState/fieldEdit.js.map +1 -0
- package/dist/esm/state/ImportState/hubJobClient.js +43 -0
- package/dist/esm/state/ImportState/hubJobClient.js.map +1 -0
- package/dist/esm/state/ImportState/importJobPoller.js +34 -0
- package/dist/esm/state/ImportState/importJobPoller.js.map +1 -0
- package/dist/esm/state/ImportState/index.js +2 -0
- package/dist/esm/state/ImportState/index.js.map +1 -1
- package/dist/esm/state/ImportState/parseCsv.js +34 -10
- package/dist/esm/state/ImportState/parseCsv.js.map +1 -1
- package/dist/esm/state/ImportState/rewriteCsv.js +39 -0
- package/dist/esm/state/ImportState/rewriteCsv.js.map +1 -0
- package/dist/esm/testkit/WixPatternsContainer.driver.js +4 -1
- package/dist/esm/testkit/WixPatternsContainer.driver.js.map +1 -1
- package/dist/esm/testkit/client.js +1 -0
- package/dist/esm/testkit/client.js.map +1 -1
- package/dist/esm/testkit/hubJobImportMocks.js +173 -0
- package/dist/esm/testkit/hubJobImportMocks.js.map +1 -0
- package/dist/esm/testkit/index.js +1 -0
- package/dist/esm/testkit/index.js.map +1 -1
- package/dist/types/hooks/useImport.d.ts +5 -3
- package/dist/types/hooks/useImport.d.ts.map +1 -1
- package/dist/types/hooks/useImportConfig.d.ts +2 -3
- package/dist/types/hooks/useImportConfig.d.ts.map +1 -1
- package/dist/types/services/translations.d.ts +1 -1
- package/dist/types/services/translations.d.ts.map +1 -1
- package/dist/types/state/ImportState/ImportState.d.ts +118 -20
- package/dist/types/state/ImportState/ImportState.d.ts.map +1 -1
- package/dist/types/state/ImportState/__tests__/ImportState.engine.spec.d.ts +2 -0
- package/dist/types/state/ImportState/__tests__/ImportState.engine.spec.d.ts.map +1 -0
- package/dist/types/state/ImportState/__tests__/hubJobClient.spec.d.ts +2 -0
- package/dist/types/state/ImportState/__tests__/hubJobClient.spec.d.ts.map +1 -0
- package/dist/types/state/ImportState/__tests__/importJobPoller.spec.d.ts +2 -0
- package/dist/types/state/ImportState/__tests__/importJobPoller.spec.d.ts.map +1 -0
- package/dist/types/state/ImportState/__tests__/parseCsv.spec.d.ts +2 -0
- package/dist/types/state/ImportState/__tests__/parseCsv.spec.d.ts.map +1 -0
- package/dist/types/state/ImportState/__tests__/rewriteCsv.spec.d.ts +2 -0
- package/dist/types/state/ImportState/__tests__/rewriteCsv.spec.d.ts.map +1 -0
- package/dist/types/state/ImportState/fieldEdit.d.ts +10 -0
- package/dist/types/state/ImportState/fieldEdit.d.ts.map +1 -0
- package/dist/types/state/ImportState/hubJobClient.d.ts +47 -0
- package/dist/types/state/ImportState/hubJobClient.d.ts.map +1 -0
- package/dist/types/state/ImportState/importJobPoller.d.ts +20 -0
- package/dist/types/state/ImportState/importJobPoller.d.ts.map +1 -0
- package/dist/types/state/ImportState/index.d.ts +2 -0
- package/dist/types/state/ImportState/index.d.ts.map +1 -1
- package/dist/types/state/ImportState/parseCsv.d.ts +7 -0
- package/dist/types/state/ImportState/parseCsv.d.ts.map +1 -1
- package/dist/types/state/ImportState/rewriteCsv.d.ts +25 -0
- package/dist/types/state/ImportState/rewriteCsv.d.ts.map +1 -0
- package/dist/types/testkit/WixPatternsContainer.driver.d.ts +22 -0
- package/dist/types/testkit/WixPatternsContainer.driver.d.ts.map +1 -1
- package/dist/types/testkit/client.d.ts +1 -0
- package/dist/types/testkit/client.d.ts.map +1 -1
- package/dist/types/testkit/hubJobImportMocks.d.ts +40 -0
- package/dist/types/testkit/hubJobImportMocks.d.ts.map +1 -0
- package/dist/types/testkit/index.d.ts +1 -0
- package/dist/types/testkit/index.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -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
|
|
7
|
-
|
|
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
|
|
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();
|
|
@@ -68,25 +69,36 @@ export class ImportState {
|
|
|
68
69
|
this.csvHeaders = [];
|
|
69
70
|
this.csvRows = [];
|
|
70
71
|
this.mappingByHeader = {};
|
|
72
|
+
// How rows that collide on `_id` are written. Only meaningful (and only sent
|
|
73
|
+
// to the server) when a CSV column is mapped to the `_id` field — see
|
|
74
|
+
// `idMappedHeader` and `_destination`. Defaults to the server default
|
|
75
|
+
// (`OVERWRITE` = upsert by `_id`).
|
|
76
|
+
//
|
|
77
|
+
// Retention: the choice is deliberately kept while editing mappings within a
|
|
78
|
+
// file — remapping `_id` away and back keeps the user's selection rather than
|
|
79
|
+
// silently reverting it. It resets to the default only on a *new file* upload
|
|
80
|
+
// (`applyParsedCsv`) or a full `reset()`.
|
|
81
|
+
this.writePolicy = WritePolicy.OVERWRITE;
|
|
71
82
|
this.processedCount = 0;
|
|
72
83
|
this.totalCount = 0;
|
|
73
84
|
this.result = {
|
|
74
85
|
succeeded: 0,
|
|
75
86
|
failed: 0,
|
|
76
|
-
errors: [],
|
|
77
87
|
};
|
|
78
88
|
this.isBackingUp = false;
|
|
79
89
|
this.isRestoring = false;
|
|
80
90
|
this.restoreFn = null;
|
|
81
91
|
this._cancelled = false;
|
|
82
92
|
this._abortController = null;
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
|
|
93
|
+
// Id of the running data-movement job, set once submitted — used to cancel it
|
|
94
|
+
// (`terminateJob`) if the user closes mid-import.
|
|
95
|
+
this._jobId = null;
|
|
96
|
+
// The column whose pending new field the field editor is currently editing.
|
|
97
|
+
// Not observable — transient wiring between `beginFieldEdit` and the editor's
|
|
98
|
+
// `applyFieldEdit` callback, never rendered.
|
|
99
|
+
this._editingHeader = null;
|
|
88
100
|
this.fields = (props.fields ?? []);
|
|
89
|
-
this.
|
|
101
|
+
this.collectionId = props.collectionId;
|
|
90
102
|
this.backup = props.backup;
|
|
91
103
|
this.updateSchema = props.updateSchema;
|
|
92
104
|
this.collection = props.collection;
|
|
@@ -95,8 +107,12 @@ export class ImportState {
|
|
|
95
107
|
this.errorHandler = props.errorHandler;
|
|
96
108
|
this.showToast = props.showToast;
|
|
97
109
|
this.downloadCsv = props.downloadCsv;
|
|
98
|
-
this.
|
|
110
|
+
this.onJobError = props.onJobError;
|
|
99
111
|
this._detectColumnType = props.detectColumnType ?? detectColumnType;
|
|
112
|
+
this.hubJobClient = createHubJobClient({
|
|
113
|
+
httpClient: props.httpClient,
|
|
114
|
+
errorHandler: props.errorHandler,
|
|
115
|
+
});
|
|
100
116
|
makeObservable(this, {
|
|
101
117
|
currentStep: observable.ref,
|
|
102
118
|
importStatus: observable.ref,
|
|
@@ -104,6 +120,7 @@ export class ImportState {
|
|
|
104
120
|
csvHeaders: observable.ref,
|
|
105
121
|
csvRows: observable.ref,
|
|
106
122
|
mappingByHeader: observable.ref,
|
|
123
|
+
writePolicy: observable.ref,
|
|
107
124
|
processedCount: observable.ref,
|
|
108
125
|
totalCount: observable.ref,
|
|
109
126
|
result: observable.ref,
|
|
@@ -113,10 +130,15 @@ export class ImportState {
|
|
|
113
130
|
hasMappings: computed,
|
|
114
131
|
hasNoMappedData: computed,
|
|
115
132
|
selectedFieldIds: computed,
|
|
133
|
+
idMappedHeader: computed,
|
|
134
|
+
setWritePolicy: action.bound,
|
|
116
135
|
onUploadContinue: action.bound,
|
|
117
136
|
goToUpload: action.bound,
|
|
118
137
|
setMapping: action.bound,
|
|
119
138
|
setNewFieldMapping: action.bound,
|
|
139
|
+
setNewFieldConfig: action.bound,
|
|
140
|
+
beginFieldEdit: action.bound,
|
|
141
|
+
applyFieldEdit: action.bound,
|
|
120
142
|
close: action.bound,
|
|
121
143
|
onFileSelected: action.bound,
|
|
122
144
|
startImport: action.bound,
|
|
@@ -124,7 +146,15 @@ export class ImportState {
|
|
|
124
146
|
});
|
|
125
147
|
}
|
|
126
148
|
init() {
|
|
127
|
-
return () =>
|
|
149
|
+
return () => {
|
|
150
|
+
// Unmounting mid-import stops the poll loop (it checks `_cancelled`) so it
|
|
151
|
+
// doesn't run to terminal holding references. We deliberately do NOT abort
|
|
152
|
+
// the AbortController: during a cancel-triggered restore it holds the
|
|
153
|
+
// restore controller, and aborting it would cancel an in-progress restore.
|
|
154
|
+
// The server-side job is also left running — an unmount isn't an explicit
|
|
155
|
+
// user cancel.
|
|
156
|
+
this._cancelled = true;
|
|
157
|
+
};
|
|
128
158
|
}
|
|
129
159
|
get hasParsedRows() {
|
|
130
160
|
return this.csvHeaders.length > 0 && this.csvRows.length > 0;
|
|
@@ -156,6 +186,19 @@ export class ImportState {
|
|
|
156
186
|
.filter((m) => m?.kind === 'existing')
|
|
157
187
|
.map((m) => m.fieldId));
|
|
158
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* The CSV header currently mapped to the collection's `_id` field, or `null`
|
|
191
|
+
* when no column targets it. Drives both the "When items share the same ID"
|
|
192
|
+
* control (only shown on this row) and whether `writePolicy` is sent at all —
|
|
193
|
+
* with no `_id` mapped, every row is a fresh insert and collisions can't occur.
|
|
194
|
+
*/
|
|
195
|
+
get idMappedHeader() {
|
|
196
|
+
const entry = Object.entries(this.mappingByHeader).find(([, mapping]) => mapping?.kind === 'existing' && mapping.fieldId === ID_FIELD_KEY);
|
|
197
|
+
return entry ? entry[0] : null;
|
|
198
|
+
}
|
|
199
|
+
setWritePolicy(policy) {
|
|
200
|
+
this.writePolicy = policy;
|
|
201
|
+
}
|
|
159
202
|
/**
|
|
160
203
|
* Whether columns may be mapped to a brand-new field created during import.
|
|
161
204
|
* Driven purely by the presence of the `updateSchema` callback — providing the
|
|
@@ -176,23 +219,39 @@ export class ImportState {
|
|
|
176
219
|
this.csvHeaders = [];
|
|
177
220
|
this.csvRows = [];
|
|
178
221
|
this.mappingByHeader = {};
|
|
222
|
+
this.writePolicy = WritePolicy.OVERWRITE;
|
|
179
223
|
this.processedCount = 0;
|
|
180
224
|
this.totalCount = 0;
|
|
181
225
|
this.result = {
|
|
182
226
|
succeeded: 0,
|
|
183
227
|
failed: 0,
|
|
184
|
-
errors: [],
|
|
185
228
|
};
|
|
186
229
|
this.isBackingUp = false;
|
|
187
230
|
this.isRestoring = false;
|
|
188
231
|
this.restoreFn = null;
|
|
189
232
|
this._cancelled = false;
|
|
190
|
-
this.
|
|
233
|
+
this._jobId = null;
|
|
191
234
|
}
|
|
192
235
|
async close() {
|
|
193
|
-
|
|
236
|
+
// While on the progress step we treat close as a cancel — terminate the job
|
|
237
|
+
// and restore the backup — UNLESS the import already succeeded. On the
|
|
238
|
+
// success path `startImport` lingers ~600ms on the completed bar with the
|
|
239
|
+
// step still 'progress'; closing during that window must NOT restore (it
|
|
240
|
+
// would undo a clean import). A successful close just closes the modal and
|
|
241
|
+
// lets the success path do its collection refresh.
|
|
242
|
+
if (this.currentStep === 'progress' && !this.importStatus.isSuccess) {
|
|
194
243
|
this._cancelled = true;
|
|
195
244
|
this._abortController?.abort();
|
|
245
|
+
// Stop the server-side job. It does not roll back rows already written —
|
|
246
|
+
// that's what the backup restore below is for.
|
|
247
|
+
if (this._jobId) {
|
|
248
|
+
try {
|
|
249
|
+
await this.hubJobClient.terminateJob(this._jobId);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
console.error('Terminate import job failed', error);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
196
255
|
if (!this.isBackingUp) {
|
|
197
256
|
await this._restoreBackup();
|
|
198
257
|
}
|
|
@@ -283,6 +342,10 @@ export class ImportState {
|
|
|
283
342
|
this.csvHeaders = parsedCsv.headers;
|
|
284
343
|
this.csvRows = parsedCsv.rows;
|
|
285
344
|
this.mappingByHeader = this.getDefaultMapping(parsedCsv.headers);
|
|
345
|
+
// A new file is a fresh import: start from the default policy so a prior
|
|
346
|
+
// file's Skip choice doesn't silently carry over (only the new-file path
|
|
347
|
+
// resets — remapping `_id` within the same file keeps the user's choice).
|
|
348
|
+
this.writePolicy = WritePolicy.OVERWRITE;
|
|
286
349
|
this.currentStep = 'configuration';
|
|
287
350
|
}
|
|
288
351
|
async onFileSelected(file) {
|
|
@@ -354,12 +417,11 @@ export class ImportState {
|
|
|
354
417
|
});
|
|
355
418
|
}
|
|
356
419
|
_showPartialSuccessToast() {
|
|
357
|
-
const failed = this._failedRowIndices.length;
|
|
358
420
|
this.showToast?.({
|
|
359
421
|
type: 'WARNING',
|
|
360
422
|
message: this.translate('cairo.import.partialSuccessToast', {
|
|
361
|
-
failed: String(failed),
|
|
362
|
-
imported: String(this.
|
|
423
|
+
failed: String(this.result.failed),
|
|
424
|
+
imported: String(this.result.succeeded),
|
|
363
425
|
}),
|
|
364
426
|
action: {
|
|
365
427
|
text: this.translate('cairo.import.downloadFailedRows'),
|
|
@@ -368,11 +430,66 @@ export class ImportState {
|
|
|
368
430
|
},
|
|
369
431
|
});
|
|
370
432
|
}
|
|
371
|
-
|
|
372
|
-
|
|
433
|
+
/**
|
|
434
|
+
* Builds a plain, re-importable CSV of the rows the server rejected — the
|
|
435
|
+
* original headers plus just the failed rows — and hands it to `downloadCsv`.
|
|
436
|
+
* The failed rows come from the job's movement logs (which row failed), mapped
|
|
437
|
+
* back to the parsed `csvRows` we still hold in memory.
|
|
438
|
+
*/
|
|
439
|
+
async _downloadFailedRows() {
|
|
440
|
+
const jobId = this._jobId;
|
|
441
|
+
if (!jobId) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
let indices;
|
|
445
|
+
try {
|
|
446
|
+
indices = await this._fetchFailedRowIndices(jobId);
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
console.error('Fetching failed rows report failed', error);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const failedRows = indices
|
|
453
|
+
.map((index) => this.csvRows[index])
|
|
454
|
+
.filter((row) => row != null);
|
|
455
|
+
if (failedRows.length === 0) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
373
458
|
const content = unparseCsv(this.csvHeaders, failedRows);
|
|
374
459
|
this.downloadCsv?.(content, `failed-${this.fileName || 'import.csv'}`);
|
|
375
460
|
}
|
|
461
|
+
// Pages through the job's movement logs and returns the `csvRows` indices of
|
|
462
|
+
// the rows that failed. data-hub's CSV decoder
|
|
463
|
+
// (wix-data-hub: packages/wix-data-hub-executor-c/src/plugins/formats/csv/
|
|
464
|
+
// csv-binary-decoder.ts) parses with the `csv-parse` library (`columns: true`)
|
|
465
|
+
// and sets `sourceItemId = "row ${++index + 1}"`, incrementing once **per
|
|
466
|
+
// record** — not per physical line. So N is the 1-based record number with
|
|
467
|
+
// the header consumed as columns: the first data record is `"row 2"` →
|
|
468
|
+
// `csvRows[0]`, i.e. `csvRows[N - 2]`. Because `csv-parse` is RFC-4180,
|
|
469
|
+
// quoted cells containing embedded newlines are a single record, so they do
|
|
470
|
+
// NOT shift the numbering. Out-of-range indices are guarded below.
|
|
471
|
+
async _fetchFailedRowIndices(jobId) {
|
|
472
|
+
const indices = [];
|
|
473
|
+
let cursor;
|
|
474
|
+
do {
|
|
475
|
+
const { logs, pagingMetadata } = await this.hubJobClient.listMovementLogs(jobId, cursor ? { cursor } : undefined);
|
|
476
|
+
for (const log of logs ?? []) {
|
|
477
|
+
if (!log.failure || log.sourceItemId == null) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
const fileLine = Number(/\d+/.exec(log.sourceItemId)?.[0]);
|
|
481
|
+
const index = fileLine - 2;
|
|
482
|
+
if (Number.isInteger(index) &&
|
|
483
|
+
index >= 0 &&
|
|
484
|
+
index < this.csvRows.length) {
|
|
485
|
+
indices.push(index);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
cursor =
|
|
489
|
+
(pagingMetadata?.hasNext && pagingMetadata.cursors?.next) || undefined;
|
|
490
|
+
} while (cursor);
|
|
491
|
+
return indices;
|
|
492
|
+
}
|
|
376
493
|
setMapping(header, fieldId) {
|
|
377
494
|
this.mappingByHeader = {
|
|
378
495
|
...this.mappingByHeader,
|
|
@@ -387,53 +504,133 @@ export class ImportState {
|
|
|
387
504
|
[header]: { kind: 'new', fieldType },
|
|
388
505
|
};
|
|
389
506
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
507
|
+
/**
|
|
508
|
+
* Persists the result of the "Edit field" editor for a column's pending new
|
|
509
|
+
* field: the (possibly overridden) type, an optional display-name override,
|
|
510
|
+
* and the per-type configuration. No-op if the column isn't currently mapped
|
|
511
|
+
* to a new field.
|
|
512
|
+
*/
|
|
513
|
+
setNewFieldConfig(header, edit) {
|
|
514
|
+
const mapping = this.mappingByHeader[header];
|
|
515
|
+
if (mapping?.kind !== 'new') {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
this.mappingByHeader = {
|
|
519
|
+
...this.mappingByHeader,
|
|
520
|
+
[header]: {
|
|
521
|
+
kind: 'new',
|
|
522
|
+
fieldType: edit.fieldType,
|
|
523
|
+
...(edit.displayName != null && { displayName: edit.displayName }),
|
|
524
|
+
...(edit.config && { config: edit.config }),
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Marks which column's pending new field the field editor is currently
|
|
530
|
+
* editing, so a later `applyFieldEdit` (the editor's `onSubmit`) knows where to
|
|
531
|
+
* write the result.
|
|
532
|
+
*/
|
|
533
|
+
beginFieldEdit(header) {
|
|
534
|
+
this._editingHeader = header;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* `onSubmit` handler for the field editor: maps the edited field spec into the
|
|
538
|
+
* column being edited (set via `beginFieldEdit`). No-op if nothing is being
|
|
539
|
+
* edited.
|
|
540
|
+
*/
|
|
541
|
+
applyFieldEdit(field) {
|
|
542
|
+
if (this._editingHeader == null) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
this.setNewFieldConfig(this._editingHeader, toFieldEdit(field));
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Runs the server-side import: align the CSV to the resolved field keys,
|
|
549
|
+
* upload it, submit a data-movement job, then poll until terminal. Throws on
|
|
550
|
+
* a failed job so `startImport` surfaces the error state; partial success is
|
|
551
|
+
* reported via `result.failed` and the partial toast.
|
|
552
|
+
*/
|
|
553
|
+
async _runImportJob(resolvedMapping) {
|
|
554
|
+
const csv = rewriteParsedCsvForImport(this.csvHeaders, this.csvRows, resolvedMapping);
|
|
555
|
+
const file = new File([csv], this.fileName || 'import.csv', {
|
|
556
|
+
type: 'text/csv',
|
|
557
|
+
});
|
|
558
|
+
const fileId = await this.hubJobClient.uploadFile(file);
|
|
559
|
+
if (this._cancelled) {
|
|
560
|
+
return this.result;
|
|
561
|
+
}
|
|
562
|
+
const jobId = await this.hubJobClient.submitImportJob({
|
|
563
|
+
fileId,
|
|
564
|
+
name: this.fileName,
|
|
565
|
+
destination: this._destination(),
|
|
566
|
+
});
|
|
567
|
+
runInAction(() => {
|
|
568
|
+
this._jobId = jobId;
|
|
569
|
+
});
|
|
570
|
+
if (this._cancelled) {
|
|
571
|
+
// The user cancelled while `submitImportJob` was in flight, so `close()`
|
|
572
|
+
// saw no `_jobId` and couldn't terminate it. Terminate it here, otherwise
|
|
573
|
+
// the job keeps importing server-side after cancel (and would overwrite a
|
|
574
|
+
// restored backup).
|
|
575
|
+
await this.hubJobClient.terminateJob(jobId).catch(() => undefined);
|
|
576
|
+
return this.result;
|
|
577
|
+
}
|
|
578
|
+
const job = await pollImportJob((id) => this.hubJobClient.getJob(id), jobId, {
|
|
579
|
+
isCancelled: () => this._cancelled,
|
|
580
|
+
onProgress: (polled) => this._applyJobProgress(polled),
|
|
581
|
+
});
|
|
582
|
+
// `null` means cancelled mid-poll — leave whatever progress we have.
|
|
583
|
+
if (!job) {
|
|
584
|
+
return this.result;
|
|
585
|
+
}
|
|
586
|
+
if (job.status === JobStatus.FAILED) {
|
|
587
|
+
// The job ran server-side, so a failure arrives on a 200 (the poll
|
|
588
|
+
// succeeded; the job didn't) with no HTTP error to carry the application
|
|
589
|
+
// error. Build a synthetic, HTTP-error-shaped error (the error handler
|
|
590
|
+
// reads the code from `response.data.details.applicationError`) and hand
|
|
591
|
+
// the throwing operation to the consumer's `onJobError` so THEY wrap it
|
|
592
|
+
// with their own `withErrorHandler` + error-code map — `job.error` is the
|
|
593
|
+
// consumer's application error (their import business logic), so only they
|
|
594
|
+
// can map its codes, and the map stays coupled to the `withErrorHandler`
|
|
595
|
+
// call as everywhere else (e.g. `fetchData`). The map is attached to the
|
|
596
|
+
// error object, which the progress step resolves via `getResolvedError`.
|
|
597
|
+
// Without a handler the same error still throws, falling back to the
|
|
598
|
+
// generic message.
|
|
599
|
+
const error = Object.assign(new Error(`ImportJob.${job.error?.code ?? 'failed'}`), { response: { data: { details: { applicationError: job.error } } } });
|
|
600
|
+
const run = () => Promise.reject(error);
|
|
601
|
+
if (this.onJobError) {
|
|
602
|
+
// The consumer wraps `run` with `withErrorHandler`; it re-rejects with
|
|
603
|
+
// `error` (now carrying their map), which propagates from here.
|
|
604
|
+
await this.onJobError(run);
|
|
428
605
|
}
|
|
429
|
-
|
|
430
|
-
runInAction(() => {
|
|
431
|
-
this.processedCount = Math.min(processedRows, this.totalCount);
|
|
432
|
-
});
|
|
606
|
+
throw error;
|
|
433
607
|
}
|
|
434
|
-
this.
|
|
608
|
+
this._applyJobProgress(job);
|
|
435
609
|
return this.result;
|
|
436
610
|
}
|
|
611
|
+
// Destination: the consumer's Wix Data collection. `writePolicy` is sent only
|
|
612
|
+
// when a CSV column is mapped to `_id` (the only case collisions can occur);
|
|
613
|
+
// otherwise it's omitted and the server applies its default (upsert by `_id`).
|
|
614
|
+
_destination() {
|
|
615
|
+
return {
|
|
616
|
+
wixDataCollection: {
|
|
617
|
+
collectionId: this.collectionId,
|
|
618
|
+
...(this.idMappedHeader != null && { writePolicy: this.writePolicy }),
|
|
619
|
+
},
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
_applyJobProgress(job) {
|
|
623
|
+
const progress = job.progress;
|
|
624
|
+
const succeeded = progress?.itemsSuccessful ?? 0;
|
|
625
|
+
const failed = progress?.itemsRejected ?? 0;
|
|
626
|
+
const current = progress?.current ?? succeeded + failed;
|
|
627
|
+
// `totalCount` is fixed up front in `startImport` from the CSV row count
|
|
628
|
+
// (one uploaded row per CSV row); polling only advances `processedCount`.
|
|
629
|
+
runInAction(() => {
|
|
630
|
+
this.processedCount = Math.min(current, this.totalCount);
|
|
631
|
+
this.result = { succeeded, failed };
|
|
632
|
+
});
|
|
633
|
+
}
|
|
437
634
|
async _runBackup() {
|
|
438
635
|
const backup = this.backup;
|
|
439
636
|
if (!backup) {
|
|
@@ -494,10 +691,18 @@ export class ImportState {
|
|
|
494
691
|
resolved[header] = mapping.fieldId;
|
|
495
692
|
}
|
|
496
693
|
else if (this.updateSchema) {
|
|
497
|
-
|
|
694
|
+
// The user's name override (from the field editor) seeds the unique
|
|
695
|
+
// name/key derivation; otherwise fall back to the column header.
|
|
696
|
+
const baseName = mapping.displayName?.trim() || header;
|
|
697
|
+
const { displayName, key } = uniqueFieldNameAndKey(baseName, usedNames, usedKeys);
|
|
498
698
|
usedKeys.add(key);
|
|
499
699
|
usedNames.add(displayName);
|
|
500
|
-
newFields.push({
|
|
700
|
+
newFields.push({
|
|
701
|
+
key,
|
|
702
|
+
type: mapping.fieldType,
|
|
703
|
+
displayName,
|
|
704
|
+
...(mapping.config && { config: mapping.config }),
|
|
705
|
+
});
|
|
501
706
|
resolved[header] = key;
|
|
502
707
|
}
|
|
503
708
|
else {
|
|
@@ -510,23 +715,14 @@ export class ImportState {
|
|
|
510
715
|
}
|
|
511
716
|
return resolved;
|
|
512
717
|
}
|
|
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
718
|
async startImport() {
|
|
523
719
|
if (!this.hasMappings) {
|
|
524
720
|
return;
|
|
525
721
|
}
|
|
526
722
|
this._cancelled = false;
|
|
527
723
|
this._abortController = new AbortController();
|
|
528
|
-
// One
|
|
529
|
-
//
|
|
724
|
+
// One uploaded row per CSV row (the rewrite drops columns, not rows), so the
|
|
725
|
+
// total is known up front from the parsed CSV and stays fixed through polling.
|
|
530
726
|
const totalCount = this.csvRows.length;
|
|
531
727
|
runInAction(() => {
|
|
532
728
|
this.currentStep = 'progress';
|
|
@@ -539,9 +735,8 @@ export class ImportState {
|
|
|
539
735
|
this.result = {
|
|
540
736
|
succeeded: 0,
|
|
541
737
|
failed: 0,
|
|
542
|
-
errors: [],
|
|
543
738
|
};
|
|
544
|
-
this.
|
|
739
|
+
this._jobId = null;
|
|
545
740
|
});
|
|
546
741
|
const backupFailed = await this._runBackup();
|
|
547
742
|
if (backupFailed) {
|
|
@@ -551,8 +746,8 @@ export class ImportState {
|
|
|
551
746
|
return;
|
|
552
747
|
}
|
|
553
748
|
// Create any new fields and resolve every column to its destination key
|
|
554
|
-
// before
|
|
555
|
-
//
|
|
749
|
+
// before uploading. A creation failure surfaces as the error state with a
|
|
750
|
+
// "Try again" CTA, like an upload/job failure.
|
|
556
751
|
let resolvedMapping;
|
|
557
752
|
try {
|
|
558
753
|
resolvedMapping = await this._resolveFieldMapping();
|
|
@@ -569,9 +764,7 @@ export class ImportState {
|
|
|
569
764
|
if (this._cancelled) {
|
|
570
765
|
return;
|
|
571
766
|
}
|
|
572
|
-
const
|
|
573
|
-
const batches = this._createRowsBatches(rows);
|
|
574
|
-
const promise = this._uploadBatches(batches);
|
|
767
|
+
const promise = this._runImportJob(resolvedMapping);
|
|
575
768
|
this.importStatus = {
|
|
576
769
|
...queryStatus.loading,
|
|
577
770
|
promise,
|
|
@@ -591,17 +784,16 @@ export class ImportState {
|
|
|
591
784
|
// briefly so the completion is perceptible even for very few rows. Skip
|
|
592
785
|
// the wait when there are failed rows — that path closes and surfaces a
|
|
593
786
|
// toast instead.
|
|
594
|
-
if (this.
|
|
787
|
+
if (this.result.failed === 0) {
|
|
595
788
|
await this._lingerOnCompletion();
|
|
596
789
|
if (this._cancelled) {
|
|
597
790
|
return;
|
|
598
791
|
}
|
|
599
792
|
}
|
|
600
793
|
runInAction(() => {
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
|
|
604
|
-
if (this._failedRowIndices.length > 0) {
|
|
794
|
+
// Rows the server rejected are reported via a toast (the downloadable
|
|
795
|
+
// failed-rows report is added by the failed-rows-report task).
|
|
796
|
+
if (this.result.failed > 0) {
|
|
605
797
|
this._showPartialSuccessToast();
|
|
606
798
|
}
|
|
607
799
|
this.importModalState.close();
|