@wix/bex-core 2.332.0 → 2.333.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/assets/locale/messages_ar.json +3 -0
- package/dist/cjs/assets/locale/messages_bg.json +3 -0
- package/dist/cjs/assets/locale/messages_ca.json +3 -0
- package/dist/cjs/assets/locale/messages_cs.json +3 -0
- package/dist/cjs/assets/locale/messages_da.json +3 -0
- package/dist/cjs/assets/locale/messages_de.json +3 -0
- package/dist/cjs/assets/locale/messages_el.json +3 -0
- package/dist/cjs/assets/locale/messages_en.json +3 -0
- package/dist/cjs/assets/locale/messages_es.json +3 -0
- package/dist/cjs/assets/locale/messages_fi.json +3 -0
- package/dist/cjs/assets/locale/messages_fr.json +3 -0
- package/dist/cjs/assets/locale/messages_he.json +3 -0
- package/dist/cjs/assets/locale/messages_hi.json +3 -0
- package/dist/cjs/assets/locale/messages_hu.json +3 -0
- package/dist/cjs/assets/locale/messages_id.json +3 -0
- package/dist/cjs/assets/locale/messages_it.json +3 -0
- package/dist/cjs/assets/locale/messages_ja.json +3 -0
- package/dist/cjs/assets/locale/messages_ko.json +3 -0
- package/dist/cjs/assets/locale/messages_lt.json +3 -0
- package/dist/cjs/assets/locale/messages_ms.json +3 -0
- package/dist/cjs/assets/locale/messages_nl.json +3 -0
- package/dist/cjs/assets/locale/messages_no.json +3 -0
- package/dist/cjs/assets/locale/messages_pl.json +3 -0
- package/dist/cjs/assets/locale/messages_pt.json +3 -0
- package/dist/cjs/assets/locale/messages_ro.json +3 -0
- package/dist/cjs/assets/locale/messages_ru.json +3 -0
- package/dist/cjs/assets/locale/messages_sk.json +3 -0
- package/dist/cjs/assets/locale/messages_sl.json +3 -0
- package/dist/cjs/assets/locale/messages_sv.json +3 -0
- package/dist/cjs/assets/locale/messages_th.json +3 -0
- package/dist/cjs/assets/locale/messages_tl.json +3 -0
- package/dist/cjs/assets/locale/messages_tr.json +3 -0
- package/dist/cjs/assets/locale/messages_uk.json +3 -0
- package/dist/cjs/assets/locale/messages_vi.json +3 -0
- package/dist/cjs/assets/locale/messages_zh.json +3 -0
- package/dist/cjs/assets/locale/version_file +1 -1
- package/dist/cjs/assets/visual-e2e-locale/messages_visuale2e.json +29 -2
- package/dist/cjs/hooks/index.js +0 -6
- package/dist/cjs/hooks/index.js.map +1 -1
- package/dist/cjs/index.js +0 -7
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/services/translations.js.map +1 -1
- package/dist/cjs/state/CustomFieldModalState.js +12 -0
- package/dist/cjs/state/CustomFieldModalState.js.map +1 -1
- package/dist/cjs/state/ImportState/ImportState.js +324 -133
- package/dist/cjs/state/ImportState/ImportState.js.map +1 -1
- package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js +413 -79
- package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -1
- package/dist/cjs/state/ImportState/detectFieldType.js +23 -0
- package/dist/cjs/state/ImportState/detectFieldType.js.map +1 -1
- package/dist/cjs/state/ImportState/fieldEdit.js +49 -0
- package/dist/cjs/state/ImportState/fieldEdit.js.map +1 -1
- package/dist/cjs/testkit/createInMemorySchemaSource.js +186 -0
- package/dist/cjs/testkit/createInMemorySchemaSource.js.map +1 -0
- package/dist/cjs/testkit/hubJobImportMocks.js +18 -2
- package/dist/cjs/testkit/hubJobImportMocks.js.map +1 -1
- package/dist/cjs/testkit/index.js +6 -0
- package/dist/cjs/testkit/index.js.map +1 -1
- package/dist/cjs/types/SchemaSource.js +2 -1
- package/dist/cjs/types/SchemaSource.js.map +1 -1
- package/dist/esm/assets/locale/messages_ar.json +3 -0
- package/dist/esm/assets/locale/messages_bg.json +3 -0
- package/dist/esm/assets/locale/messages_ca.json +3 -0
- package/dist/esm/assets/locale/messages_cs.json +3 -0
- package/dist/esm/assets/locale/messages_da.json +3 -0
- package/dist/esm/assets/locale/messages_de.json +3 -0
- package/dist/esm/assets/locale/messages_el.json +3 -0
- package/dist/esm/assets/locale/messages_en.json +3 -0
- package/dist/esm/assets/locale/messages_es.json +3 -0
- package/dist/esm/assets/locale/messages_fi.json +3 -0
- package/dist/esm/assets/locale/messages_fr.json +3 -0
- package/dist/esm/assets/locale/messages_he.json +3 -0
- package/dist/esm/assets/locale/messages_hi.json +3 -0
- package/dist/esm/assets/locale/messages_hu.json +3 -0
- package/dist/esm/assets/locale/messages_id.json +3 -0
- package/dist/esm/assets/locale/messages_it.json +3 -0
- package/dist/esm/assets/locale/messages_ja.json +3 -0
- package/dist/esm/assets/locale/messages_ko.json +3 -0
- package/dist/esm/assets/locale/messages_lt.json +3 -0
- package/dist/esm/assets/locale/messages_ms.json +3 -0
- package/dist/esm/assets/locale/messages_nl.json +3 -0
- package/dist/esm/assets/locale/messages_no.json +3 -0
- package/dist/esm/assets/locale/messages_pl.json +3 -0
- package/dist/esm/assets/locale/messages_pt.json +3 -0
- package/dist/esm/assets/locale/messages_ro.json +3 -0
- package/dist/esm/assets/locale/messages_ru.json +3 -0
- package/dist/esm/assets/locale/messages_sk.json +3 -0
- package/dist/esm/assets/locale/messages_sl.json +3 -0
- package/dist/esm/assets/locale/messages_sv.json +3 -0
- package/dist/esm/assets/locale/messages_th.json +3 -0
- package/dist/esm/assets/locale/messages_tl.json +3 -0
- package/dist/esm/assets/locale/messages_tr.json +3 -0
- package/dist/esm/assets/locale/messages_uk.json +3 -0
- package/dist/esm/assets/locale/messages_vi.json +3 -0
- package/dist/esm/assets/locale/messages_zh.json +3 -0
- package/dist/esm/assets/locale/version_file +1 -1
- package/dist/esm/assets/visual-e2e-locale/messages_visuale2e.json +29 -2
- package/dist/esm/hooks/index.js +0 -1
- package/dist/esm/hooks/index.js.map +1 -1
- package/dist/esm/index.js +0 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/state/CustomFieldModalState.js +8 -0
- package/dist/esm/state/CustomFieldModalState.js.map +1 -1
- package/dist/esm/state/ImportState/ImportState.js +246 -111
- package/dist/esm/state/ImportState/ImportState.js.map +1 -1
- package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js +293 -67
- package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -1
- package/dist/esm/state/ImportState/detectFieldType.js +19 -0
- package/dist/esm/state/ImportState/detectFieldType.js.map +1 -1
- package/dist/esm/state/ImportState/fieldEdit.js +44 -0
- package/dist/esm/state/ImportState/fieldEdit.js.map +1 -1
- package/dist/esm/testkit/createInMemorySchemaSource.js +140 -0
- package/dist/esm/testkit/createInMemorySchemaSource.js.map +1 -0
- package/dist/esm/testkit/hubJobImportMocks.js +17 -1
- package/dist/esm/testkit/hubJobImportMocks.js.map +1 -1
- package/dist/esm/testkit/index.js +1 -0
- package/dist/esm/testkit/index.js.map +1 -1
- package/dist/esm/types/SchemaSource.js.map +1 -1
- package/dist/types/hooks/index.d.ts +0 -1
- package/dist/types/hooks/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/services/translations.d.ts +7 -1
- package/dist/types/services/translations.d.ts.map +1 -1
- package/dist/types/state/CustomFieldModalState.d.ts +9 -0
- package/dist/types/state/CustomFieldModalState.d.ts.map +1 -1
- package/dist/types/state/ImportState/ImportState.d.ts +165 -61
- package/dist/types/state/ImportState/ImportState.d.ts.map +1 -1
- package/dist/types/state/ImportState/detectFieldType.d.ts +2 -0
- package/dist/types/state/ImportState/detectFieldType.d.ts.map +1 -1
- package/dist/types/state/ImportState/fieldEdit.d.ts +6 -0
- package/dist/types/state/ImportState/fieldEdit.d.ts.map +1 -1
- package/dist/types/testkit/WixPatternsContainer.driver.d.ts +4 -0
- package/dist/types/testkit/WixPatternsContainer.driver.d.ts.map +1 -1
- package/dist/types/testkit/createInMemorySchemaSource.d.ts +41 -0
- package/dist/types/testkit/createInMemorySchemaSource.d.ts.map +1 -0
- package/dist/types/testkit/hubJobImportMocks.d.ts +11 -1
- package/dist/types/testkit/hubJobImportMocks.d.ts.map +1 -1
- package/dist/types/testkit/index.d.ts +1 -0
- package/dist/types/testkit/index.d.ts.map +1 -1
- package/dist/types/types/SchemaSource.d.ts +6 -4
- package/dist/types/types/SchemaSource.d.ts.map +1 -1
- package/package.json +4 -4
- package/dist/cjs/hooks/useImportConfig.js +0 -12
- package/dist/cjs/hooks/useImportConfig.js.map +0 -1
- package/dist/esm/hooks/useImportConfig.js +0 -6
- package/dist/esm/hooks/useImportConfig.js.map +0 -1
- package/dist/types/hooks/useImportConfig.d.ts +0 -5
- package/dist/types/hooks/useImportConfig.d.ts.map +0 -1
|
@@ -7,16 +7,16 @@ import { rewriteParsedCsvForImport } from './rewriteCsv';
|
|
|
7
7
|
import { createHubJobClient } from './hubJobClient';
|
|
8
8
|
import { pollImportJob } from './importJobPoller';
|
|
9
9
|
import { detectColumnType, TYPE_SAMPLE_SIZE, } from './detectFieldType';
|
|
10
|
-
import { toFieldEdit } from './fieldEdit';
|
|
10
|
+
import { toDataExtensionFieldEdit, toFieldEdit } from './fieldEdit';
|
|
11
11
|
// `NewFieldType`, `detectColumnType`, etc. are re-exported from the package via
|
|
12
12
|
// the `ImportState` barrel (`./index`), which also exports `./detectFieldType`.
|
|
13
13
|
// Re-export the write-policy enum so consumers (and the cairo UI) reference it
|
|
14
14
|
// from `@wix/bex-core` rather than the bundled ambassador path.
|
|
15
15
|
export { WritePolicy } from '@wix/bex-utils/@wix/ambassador-hub-v1-job/types';
|
|
16
|
-
// How long to
|
|
17
|
-
// the
|
|
18
|
-
//
|
|
19
|
-
//
|
|
16
|
+
// How long to hold the completed (100%) progress bar on screen before moving to
|
|
17
|
+
// the summary step (CAIRO-4476). The job reports counts only when it finishes,
|
|
18
|
+
// so the bar sits at 0 for the whole import and would otherwise flip straight to
|
|
19
|
+
// the summary without the user ever seeing it reach completion.
|
|
20
20
|
const COMPLETION_LINGER_MS = 600;
|
|
21
21
|
// The Wix Data collection's id field key. A CSV column mapped to this field lets
|
|
22
22
|
// the user choose how rows that collide on `_id` are written (writePolicy).
|
|
@@ -90,6 +90,8 @@ export class ImportState {
|
|
|
90
90
|
this.result = {
|
|
91
91
|
succeeded: 0,
|
|
92
92
|
failed: 0,
|
|
93
|
+
created: 0,
|
|
94
|
+
updated: 0,
|
|
93
95
|
};
|
|
94
96
|
this.isBackingUp = false;
|
|
95
97
|
this.isRestoring = false;
|
|
@@ -99,10 +101,11 @@ export class ImportState {
|
|
|
99
101
|
// Id of the running data-movement job, set once submitted — used to cancel it
|
|
100
102
|
// (`terminateJob`) if the user closes mid-import.
|
|
101
103
|
this._jobId = null;
|
|
102
|
-
// The column whose pending new field the field editor is currently editing
|
|
103
|
-
//
|
|
104
|
-
// `
|
|
105
|
-
|
|
104
|
+
// The column whose pending new field the field editor is currently editing, or
|
|
105
|
+
// `null` when the editor is closed. Observable so the source's field-editor
|
|
106
|
+
// component (rendered via `renderFieldEditor`) opens/prefills itself when this
|
|
107
|
+
// is set and closes when it clears — the editor is decoupled from the layout.
|
|
108
|
+
this.editingHeader = null;
|
|
106
109
|
this.config = props.config;
|
|
107
110
|
this.collection = props.collection;
|
|
108
111
|
this.importModalState = props.importModalState;
|
|
@@ -133,7 +136,12 @@ export class ImportState {
|
|
|
133
136
|
hasMappings: computed,
|
|
134
137
|
hasNoMappedData: computed,
|
|
135
138
|
selectedFieldIds: computed,
|
|
139
|
+
missingRequiredFields: computed,
|
|
140
|
+
hasMissingRequiredFields: computed,
|
|
136
141
|
idMappedHeader: computed,
|
|
142
|
+
editingHeader: observable.ref,
|
|
143
|
+
editingMapping: computed,
|
|
144
|
+
endFieldEdit: action.bound,
|
|
137
145
|
setWritePolicy: action.bound,
|
|
138
146
|
onUploadContinue: action.bound,
|
|
139
147
|
goToUpload: action.bound,
|
|
@@ -142,7 +150,10 @@ export class ImportState {
|
|
|
142
150
|
setNewFieldConfig: action.bound,
|
|
143
151
|
beginFieldEdit: action.bound,
|
|
144
152
|
applyFieldEdit: action.bound,
|
|
153
|
+
applyDataExtensionFieldEdit: action.bound,
|
|
145
154
|
close: action.bound,
|
|
155
|
+
done: action.bound,
|
|
156
|
+
downloadFailedRows: action.bound,
|
|
146
157
|
onFileSelected: action.bound,
|
|
147
158
|
startImport: action.bound,
|
|
148
159
|
reset: action.bound,
|
|
@@ -156,19 +167,33 @@ export class ImportState {
|
|
|
156
167
|
}
|
|
157
168
|
/** Wix Data collection the import writes into (unset for platformized). */
|
|
158
169
|
get collectionId() {
|
|
159
|
-
|
|
170
|
+
const ds = this.config.dataSource;
|
|
171
|
+
return 'collectionId' in ds ? ds.collectionId : undefined;
|
|
160
172
|
}
|
|
161
173
|
get destination() {
|
|
162
|
-
|
|
174
|
+
const ds = this.config.dataSource;
|
|
175
|
+
return 'destination' in ds ? ds.destination : undefined;
|
|
163
176
|
}
|
|
164
177
|
get backup() {
|
|
165
178
|
return this.config.backup;
|
|
166
179
|
}
|
|
180
|
+
/** CMS create-fields callback, present only on the `collectionId` branch. */
|
|
167
181
|
get updateSchema() {
|
|
168
|
-
|
|
182
|
+
const ds = this.config.dataSource;
|
|
183
|
+
return 'collectionId' in ds ? ds.updateSchema : undefined;
|
|
169
184
|
}
|
|
170
|
-
|
|
171
|
-
|
|
185
|
+
/** DataExtension create-fields callback, present only on the `destination` branch. */
|
|
186
|
+
get createDataExtensionFields() {
|
|
187
|
+
const ds = this.config.dataSource;
|
|
188
|
+
return 'destination' in ds ? ds.createFields : undefined;
|
|
189
|
+
}
|
|
190
|
+
/** DataExtension field-management for the editor, on the `destination` branch. */
|
|
191
|
+
get dataExtensionFieldManagement() {
|
|
192
|
+
const ds = this.config.dataSource;
|
|
193
|
+
return 'destination' in ds ? ds.fieldManagement : undefined;
|
|
194
|
+
}
|
|
195
|
+
get jobApplicationErrorMap() {
|
|
196
|
+
return this.config.jobApplicationErrorMap;
|
|
172
197
|
}
|
|
173
198
|
// The collection's id-field key; a schema-backed config may report a non-`_id`
|
|
174
199
|
// identifier. Defaults to the Wix Data `_id`.
|
|
@@ -216,6 +241,30 @@ export class ImportState {
|
|
|
216
241
|
.filter((m) => m?.kind === 'existing')
|
|
217
242
|
.map((m) => m.fieldId));
|
|
218
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Required destination fields that no CSV column is mapped to (CAIRO-4452).
|
|
246
|
+
* A required field is an existing collection field, so only an `existing`
|
|
247
|
+
* mapping can satisfy it — a "create new field" column can't. Covers both
|
|
248
|
+
* ways a required field ends up unmapped: the CSV has no matching column at
|
|
249
|
+
* all, or the user set its column to "don't import". Drives the footer
|
|
250
|
+
* indicator and blocks `startImport`.
|
|
251
|
+
*/
|
|
252
|
+
get missingRequiredFields() {
|
|
253
|
+
return this.fields.filter((field) => field.required &&
|
|
254
|
+
field.id != null &&
|
|
255
|
+
!this.selectedFieldIds.has(field.id));
|
|
256
|
+
}
|
|
257
|
+
get hasMissingRequiredFields() {
|
|
258
|
+
return this.missingRequiredFields.length > 0;
|
|
259
|
+
}
|
|
260
|
+
// Comma-separated display names of the missing required fields, for the block
|
|
261
|
+
// toast. Falls back to the field id when a field carries no header.
|
|
262
|
+
_missingRequiredFieldNames() {
|
|
263
|
+
return this.missingRequiredFields
|
|
264
|
+
.map((field) => field.header || field.id || '')
|
|
265
|
+
.filter(Boolean)
|
|
266
|
+
.join(', ');
|
|
267
|
+
}
|
|
219
268
|
/**
|
|
220
269
|
* The CSV header currently mapped to the collection's `_id` field, or `null`
|
|
221
270
|
* when no column targets it. Drives both the "When items share the same ID"
|
|
@@ -232,12 +281,13 @@ export class ImportState {
|
|
|
232
281
|
}
|
|
233
282
|
/**
|
|
234
283
|
* Whether columns may be mapped to a brand-new field created during import.
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
284
|
+
* The opt-in is the presence of the active `dataSource` branch's create
|
|
285
|
+
* callback — `updateSchema` (CMS) or `createFields` (DataExtension). A source
|
|
286
|
+
* that can't create fields (e.g. no create permission) supplies neither, so
|
|
287
|
+
* columns fall back to "don't import" (see `getDefaultMapping`).
|
|
238
288
|
*/
|
|
239
289
|
get canCreateNewField() {
|
|
240
|
-
return this.
|
|
290
|
+
return (this.updateSchema != null || this.createDataExtensionFields != null);
|
|
241
291
|
}
|
|
242
292
|
/**
|
|
243
293
|
* Whether the write-policy (id-collision) choice is meaningful for this import.
|
|
@@ -269,6 +319,8 @@ export class ImportState {
|
|
|
269
319
|
this.result = {
|
|
270
320
|
succeeded: 0,
|
|
271
321
|
failed: 0,
|
|
322
|
+
created: 0,
|
|
323
|
+
updated: 0,
|
|
272
324
|
};
|
|
273
325
|
this._pendingFieldsMap = undefined;
|
|
274
326
|
this.isBackingUp = false;
|
|
@@ -279,11 +331,10 @@ export class ImportState {
|
|
|
279
331
|
}
|
|
280
332
|
async close() {
|
|
281
333
|
// While on the progress step we treat close as a cancel — terminate the job
|
|
282
|
-
// and restore the backup — UNLESS the import already succeeded. On
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
// lets the success path do its collection refresh.
|
|
334
|
+
// and restore the backup — UNLESS the import already succeeded. On success
|
|
335
|
+
// `startImport` moves straight to the 'summary' step, so a close from there
|
|
336
|
+
// just closes the modal (the summary's own `done` is the normal exit); this
|
|
337
|
+
// guard covers only closing mid-import.
|
|
287
338
|
if (this.currentStep === 'progress' && !this.importStatus.isSuccess) {
|
|
288
339
|
this._cancelled = true;
|
|
289
340
|
this._abortController?.abort();
|
|
@@ -306,6 +357,13 @@ export class ImportState {
|
|
|
306
357
|
}
|
|
307
358
|
this.importModalState.close();
|
|
308
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Closes the summary step at the end of a completed import. The collection was
|
|
362
|
+
* already refreshed when the summary opened, so this only dismisses the modal.
|
|
363
|
+
*/
|
|
364
|
+
done() {
|
|
365
|
+
this.importModalState.close();
|
|
366
|
+
}
|
|
309
367
|
async _restoreBackup() {
|
|
310
368
|
const restoreFn = this.restoreFn;
|
|
311
369
|
if (!restoreFn) {
|
|
@@ -474,50 +532,44 @@ export class ImportState {
|
|
|
474
532
|
},
|
|
475
533
|
});
|
|
476
534
|
}
|
|
477
|
-
_showPartialSuccessToast() {
|
|
478
|
-
this.showToast?.({
|
|
479
|
-
type: 'WARNING',
|
|
480
|
-
message: this.translate('cairo.import.partialSuccessToast', {
|
|
481
|
-
failed: String(this.result.failed),
|
|
482
|
-
imported: String(this.result.succeeded),
|
|
483
|
-
}),
|
|
484
|
-
action: {
|
|
485
|
-
text: this.translate('cairo.import.downloadFailedRows'),
|
|
486
|
-
onClick: () => this._downloadFailedRows(),
|
|
487
|
-
removeToastOnClick: true,
|
|
488
|
-
},
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
535
|
/**
|
|
492
|
-
* Builds a
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
536
|
+
* Builds a re-importable CSV of the rows the server rejected and hands it to
|
|
537
|
+
* `downloadCsv`. Each failed row is prefixed with a leading "why it failed"
|
|
538
|
+
* column holding the server's error message, so the user can see and fix each
|
|
539
|
+
* reason. The failed rows (and their reasons) come from the job's movement
|
|
540
|
+
* logs, mapped back to the parsed `csvRows` we still hold in memory.
|
|
496
541
|
*/
|
|
497
|
-
async
|
|
542
|
+
async downloadFailedRows() {
|
|
498
543
|
const jobId = this._jobId;
|
|
499
544
|
if (!jobId) {
|
|
500
545
|
return;
|
|
501
546
|
}
|
|
502
|
-
let
|
|
547
|
+
let failures;
|
|
503
548
|
try {
|
|
504
|
-
|
|
549
|
+
failures = await this._fetchFailedRows(jobId);
|
|
505
550
|
}
|
|
506
551
|
catch (error) {
|
|
507
552
|
console.error('Fetching failed rows report failed', error);
|
|
508
553
|
return;
|
|
509
554
|
}
|
|
510
|
-
const
|
|
511
|
-
.map((index) =>
|
|
555
|
+
const rows = failures
|
|
556
|
+
.map(({ index, reason }) => {
|
|
557
|
+
const row = this.csvRows[index];
|
|
558
|
+
return row != null ? [reason, ...row] : null;
|
|
559
|
+
})
|
|
512
560
|
.filter((row) => row != null);
|
|
513
|
-
if (
|
|
561
|
+
if (rows.length === 0) {
|
|
514
562
|
return;
|
|
515
563
|
}
|
|
516
|
-
const
|
|
564
|
+
const headers = [
|
|
565
|
+
this.translate('cairo.import.failureReasonColumn'),
|
|
566
|
+
...this.csvHeaders,
|
|
567
|
+
];
|
|
568
|
+
const content = unparseCsv(headers, rows);
|
|
517
569
|
this.downloadCsv?.(content, `failed-${this.fileName || 'import.csv'}`);
|
|
518
570
|
}
|
|
519
|
-
// Pages through the job's movement logs and returns
|
|
520
|
-
// the
|
|
571
|
+
// Pages through the job's movement logs and returns, for each row that failed,
|
|
572
|
+
// its `csvRows` index and the server's failure reason. data-hub's CSV decoder
|
|
521
573
|
// (wix-data-hub: packages/wix-data-hub-executor-c/src/plugins/formats/csv/
|
|
522
574
|
// csv-binary-decoder.ts) parses with the `csv-parse` library (`columns: true`)
|
|
523
575
|
// and sets `sourceItemId = "row ${++index + 1}"`, incrementing once **per
|
|
@@ -526,8 +578,8 @@ export class ImportState {
|
|
|
526
578
|
// `csvRows[0]`, i.e. `csvRows[N - 2]`. Because `csv-parse` is RFC-4180,
|
|
527
579
|
// quoted cells containing embedded newlines are a single record, so they do
|
|
528
580
|
// NOT shift the numbering. Out-of-range indices are guarded below.
|
|
529
|
-
async
|
|
530
|
-
const
|
|
581
|
+
async _fetchFailedRows(jobId) {
|
|
582
|
+
const failures = [];
|
|
531
583
|
let cursor;
|
|
532
584
|
do {
|
|
533
585
|
const { logs, pagingMetadata } = await this.hubJobClient.listMovementLogs(jobId, cursor ? { cursor } : undefined);
|
|
@@ -540,13 +592,13 @@ export class ImportState {
|
|
|
540
592
|
if (Number.isInteger(index) &&
|
|
541
593
|
index >= 0 &&
|
|
542
594
|
index < this.csvRows.length) {
|
|
543
|
-
|
|
595
|
+
failures.push({ index, reason: log.failure.description ?? '' });
|
|
544
596
|
}
|
|
545
597
|
}
|
|
546
598
|
cursor =
|
|
547
599
|
(pagingMetadata?.hasNext && pagingMetadata.cursors?.next) || undefined;
|
|
548
600
|
} while (cursor);
|
|
549
|
-
return
|
|
601
|
+
return failures;
|
|
550
602
|
}
|
|
551
603
|
setMapping(header, fieldId) {
|
|
552
604
|
this.mappingByHeader = {
|
|
@@ -584,23 +636,51 @@ export class ImportState {
|
|
|
584
636
|
};
|
|
585
637
|
}
|
|
586
638
|
/**
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
639
|
+
* Opens the field editor for a column's pending new field: sets
|
|
640
|
+
* `editingHeader`, which the source's editor component watches to open and
|
|
641
|
+
* prefill itself. No-op if the column isn't mapped to a new field.
|
|
590
642
|
*/
|
|
591
643
|
beginFieldEdit(header) {
|
|
592
|
-
this.
|
|
644
|
+
if (this.mappingByHeader[header]?.kind === 'new') {
|
|
645
|
+
this.editingHeader = header;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/** Closes the field editor (clears `editingHeader`). */
|
|
649
|
+
endFieldEdit() {
|
|
650
|
+
this.editingHeader = null;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* The `new` mapping currently being edited, or `null` when the editor is
|
|
654
|
+
* closed. The editor component reads this to prefill its form.
|
|
655
|
+
*/
|
|
656
|
+
get editingMapping() {
|
|
657
|
+
if (this.editingHeader == null) {
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
const mapping = this.mappingByHeader[this.editingHeader];
|
|
661
|
+
return mapping?.kind === 'new' ? mapping : null;
|
|
593
662
|
}
|
|
594
663
|
/**
|
|
595
|
-
* `onSubmit` handler for the field editor: maps the edited field spec into
|
|
596
|
-
* column being edited (set via `beginFieldEdit`). No-op if nothing is being
|
|
664
|
+
* `onSubmit` handler for the CMS field editor: maps the edited field spec into
|
|
665
|
+
* the column being edited (set via `beginFieldEdit`). No-op if nothing is being
|
|
597
666
|
* edited.
|
|
598
667
|
*/
|
|
599
668
|
applyFieldEdit(field) {
|
|
600
|
-
if (this.
|
|
669
|
+
if (this.editingHeader == null) {
|
|
601
670
|
return;
|
|
602
671
|
}
|
|
603
|
-
this.setNewFieldConfig(this.
|
|
672
|
+
this.setNewFieldConfig(this.editingHeader, toFieldEdit(field));
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* `onSubmit` handler for the DataExtension field editor: maps the edited
|
|
676
|
+
* DataExtension draft (which carries per-audience permissions) into the column
|
|
677
|
+
* being edited. The `destination`-branch counterpart to `applyFieldEdit`.
|
|
678
|
+
*/
|
|
679
|
+
applyDataExtensionFieldEdit(draft) {
|
|
680
|
+
if (this.editingHeader == null) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
this.setNewFieldConfig(this.editingHeader, toDataExtensionFieldEdit(draft));
|
|
604
684
|
}
|
|
605
685
|
/**
|
|
606
686
|
* Runs the server-side import: align the CSV to the resolved field keys,
|
|
@@ -645,21 +725,30 @@ export class ImportState {
|
|
|
645
725
|
// The job ran server-side, so a failure arrives on a 200 (the poll
|
|
646
726
|
// succeeded; the job didn't) with no HTTP error to carry the application
|
|
647
727
|
// error. Build a synthetic, HTTP-error-shaped error (the error handler
|
|
648
|
-
// reads the code from `response.data.details.applicationError`)
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
// Without a
|
|
656
|
-
// generic message.
|
|
728
|
+
// reads the code from `response.data.details.applicationError`), then
|
|
729
|
+
// resolve it through the consumer's per-code map via `withErrorHandler` —
|
|
730
|
+
// the same way as `fetchData` etc. `job.error` is the consumer's
|
|
731
|
+
// application error (their import business logic), so only they can map
|
|
732
|
+
// its codes; they supply just the code→message map and we do the
|
|
733
|
+
// wrapping. `withErrorHandler` attaches the map to the error and
|
|
734
|
+
// re-rejects it, so the progress step resolves a consumer-specific
|
|
735
|
+
// message via `getResolvedError`. Without a map the same error still
|
|
736
|
+
// throws, falling back to the generic message.
|
|
657
737
|
const error = Object.assign(new Error(`ImportJob.${job.error?.code ?? 'failed'}`), { response: { data: { details: { applicationError: job.error } } } });
|
|
658
|
-
const
|
|
659
|
-
if (
|
|
660
|
-
//
|
|
661
|
-
// `
|
|
662
|
-
|
|
738
|
+
const jobApplicationErrorMap = this.jobApplicationErrorMap;
|
|
739
|
+
if (jobApplicationErrorMap) {
|
|
740
|
+
// Adapt the consumer's code→message map into the error handler's
|
|
741
|
+
// `errorCodesMap`: each entry, when its code matches the job's
|
|
742
|
+
// application error, resolves with that error. `withErrorHandler`
|
|
743
|
+
// attaches the resolved message to `error` and re-rejects it, so the
|
|
744
|
+
// progress step surfaces it via `getResolvedError` — the consumer only
|
|
745
|
+
// supplies the map, not the wrapping.
|
|
746
|
+
const appError = (job.error ?? {});
|
|
747
|
+
const errorCodesMap = Object.fromEntries(Object.entries(jobApplicationErrorMap).map(([code, resolve]) => [
|
|
748
|
+
code,
|
|
749
|
+
() => resolve(appError),
|
|
750
|
+
]));
|
|
751
|
+
await this.errorHandler.withErrorHandler(() => Promise.reject(error), errorCodesMap);
|
|
663
752
|
}
|
|
664
753
|
throw error;
|
|
665
754
|
}
|
|
@@ -688,13 +777,21 @@ export class ImportState {
|
|
|
688
777
|
const succeeded = progress?.itemsSuccessful ?? 0;
|
|
689
778
|
const failed = progress?.itemsRejected ?? 0;
|
|
690
779
|
const current = progress?.current ?? succeeded + failed;
|
|
780
|
+
// The server splits successful items into created (new) and updated
|
|
781
|
+
// (existing) — the summary step reports each. Absent on older jobs, in
|
|
782
|
+
// which case both stay 0 and only the total `succeeded` is shown.
|
|
783
|
+
const created = progress?.itemsSuccessfulInfo?.itemsCreated ?? 0;
|
|
784
|
+
const updated = progress?.itemsSuccessfulInfo?.itemsUpdated ?? 0;
|
|
691
785
|
// `totalCount` is fixed up front in `startImport` from the CSV row count
|
|
692
786
|
// (one uploaded row per CSV row); polling only advances `processedCount`.
|
|
693
787
|
runInAction(() => {
|
|
694
788
|
this.processedCount = Math.min(current, this.totalCount);
|
|
695
|
-
this.result = { succeeded, failed };
|
|
789
|
+
this.result = { succeeded, failed, created, updated };
|
|
696
790
|
});
|
|
697
791
|
}
|
|
792
|
+
_lingerOnCompletion() {
|
|
793
|
+
return new Promise((resolve) => setTimeout(resolve, COMPLETION_LINGER_MS));
|
|
794
|
+
}
|
|
698
795
|
async _runBackup() {
|
|
699
796
|
const backup = this.backup;
|
|
700
797
|
if (!backup) {
|
|
@@ -727,9 +824,6 @@ export class ImportState {
|
|
|
727
824
|
});
|
|
728
825
|
return false;
|
|
729
826
|
}
|
|
730
|
-
_lingerOnCompletion() {
|
|
731
|
-
return new Promise((resolve) => setTimeout(resolve, COMPLETION_LINGER_MS));
|
|
732
|
-
}
|
|
733
827
|
/**
|
|
734
828
|
* Resolves every column to the final destination key used in the upload
|
|
735
829
|
* payload: an existing field id, a new field's (Cairo-decided) key, or `null`
|
|
@@ -739,13 +833,16 @@ export class ImportState {
|
|
|
739
833
|
*/
|
|
740
834
|
async _resolveFieldMapping() {
|
|
741
835
|
const resolved = {};
|
|
742
|
-
|
|
836
|
+
// Pending new columns with their deduped key + display name. Source-neutral;
|
|
837
|
+
// the per-source field def is built from these in `_createPendingFields`.
|
|
838
|
+
const pending = [];
|
|
743
839
|
// Seed with existing field ids/display names so derived keys and names never
|
|
744
840
|
// collide with them or with each other.
|
|
745
841
|
const usedKeys = new Set(this.fields.map((field) => field.id).filter((id) => !!id));
|
|
746
842
|
const usedNames = new Set(this.fields
|
|
747
843
|
.map((field) => field.header)
|
|
748
844
|
.filter((header) => !!header));
|
|
845
|
+
const canCreate = this.canCreateNewField;
|
|
749
846
|
for (const header of this.csvHeaders) {
|
|
750
847
|
const mapping = this.mappingByHeader[header];
|
|
751
848
|
if (mapping == null) {
|
|
@@ -754,41 +851,75 @@ export class ImportState {
|
|
|
754
851
|
else if (mapping.kind === 'existing') {
|
|
755
852
|
resolved[header] = mapping.fieldId;
|
|
756
853
|
}
|
|
757
|
-
else if (
|
|
854
|
+
else if (canCreate) {
|
|
758
855
|
// The user's name override (from the field editor) seeds the unique
|
|
759
856
|
// name/key derivation; otherwise fall back to the column header.
|
|
760
857
|
const baseName = mapping.displayName?.trim() || header;
|
|
761
858
|
const { displayName, key } = uniqueFieldNameAndKey(baseName, usedNames, usedKeys);
|
|
762
859
|
usedKeys.add(key);
|
|
763
860
|
usedNames.add(displayName);
|
|
764
|
-
|
|
765
|
-
key,
|
|
766
|
-
type: mapping.fieldType,
|
|
767
|
-
displayName,
|
|
768
|
-
...(mapping.config && { config: mapping.config }),
|
|
769
|
-
});
|
|
861
|
+
pending.push({ key, displayName, mapping });
|
|
770
862
|
resolved[header] = key;
|
|
771
863
|
}
|
|
772
864
|
else {
|
|
773
|
-
// "new" mapping
|
|
865
|
+
// "new" mapping with no create callback on this source — nothing to map into.
|
|
774
866
|
resolved[header] = null;
|
|
775
867
|
}
|
|
776
868
|
}
|
|
777
|
-
if (
|
|
869
|
+
if (pending.length > 0) {
|
|
778
870
|
// Stash the returned fields map to merge into the table on success; kept off
|
|
779
|
-
// the observable until then so columns don't update mid-import.
|
|
780
|
-
|
|
781
|
-
// wipe every column — treat a missing/empty return as "nothing to merge".
|
|
782
|
-
const updated = await this.updateSchema(newFields);
|
|
783
|
-
this._pendingFieldsMap =
|
|
784
|
-
updated && Object.keys(updated).length > 0 ? updated : undefined;
|
|
871
|
+
// the observable until then so columns don't update mid-import.
|
|
872
|
+
this._pendingFieldsMap = await this._createPendingFields(pending);
|
|
785
873
|
}
|
|
786
874
|
return resolved;
|
|
787
875
|
}
|
|
876
|
+
/**
|
|
877
|
+
* Creates the pending new fields via the active `dataSource` branch's callback
|
|
878
|
+
* — `updateSchema` (CMS) or `createFields` (DataExtension) — building the
|
|
879
|
+
* source-specific field def from the shared, already-deduped pending columns.
|
|
880
|
+
* The callback may throw; `startImport` surfaces that as the error state.
|
|
881
|
+
*/
|
|
882
|
+
async _createPendingFields(pending) {
|
|
883
|
+
const updateSchema = this.updateSchema;
|
|
884
|
+
const createDataExtensionFields = this.createDataExtensionFields;
|
|
885
|
+
let updated;
|
|
886
|
+
if (updateSchema) {
|
|
887
|
+
updated = await updateSchema(pending.map(({ key, displayName, mapping }) => ({
|
|
888
|
+
key,
|
|
889
|
+
type: mapping.fieldType,
|
|
890
|
+
displayName,
|
|
891
|
+
...(mapping.config && { config: mapping.config }),
|
|
892
|
+
})));
|
|
893
|
+
}
|
|
894
|
+
else if (createDataExtensionFields) {
|
|
895
|
+
// The DataExtension source builds its own field shape from the neutral
|
|
896
|
+
// pending columns — no DataExtension-specific def-building in core.
|
|
897
|
+
updated = await createDataExtensionFields(pending);
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
return undefined;
|
|
901
|
+
}
|
|
902
|
+
// Guard the empty map: SchemaState.mergeFields is a full replace, so merging
|
|
903
|
+
// `{}` would wipe every column — treat a missing/empty return as "nothing to
|
|
904
|
+
// merge".
|
|
905
|
+
return updated && Object.keys(updated).length > 0 ? updated : undefined;
|
|
906
|
+
}
|
|
788
907
|
async startImport() {
|
|
789
908
|
if (!this.hasMappings) {
|
|
790
909
|
return;
|
|
791
910
|
}
|
|
911
|
+
// Block import while any required destination field is unmapped and tell the
|
|
912
|
+
// user which (CAIRO-4452). The footer already flags this; the Import button
|
|
913
|
+
// stays enabled, so this is where the click is stopped.
|
|
914
|
+
if (this.hasMissingRequiredFields) {
|
|
915
|
+
this.showToast?.({
|
|
916
|
+
type: 'ERROR',
|
|
917
|
+
message: this.translate('cairo.import.missingRequiredMessage', {
|
|
918
|
+
fields: this._missingRequiredFieldNames(),
|
|
919
|
+
}),
|
|
920
|
+
});
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
792
923
|
this._cancelled = false;
|
|
793
924
|
this._abortController = new AbortController();
|
|
794
925
|
// One uploaded row per CSV row (the rewrite drops columns, not rows), so the
|
|
@@ -805,6 +936,8 @@ export class ImportState {
|
|
|
805
936
|
this.result = {
|
|
806
937
|
succeeded: 0,
|
|
807
938
|
failed: 0,
|
|
939
|
+
created: 0,
|
|
940
|
+
updated: 0,
|
|
808
941
|
};
|
|
809
942
|
this._jobId = null;
|
|
810
943
|
// Drop any fields map from a prior attempt so a no-new-fields "Try again"
|
|
@@ -857,28 +990,30 @@ export class ImportState {
|
|
|
857
990
|
...queryStatus.success,
|
|
858
991
|
data: this.result,
|
|
859
992
|
};
|
|
993
|
+
// The job reports counts only when it finishes, so the bar sat at 0 for
|
|
994
|
+
// the whole import (CAIRO-4476). Snap it to 100% and hold it (below)
|
|
995
|
+
// before the summary step replaces the progress step, so the user sees
|
|
996
|
+
// the import reach completion.
|
|
997
|
+
this.processedCount = this.totalCount;
|
|
860
998
|
});
|
|
861
|
-
//
|
|
862
|
-
//
|
|
863
|
-
// the
|
|
864
|
-
|
|
865
|
-
if (this.
|
|
866
|
-
|
|
867
|
-
if (this._cancelled) {
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
999
|
+
// Keep the completed bar on screen briefly before moving on. `importStatus`
|
|
1000
|
+
// is already success, so a close during this window just dismisses the
|
|
1001
|
+
// modal (the `close()` guard doesn't treat it as a cancel).
|
|
1002
|
+
await this._lingerOnCompletion();
|
|
1003
|
+
if (this._cancelled) {
|
|
1004
|
+
return;
|
|
870
1005
|
}
|
|
871
1006
|
// Success path only. `_pendingFieldsMap` holds this run's returned map — it's
|
|
872
1007
|
// cleared at the start of `startImport` (the stale-guard) and set by
|
|
873
1008
|
// `_resolveFieldMapping`; snapshotting here keeps the merge success-gated.
|
|
874
1009
|
createdFields = this._pendingFieldsMap;
|
|
875
1010
|
runInAction(() => {
|
|
876
|
-
//
|
|
877
|
-
//
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
this.
|
|
1011
|
+
// Import finished: show the summary step (created / updated / failed
|
|
1012
|
+
// counts, plus a "Download CSV" of the failed rows when any failed).
|
|
1013
|
+
// The modal stays open; the user dismisses it from the summary via
|
|
1014
|
+
// `done`. Refresh the collection now so the table behind the modal is
|
|
1015
|
+
// up to date by the time it closes.
|
|
1016
|
+
this.currentStep = 'summary';
|
|
882
1017
|
this.collection.clearResultAndMoveToStart({ force: true });
|
|
883
1018
|
});
|
|
884
1019
|
}
|