@wix/bex-core 2.332.0 → 2.336.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/assets/locale/messages_ar.json +13 -2
- package/dist/cjs/assets/locale/messages_bg.json +13 -2
- package/dist/cjs/assets/locale/messages_ca.json +13 -2
- package/dist/cjs/assets/locale/messages_cs.json +11 -0
- package/dist/cjs/assets/locale/messages_da.json +11 -0
- package/dist/cjs/assets/locale/messages_de.json +11 -0
- package/dist/cjs/assets/locale/messages_el.json +13 -2
- package/dist/cjs/assets/locale/messages_en.json +13 -2
- package/dist/cjs/assets/locale/messages_es.json +11 -0
- package/dist/cjs/assets/locale/messages_fi.json +13 -2
- package/dist/cjs/assets/locale/messages_fr.json +11 -0
- package/dist/cjs/assets/locale/messages_he.json +13 -2
- package/dist/cjs/assets/locale/messages_hi.json +11 -0
- package/dist/cjs/assets/locale/messages_hu.json +13 -2
- package/dist/cjs/assets/locale/messages_id.json +11 -0
- package/dist/cjs/assets/locale/messages_it.json +11 -0
- package/dist/cjs/assets/locale/messages_ja.json +11 -0
- package/dist/cjs/assets/locale/messages_ko.json +11 -0
- package/dist/cjs/assets/locale/messages_lt.json +13 -2
- package/dist/cjs/assets/locale/messages_ms.json +13 -2
- package/dist/cjs/assets/locale/messages_nl.json +11 -0
- package/dist/cjs/assets/locale/messages_no.json +11 -0
- package/dist/cjs/assets/locale/messages_pl.json +11 -0
- package/dist/cjs/assets/locale/messages_pt.json +11 -0
- package/dist/cjs/assets/locale/messages_ro.json +13 -2
- package/dist/cjs/assets/locale/messages_ru.json +11 -0
- package/dist/cjs/assets/locale/messages_sk.json +13 -2
- package/dist/cjs/assets/locale/messages_sl.json +13 -2
- package/dist/cjs/assets/locale/messages_sv.json +11 -0
- package/dist/cjs/assets/locale/messages_th.json +11 -0
- package/dist/cjs/assets/locale/messages_tl.json +13 -2
- package/dist/cjs/assets/locale/messages_tr.json +11 -0
- package/dist/cjs/assets/locale/messages_uk.json +11 -0
- package/dist/cjs/assets/locale/messages_vi.json +11 -0
- package/dist/cjs/assets/locale/messages_zh.json +11 -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 +6 -6
- package/dist/cjs/hooks/index.js.map +1 -1
- package/dist/cjs/hooks/useImport.js +4 -0
- package/dist/cjs/hooks/useImport.js.map +1 -1
- package/dist/cjs/hooks/useSchemaSource.js +11 -0
- package/dist/cjs/hooks/useSchemaSource.js.map +1 -0
- package/dist/cjs/index.js +4 -9
- 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 +415 -136
- package/dist/cjs/state/ImportState/ImportState.js.map +1 -1
- package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js +417 -82
- package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -1
- package/dist/cjs/state/ImportState/__tests__/buildImportConfig.spec.js +156 -0
- package/dist/cjs/state/ImportState/__tests__/buildImportConfig.spec.js.map +1 -0
- package/dist/cjs/state/ImportState/detectFieldType.js +41 -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/state/SourceState.js +150 -0
- package/dist/cjs/state/SourceState.js.map +1 -0
- package/dist/cjs/state/index.js +7 -0
- package/dist/cjs/state/index.js.map +1 -1
- package/dist/cjs/testkit/createInMemorySchemaSource.js +190 -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/Field.js.map +1 -1
- package/dist/cjs/types/Schema.js.map +1 -1
- package/dist/cjs/types/SchemaSource.js +31 -21
- package/dist/cjs/types/SchemaSource.js.map +1 -1
- package/dist/esm/assets/locale/messages_ar.json +13 -2
- package/dist/esm/assets/locale/messages_bg.json +13 -2
- package/dist/esm/assets/locale/messages_ca.json +13 -2
- package/dist/esm/assets/locale/messages_cs.json +11 -0
- package/dist/esm/assets/locale/messages_da.json +11 -0
- package/dist/esm/assets/locale/messages_de.json +11 -0
- package/dist/esm/assets/locale/messages_el.json +13 -2
- package/dist/esm/assets/locale/messages_en.json +13 -2
- package/dist/esm/assets/locale/messages_es.json +11 -0
- package/dist/esm/assets/locale/messages_fi.json +13 -2
- package/dist/esm/assets/locale/messages_fr.json +11 -0
- package/dist/esm/assets/locale/messages_he.json +13 -2
- package/dist/esm/assets/locale/messages_hi.json +11 -0
- package/dist/esm/assets/locale/messages_hu.json +13 -2
- package/dist/esm/assets/locale/messages_id.json +11 -0
- package/dist/esm/assets/locale/messages_it.json +11 -0
- package/dist/esm/assets/locale/messages_ja.json +11 -0
- package/dist/esm/assets/locale/messages_ko.json +11 -0
- package/dist/esm/assets/locale/messages_lt.json +13 -2
- package/dist/esm/assets/locale/messages_ms.json +13 -2
- package/dist/esm/assets/locale/messages_nl.json +11 -0
- package/dist/esm/assets/locale/messages_no.json +11 -0
- package/dist/esm/assets/locale/messages_pl.json +11 -0
- package/dist/esm/assets/locale/messages_pt.json +11 -0
- package/dist/esm/assets/locale/messages_ro.json +13 -2
- package/dist/esm/assets/locale/messages_ru.json +11 -0
- package/dist/esm/assets/locale/messages_sk.json +13 -2
- package/dist/esm/assets/locale/messages_sl.json +13 -2
- package/dist/esm/assets/locale/messages_sv.json +11 -0
- package/dist/esm/assets/locale/messages_th.json +11 -0
- package/dist/esm/assets/locale/messages_tl.json +13 -2
- package/dist/esm/assets/locale/messages_tr.json +11 -0
- package/dist/esm/assets/locale/messages_uk.json +11 -0
- package/dist/esm/assets/locale/messages_vi.json +11 -0
- package/dist/esm/assets/locale/messages_zh.json +11 -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 +1 -1
- package/dist/esm/hooks/index.js.map +1 -1
- package/dist/esm/hooks/useImport.js +3 -1
- package/dist/esm/hooks/useImport.js.map +1 -1
- package/dist/esm/hooks/useSchemaSource.js +7 -0
- package/dist/esm/hooks/useSchemaSource.js.map +1 -0
- package/dist/esm/index.js +1 -2
- 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 +306 -113
- package/dist/esm/state/ImportState/ImportState.js.map +1 -1
- package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js +297 -70
- package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js.map +1 -1
- package/dist/esm/state/ImportState/__tests__/buildImportConfig.spec.js +100 -0
- package/dist/esm/state/ImportState/__tests__/buildImportConfig.spec.js.map +1 -0
- package/dist/esm/state/ImportState/detectFieldType.js +34 -15
- 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/state/SourceState.js +131 -0
- package/dist/esm/state/SourceState.js.map +1 -0
- package/dist/esm/state/index.js +1 -0
- package/dist/esm/state/index.js.map +1 -1
- package/dist/esm/testkit/createInMemorySchemaSource.js +141 -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 +21 -14
- package/dist/esm/types/SchemaSource.js.map +1 -1
- package/dist/types/hooks/index.d.ts +1 -1
- package/dist/types/hooks/index.d.ts.map +1 -1
- package/dist/types/hooks/useImport.d.ts +9 -3
- package/dist/types/hooks/useImport.d.ts.map +1 -1
- package/dist/types/hooks/useSchemaSource.d.ts +4 -0
- package/dist/types/hooks/useSchemaSource.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -3
- 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 +208 -65
- package/dist/types/state/ImportState/ImportState.d.ts.map +1 -1
- package/dist/types/state/ImportState/__tests__/buildImportConfig.spec.d.ts +2 -0
- package/dist/types/state/ImportState/__tests__/buildImportConfig.spec.d.ts.map +1 -0
- package/dist/types/state/ImportState/detectFieldType.d.ts +5 -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/state/SourceState.d.ts +39 -0
- package/dist/types/state/SourceState.d.ts.map +1 -0
- package/dist/types/state/index.d.ts +1 -0
- package/dist/types/state/index.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 +43 -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/Field.d.ts +3 -0
- package/dist/types/types/Field.d.ts.map +1 -1
- package/dist/types/types/Schema.d.ts +10 -6
- package/dist/types/types/Schema.d.ts.map +1 -1
- package/dist/types/types/SchemaSource.d.ts +30 -13
- 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
|
@@ -6,17 +6,71 @@ import { parseCsv, unparseCsv } from './parseCsv';
|
|
|
6
6
|
import { rewriteParsedCsvForImport } from './rewriteCsv';
|
|
7
7
|
import { createHubJobClient } from './hubJobClient';
|
|
8
8
|
import { pollImportJob } from './importJobPoller';
|
|
9
|
-
import { detectColumnType, TYPE_SAMPLE_SIZE, } from './detectFieldType';
|
|
10
|
-
import { toFieldEdit } from './fieldEdit';
|
|
9
|
+
import { detectColumnType, toPatternsFieldType, TYPE_SAMPLE_SIZE, } from './detectFieldType';
|
|
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
|
-
//
|
|
17
|
-
// the
|
|
18
|
-
//
|
|
19
|
-
|
|
16
|
+
// Builds the Field to create from a pending new column: import decides the key,
|
|
17
|
+
// and the detected NewFieldType maps to the schema's PatternsFieldType. Import
|
|
18
|
+
// only ever creates non-reference fields, so the cast is safe.
|
|
19
|
+
const toField = (def) => ({
|
|
20
|
+
id: def.key,
|
|
21
|
+
displayName: def.displayName,
|
|
22
|
+
type: toPatternsFieldType(def.type),
|
|
23
|
+
...(def.config?.validation && { validation: def.config.validation }),
|
|
24
|
+
...(def.config?.isPii && { isPii: true }),
|
|
25
|
+
...(def.config?.helpText && { helpText: def.config.helpText }),
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Assembles a `ResolvedImportConfig` from the table's live `fieldsSource` and the
|
|
29
|
+
* consumer `inputs`. The schema-derived members are getters so they read the
|
|
30
|
+
* source live — fields import creates and fields added through the
|
|
31
|
+
* field-management panel show up immediately. Empty before a source attaches;
|
|
32
|
+
* nothing imports until then.
|
|
33
|
+
*
|
|
34
|
+
* Exported for tests.
|
|
35
|
+
*/
|
|
36
|
+
export const buildImportConfig = (fieldsSource, collection, inputs = {}) => {
|
|
37
|
+
const { destination, fields = [], backup, idFieldKey, jobApplicationErrorMap, } = inputs;
|
|
38
|
+
return {
|
|
39
|
+
get fields() {
|
|
40
|
+
const seen = new Set(fields.map((f) => f.id));
|
|
41
|
+
const live = (fieldsSource?.fields ?? [])
|
|
42
|
+
.filter((f) => !seen.has(f.id))
|
|
43
|
+
.map((f) => ({
|
|
44
|
+
id: f.id,
|
|
45
|
+
header: f.displayName,
|
|
46
|
+
required: f.validation?.required,
|
|
47
|
+
}));
|
|
48
|
+
return [...fields, ...live];
|
|
49
|
+
},
|
|
50
|
+
get dataSource() {
|
|
51
|
+
if (destination) {
|
|
52
|
+
return { destination };
|
|
53
|
+
}
|
|
54
|
+
const add = fieldsSource?.fieldManagement?.canAddFields
|
|
55
|
+
? fieldsSource.fieldActions?.addFields
|
|
56
|
+
: undefined;
|
|
57
|
+
return {
|
|
58
|
+
collectionId: collection.queryName,
|
|
59
|
+
...(add && {
|
|
60
|
+
updateSchema: (defs) => add(defs.map(toField)),
|
|
61
|
+
}),
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
mergeFields: (f) => fieldsSource?.mergeFields?.(f),
|
|
65
|
+
...(backup && { backup }),
|
|
66
|
+
...(idFieldKey && { idFieldKey }),
|
|
67
|
+
...(jobApplicationErrorMap && { jobApplicationErrorMap }),
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
// How long to hold the completed (100%) progress bar on screen before moving to
|
|
71
|
+
// the summary step (CAIRO-4476). The job reports counts only when it finishes,
|
|
72
|
+
// so the bar sits at 0 for the whole import and would otherwise flip straight to
|
|
73
|
+
// the summary without the user ever seeing it reach completion.
|
|
20
74
|
const COMPLETION_LINGER_MS = 600;
|
|
21
75
|
// The Wix Data collection's id field key. A CSV column mapped to this field lets
|
|
22
76
|
// the user choose how rows that collide on `_id` are written (writePolicy).
|
|
@@ -90,6 +144,8 @@ export class ImportState {
|
|
|
90
144
|
this.result = {
|
|
91
145
|
succeeded: 0,
|
|
92
146
|
failed: 0,
|
|
147
|
+
created: 0,
|
|
148
|
+
updated: 0,
|
|
93
149
|
};
|
|
94
150
|
this.isBackingUp = false;
|
|
95
151
|
this.isRestoring = false;
|
|
@@ -99,12 +155,17 @@ export class ImportState {
|
|
|
99
155
|
// Id of the running data-movement job, set once submitted — used to cancel it
|
|
100
156
|
// (`terminateJob`) if the user closes mid-import.
|
|
101
157
|
this._jobId = null;
|
|
102
|
-
// The column whose pending new field the field editor is currently editing
|
|
103
|
-
//
|
|
104
|
-
// `
|
|
105
|
-
|
|
106
|
-
this.
|
|
158
|
+
// The column whose pending new field the field editor is currently editing, or
|
|
159
|
+
// `null` when the editor is closed. Observable so the source's field-editor
|
|
160
|
+
// component (rendered via `renderFieldEditor`) opens/prefills itself when this
|
|
161
|
+
// is set and closes when it clears — the editor is decoupled from the layout.
|
|
162
|
+
this.editingHeader = null;
|
|
107
163
|
this.collection = props.collection;
|
|
164
|
+
// A pre-built config wins (hand-built / tests); otherwise assemble it from
|
|
165
|
+
// the live source + inputs.
|
|
166
|
+
this.config =
|
|
167
|
+
props.config ??
|
|
168
|
+
buildImportConfig(props.fieldsSource, props.collection, props.importInputs);
|
|
108
169
|
this.importModalState = props.importModalState;
|
|
109
170
|
this.translate = props.translate;
|
|
110
171
|
this.errorHandler = props.errorHandler;
|
|
@@ -133,7 +194,12 @@ export class ImportState {
|
|
|
133
194
|
hasMappings: computed,
|
|
134
195
|
hasNoMappedData: computed,
|
|
135
196
|
selectedFieldIds: computed,
|
|
197
|
+
missingRequiredFields: computed,
|
|
198
|
+
hasMissingRequiredFields: computed,
|
|
136
199
|
idMappedHeader: computed,
|
|
200
|
+
editingHeader: observable.ref,
|
|
201
|
+
editingMapping: computed,
|
|
202
|
+
endFieldEdit: action.bound,
|
|
137
203
|
setWritePolicy: action.bound,
|
|
138
204
|
onUploadContinue: action.bound,
|
|
139
205
|
goToUpload: action.bound,
|
|
@@ -142,7 +208,10 @@ export class ImportState {
|
|
|
142
208
|
setNewFieldConfig: action.bound,
|
|
143
209
|
beginFieldEdit: action.bound,
|
|
144
210
|
applyFieldEdit: action.bound,
|
|
211
|
+
applyDataExtensionFieldEdit: action.bound,
|
|
145
212
|
close: action.bound,
|
|
213
|
+
done: action.bound,
|
|
214
|
+
downloadFailedRows: action.bound,
|
|
146
215
|
onFileSelected: action.bound,
|
|
147
216
|
startImport: action.bound,
|
|
148
217
|
reset: action.bound,
|
|
@@ -156,19 +225,33 @@ export class ImportState {
|
|
|
156
225
|
}
|
|
157
226
|
/** Wix Data collection the import writes into (unset for platformized). */
|
|
158
227
|
get collectionId() {
|
|
159
|
-
|
|
228
|
+
const ds = this.config.dataSource;
|
|
229
|
+
return 'collectionId' in ds ? ds.collectionId : undefined;
|
|
160
230
|
}
|
|
161
231
|
get destination() {
|
|
162
|
-
|
|
232
|
+
const ds = this.config.dataSource;
|
|
233
|
+
return 'destination' in ds ? ds.destination : undefined;
|
|
163
234
|
}
|
|
164
235
|
get backup() {
|
|
165
236
|
return this.config.backup;
|
|
166
237
|
}
|
|
238
|
+
/** CMS create-fields callback, present only on the `collectionId` branch. */
|
|
167
239
|
get updateSchema() {
|
|
168
|
-
|
|
240
|
+
const ds = this.config.dataSource;
|
|
241
|
+
return 'collectionId' in ds ? ds.updateSchema : undefined;
|
|
242
|
+
}
|
|
243
|
+
/** DataExtension create-fields callback, present only on the `destination` branch. */
|
|
244
|
+
get createDataExtensionFields() {
|
|
245
|
+
const ds = this.config.dataSource;
|
|
246
|
+
return 'destination' in ds ? ds.createFields : undefined;
|
|
247
|
+
}
|
|
248
|
+
/** DataExtension field-management for the editor, on the `destination` branch. */
|
|
249
|
+
get dataExtensionFieldManagement() {
|
|
250
|
+
const ds = this.config.dataSource;
|
|
251
|
+
return 'destination' in ds ? ds.fieldManagement : undefined;
|
|
169
252
|
}
|
|
170
|
-
get
|
|
171
|
-
return this.config.
|
|
253
|
+
get jobApplicationErrorMap() {
|
|
254
|
+
return this.config.jobApplicationErrorMap;
|
|
172
255
|
}
|
|
173
256
|
// The collection's id-field key; a schema-backed config may report a non-`_id`
|
|
174
257
|
// identifier. Defaults to the Wix Data `_id`.
|
|
@@ -216,6 +299,30 @@ export class ImportState {
|
|
|
216
299
|
.filter((m) => m?.kind === 'existing')
|
|
217
300
|
.map((m) => m.fieldId));
|
|
218
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Required destination fields that no CSV column is mapped to (CAIRO-4452).
|
|
304
|
+
* A required field is an existing collection field, so only an `existing`
|
|
305
|
+
* mapping can satisfy it — a "create new field" column can't. Covers both
|
|
306
|
+
* ways a required field ends up unmapped: the CSV has no matching column at
|
|
307
|
+
* all, or the user set its column to "don't import". Drives the footer
|
|
308
|
+
* indicator and blocks `startImport`.
|
|
309
|
+
*/
|
|
310
|
+
get missingRequiredFields() {
|
|
311
|
+
return this.fields.filter((field) => field.required &&
|
|
312
|
+
field.id != null &&
|
|
313
|
+
!this.selectedFieldIds.has(field.id));
|
|
314
|
+
}
|
|
315
|
+
get hasMissingRequiredFields() {
|
|
316
|
+
return this.missingRequiredFields.length > 0;
|
|
317
|
+
}
|
|
318
|
+
// Comma-separated display names of the missing required fields, for the block
|
|
319
|
+
// toast. Falls back to the field id when a field carries no header.
|
|
320
|
+
_missingRequiredFieldNames() {
|
|
321
|
+
return this.missingRequiredFields
|
|
322
|
+
.map((field) => field.header || field.id || '')
|
|
323
|
+
.filter(Boolean)
|
|
324
|
+
.join(', ');
|
|
325
|
+
}
|
|
219
326
|
/**
|
|
220
327
|
* The CSV header currently mapped to the collection's `_id` field, or `null`
|
|
221
328
|
* when no column targets it. Drives both the "When items share the same ID"
|
|
@@ -232,12 +339,13 @@ export class ImportState {
|
|
|
232
339
|
}
|
|
233
340
|
/**
|
|
234
341
|
* Whether columns may be mapped to a brand-new field created during import.
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
342
|
+
* The opt-in is the presence of the active `dataSource` branch's create
|
|
343
|
+
* callback — `updateSchema` (CMS) or `createFields` (DataExtension). A source
|
|
344
|
+
* that can't create fields (e.g. no create permission) supplies neither, so
|
|
345
|
+
* columns fall back to "don't import" (see `getDefaultMapping`).
|
|
238
346
|
*/
|
|
239
347
|
get canCreateNewField() {
|
|
240
|
-
return this.
|
|
348
|
+
return this.updateSchema != null || this.createDataExtensionFields != null;
|
|
241
349
|
}
|
|
242
350
|
/**
|
|
243
351
|
* Whether the write-policy (id-collision) choice is meaningful for this import.
|
|
@@ -269,6 +377,8 @@ export class ImportState {
|
|
|
269
377
|
this.result = {
|
|
270
378
|
succeeded: 0,
|
|
271
379
|
failed: 0,
|
|
380
|
+
created: 0,
|
|
381
|
+
updated: 0,
|
|
272
382
|
};
|
|
273
383
|
this._pendingFieldsMap = undefined;
|
|
274
384
|
this.isBackingUp = false;
|
|
@@ -279,11 +389,10 @@ export class ImportState {
|
|
|
279
389
|
}
|
|
280
390
|
async close() {
|
|
281
391
|
// 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.
|
|
392
|
+
// and restore the backup — UNLESS the import already succeeded. On success
|
|
393
|
+
// `startImport` moves straight to the 'summary' step, so a close from there
|
|
394
|
+
// just closes the modal (the summary's own `done` is the normal exit); this
|
|
395
|
+
// guard covers only closing mid-import.
|
|
287
396
|
if (this.currentStep === 'progress' && !this.importStatus.isSuccess) {
|
|
288
397
|
this._cancelled = true;
|
|
289
398
|
this._abortController?.abort();
|
|
@@ -306,6 +415,13 @@ export class ImportState {
|
|
|
306
415
|
}
|
|
307
416
|
this.importModalState.close();
|
|
308
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* Closes the summary step at the end of a completed import. The collection was
|
|
420
|
+
* already refreshed when the summary opened, so this only dismisses the modal.
|
|
421
|
+
*/
|
|
422
|
+
done() {
|
|
423
|
+
this.importModalState.close();
|
|
424
|
+
}
|
|
309
425
|
async _restoreBackup() {
|
|
310
426
|
const restoreFn = this.restoreFn;
|
|
311
427
|
if (!restoreFn) {
|
|
@@ -474,50 +590,44 @@ export class ImportState {
|
|
|
474
590
|
},
|
|
475
591
|
});
|
|
476
592
|
}
|
|
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
593
|
/**
|
|
492
|
-
* Builds a
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
594
|
+
* Builds a re-importable CSV of the rows the server rejected and hands it to
|
|
595
|
+
* `downloadCsv`. Each failed row is prefixed with a leading "why it failed"
|
|
596
|
+
* column holding the server's error message, so the user can see and fix each
|
|
597
|
+
* reason. The failed rows (and their reasons) come from the job's movement
|
|
598
|
+
* logs, mapped back to the parsed `csvRows` we still hold in memory.
|
|
496
599
|
*/
|
|
497
|
-
async
|
|
600
|
+
async downloadFailedRows() {
|
|
498
601
|
const jobId = this._jobId;
|
|
499
602
|
if (!jobId) {
|
|
500
603
|
return;
|
|
501
604
|
}
|
|
502
|
-
let
|
|
605
|
+
let failures;
|
|
503
606
|
try {
|
|
504
|
-
|
|
607
|
+
failures = await this._fetchFailedRows(jobId);
|
|
505
608
|
}
|
|
506
609
|
catch (error) {
|
|
507
610
|
console.error('Fetching failed rows report failed', error);
|
|
508
611
|
return;
|
|
509
612
|
}
|
|
510
|
-
const
|
|
511
|
-
.map((index) =>
|
|
613
|
+
const rows = failures
|
|
614
|
+
.map(({ index, reason }) => {
|
|
615
|
+
const row = this.csvRows[index];
|
|
616
|
+
return row != null ? [reason, ...row] : null;
|
|
617
|
+
})
|
|
512
618
|
.filter((row) => row != null);
|
|
513
|
-
if (
|
|
619
|
+
if (rows.length === 0) {
|
|
514
620
|
return;
|
|
515
621
|
}
|
|
516
|
-
const
|
|
622
|
+
const headers = [
|
|
623
|
+
this.translate('cairo.import.failureReasonColumn'),
|
|
624
|
+
...this.csvHeaders,
|
|
625
|
+
];
|
|
626
|
+
const content = unparseCsv(headers, rows);
|
|
517
627
|
this.downloadCsv?.(content, `failed-${this.fileName || 'import.csv'}`);
|
|
518
628
|
}
|
|
519
|
-
// Pages through the job's movement logs and returns
|
|
520
|
-
// the
|
|
629
|
+
// Pages through the job's movement logs and returns, for each row that failed,
|
|
630
|
+
// its `csvRows` index and the server's failure reason. data-hub's CSV decoder
|
|
521
631
|
// (wix-data-hub: packages/wix-data-hub-executor-c/src/plugins/formats/csv/
|
|
522
632
|
// csv-binary-decoder.ts) parses with the `csv-parse` library (`columns: true`)
|
|
523
633
|
// and sets `sourceItemId = "row ${++index + 1}"`, incrementing once **per
|
|
@@ -526,8 +636,8 @@ export class ImportState {
|
|
|
526
636
|
// `csvRows[0]`, i.e. `csvRows[N - 2]`. Because `csv-parse` is RFC-4180,
|
|
527
637
|
// quoted cells containing embedded newlines are a single record, so they do
|
|
528
638
|
// NOT shift the numbering. Out-of-range indices are guarded below.
|
|
529
|
-
async
|
|
530
|
-
const
|
|
639
|
+
async _fetchFailedRows(jobId) {
|
|
640
|
+
const failures = [];
|
|
531
641
|
let cursor;
|
|
532
642
|
do {
|
|
533
643
|
const { logs, pagingMetadata } = await this.hubJobClient.listMovementLogs(jobId, cursor ? { cursor } : undefined);
|
|
@@ -540,13 +650,13 @@ export class ImportState {
|
|
|
540
650
|
if (Number.isInteger(index) &&
|
|
541
651
|
index >= 0 &&
|
|
542
652
|
index < this.csvRows.length) {
|
|
543
|
-
|
|
653
|
+
failures.push({ index, reason: log.failure.description ?? '' });
|
|
544
654
|
}
|
|
545
655
|
}
|
|
546
656
|
cursor =
|
|
547
657
|
(pagingMetadata?.hasNext && pagingMetadata.cursors?.next) || undefined;
|
|
548
658
|
} while (cursor);
|
|
549
|
-
return
|
|
659
|
+
return failures;
|
|
550
660
|
}
|
|
551
661
|
setMapping(header, fieldId) {
|
|
552
662
|
this.mappingByHeader = {
|
|
@@ -584,23 +694,51 @@ export class ImportState {
|
|
|
584
694
|
};
|
|
585
695
|
}
|
|
586
696
|
/**
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
697
|
+
* Opens the field editor for a column's pending new field: sets
|
|
698
|
+
* `editingHeader`, which the source's editor component watches to open and
|
|
699
|
+
* prefill itself. No-op if the column isn't mapped to a new field.
|
|
590
700
|
*/
|
|
591
701
|
beginFieldEdit(header) {
|
|
592
|
-
this.
|
|
702
|
+
if (this.mappingByHeader[header]?.kind === 'new') {
|
|
703
|
+
this.editingHeader = header;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
/** Closes the field editor (clears `editingHeader`). */
|
|
707
|
+
endFieldEdit() {
|
|
708
|
+
this.editingHeader = null;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* The `new` mapping currently being edited, or `null` when the editor is
|
|
712
|
+
* closed. The editor component reads this to prefill its form.
|
|
713
|
+
*/
|
|
714
|
+
get editingMapping() {
|
|
715
|
+
if (this.editingHeader == null) {
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
const mapping = this.mappingByHeader[this.editingHeader];
|
|
719
|
+
return mapping?.kind === 'new' ? mapping : null;
|
|
593
720
|
}
|
|
594
721
|
/**
|
|
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
|
|
722
|
+
* `onSubmit` handler for the CMS field editor: maps the edited field spec into
|
|
723
|
+
* the column being edited (set via `beginFieldEdit`). No-op if nothing is being
|
|
597
724
|
* edited.
|
|
598
725
|
*/
|
|
599
726
|
applyFieldEdit(field) {
|
|
600
|
-
if (this.
|
|
727
|
+
if (this.editingHeader == null) {
|
|
601
728
|
return;
|
|
602
729
|
}
|
|
603
|
-
this.setNewFieldConfig(this.
|
|
730
|
+
this.setNewFieldConfig(this.editingHeader, toFieldEdit(field));
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* `onSubmit` handler for the DataExtension field editor: maps the edited
|
|
734
|
+
* DataExtension draft (which carries per-audience permissions) into the column
|
|
735
|
+
* being edited. The `destination`-branch counterpart to `applyFieldEdit`.
|
|
736
|
+
*/
|
|
737
|
+
applyDataExtensionFieldEdit(draft) {
|
|
738
|
+
if (this.editingHeader == null) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
this.setNewFieldConfig(this.editingHeader, toDataExtensionFieldEdit(draft));
|
|
604
742
|
}
|
|
605
743
|
/**
|
|
606
744
|
* Runs the server-side import: align the CSV to the resolved field keys,
|
|
@@ -645,21 +783,30 @@ export class ImportState {
|
|
|
645
783
|
// The job ran server-side, so a failure arrives on a 200 (the poll
|
|
646
784
|
// succeeded; the job didn't) with no HTTP error to carry the application
|
|
647
785
|
// 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.
|
|
786
|
+
// reads the code from `response.data.details.applicationError`), then
|
|
787
|
+
// resolve it through the consumer's per-code map via `withErrorHandler` —
|
|
788
|
+
// the same way as `fetchData` etc. `job.error` is the consumer's
|
|
789
|
+
// application error (their import business logic), so only they can map
|
|
790
|
+
// its codes; they supply just the code→message map and we do the
|
|
791
|
+
// wrapping. `withErrorHandler` attaches the map to the error and
|
|
792
|
+
// re-rejects it, so the progress step resolves a consumer-specific
|
|
793
|
+
// message via `getResolvedError`. Without a map the same error still
|
|
794
|
+
// throws, falling back to the generic message.
|
|
657
795
|
const error = Object.assign(new Error(`ImportJob.${job.error?.code ?? 'failed'}`), { response: { data: { details: { applicationError: job.error } } } });
|
|
658
|
-
const
|
|
659
|
-
if (
|
|
660
|
-
//
|
|
661
|
-
// `
|
|
662
|
-
|
|
796
|
+
const jobApplicationErrorMap = this.jobApplicationErrorMap;
|
|
797
|
+
if (jobApplicationErrorMap) {
|
|
798
|
+
// Adapt the consumer's code→message map into the error handler's
|
|
799
|
+
// `errorCodesMap`: each entry, when its code matches the job's
|
|
800
|
+
// application error, resolves with that error. `withErrorHandler`
|
|
801
|
+
// attaches the resolved message to `error` and re-rejects it, so the
|
|
802
|
+
// progress step surfaces it via `getResolvedError` — the consumer only
|
|
803
|
+
// supplies the map, not the wrapping.
|
|
804
|
+
const appError = (job.error ?? {});
|
|
805
|
+
const errorCodesMap = Object.fromEntries(Object.entries(jobApplicationErrorMap).map(([code, resolve]) => [
|
|
806
|
+
code,
|
|
807
|
+
() => resolve(appError),
|
|
808
|
+
]));
|
|
809
|
+
await this.errorHandler.withErrorHandler(() => Promise.reject(error), errorCodesMap);
|
|
663
810
|
}
|
|
664
811
|
throw error;
|
|
665
812
|
}
|
|
@@ -688,13 +835,21 @@ export class ImportState {
|
|
|
688
835
|
const succeeded = progress?.itemsSuccessful ?? 0;
|
|
689
836
|
const failed = progress?.itemsRejected ?? 0;
|
|
690
837
|
const current = progress?.current ?? succeeded + failed;
|
|
838
|
+
// The server splits successful items into created (new) and updated
|
|
839
|
+
// (existing) — the summary step reports each. Absent on older jobs, in
|
|
840
|
+
// which case both stay 0 and only the total `succeeded` is shown.
|
|
841
|
+
const created = progress?.itemsSuccessfulInfo?.itemsCreated ?? 0;
|
|
842
|
+
const updated = progress?.itemsSuccessfulInfo?.itemsUpdated ?? 0;
|
|
691
843
|
// `totalCount` is fixed up front in `startImport` from the CSV row count
|
|
692
844
|
// (one uploaded row per CSV row); polling only advances `processedCount`.
|
|
693
845
|
runInAction(() => {
|
|
694
846
|
this.processedCount = Math.min(current, this.totalCount);
|
|
695
|
-
this.result = { succeeded, failed };
|
|
847
|
+
this.result = { succeeded, failed, created, updated };
|
|
696
848
|
});
|
|
697
849
|
}
|
|
850
|
+
_lingerOnCompletion() {
|
|
851
|
+
return new Promise((resolve) => setTimeout(resolve, COMPLETION_LINGER_MS));
|
|
852
|
+
}
|
|
698
853
|
async _runBackup() {
|
|
699
854
|
const backup = this.backup;
|
|
700
855
|
if (!backup) {
|
|
@@ -727,9 +882,6 @@ export class ImportState {
|
|
|
727
882
|
});
|
|
728
883
|
return false;
|
|
729
884
|
}
|
|
730
|
-
_lingerOnCompletion() {
|
|
731
|
-
return new Promise((resolve) => setTimeout(resolve, COMPLETION_LINGER_MS));
|
|
732
|
-
}
|
|
733
885
|
/**
|
|
734
886
|
* Resolves every column to the final destination key used in the upload
|
|
735
887
|
* payload: an existing field id, a new field's (Cairo-decided) key, or `null`
|
|
@@ -739,13 +891,16 @@ export class ImportState {
|
|
|
739
891
|
*/
|
|
740
892
|
async _resolveFieldMapping() {
|
|
741
893
|
const resolved = {};
|
|
742
|
-
|
|
894
|
+
// Pending new columns with their deduped key + display name. Source-neutral;
|
|
895
|
+
// the per-source field def is built from these in `_createPendingFields`.
|
|
896
|
+
const pending = [];
|
|
743
897
|
// Seed with existing field ids/display names so derived keys and names never
|
|
744
898
|
// collide with them or with each other.
|
|
745
899
|
const usedKeys = new Set(this.fields.map((field) => field.id).filter((id) => !!id));
|
|
746
900
|
const usedNames = new Set(this.fields
|
|
747
901
|
.map((field) => field.header)
|
|
748
902
|
.filter((header) => !!header));
|
|
903
|
+
const canCreate = this.canCreateNewField;
|
|
749
904
|
for (const header of this.csvHeaders) {
|
|
750
905
|
const mapping = this.mappingByHeader[header];
|
|
751
906
|
if (mapping == null) {
|
|
@@ -754,41 +909,75 @@ export class ImportState {
|
|
|
754
909
|
else if (mapping.kind === 'existing') {
|
|
755
910
|
resolved[header] = mapping.fieldId;
|
|
756
911
|
}
|
|
757
|
-
else if (
|
|
912
|
+
else if (canCreate) {
|
|
758
913
|
// The user's name override (from the field editor) seeds the unique
|
|
759
914
|
// name/key derivation; otherwise fall back to the column header.
|
|
760
915
|
const baseName = mapping.displayName?.trim() || header;
|
|
761
916
|
const { displayName, key } = uniqueFieldNameAndKey(baseName, usedNames, usedKeys);
|
|
762
917
|
usedKeys.add(key);
|
|
763
918
|
usedNames.add(displayName);
|
|
764
|
-
|
|
765
|
-
key,
|
|
766
|
-
type: mapping.fieldType,
|
|
767
|
-
displayName,
|
|
768
|
-
...(mapping.config && { config: mapping.config }),
|
|
769
|
-
});
|
|
919
|
+
pending.push({ key, displayName, mapping });
|
|
770
920
|
resolved[header] = key;
|
|
771
921
|
}
|
|
772
922
|
else {
|
|
773
|
-
// "new" mapping
|
|
923
|
+
// "new" mapping with no create callback on this source — nothing to map into.
|
|
774
924
|
resolved[header] = null;
|
|
775
925
|
}
|
|
776
926
|
}
|
|
777
|
-
if (
|
|
927
|
+
if (pending.length > 0) {
|
|
778
928
|
// 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;
|
|
929
|
+
// the observable until then so columns don't update mid-import.
|
|
930
|
+
this._pendingFieldsMap = await this._createPendingFields(pending);
|
|
785
931
|
}
|
|
786
932
|
return resolved;
|
|
787
933
|
}
|
|
934
|
+
/**
|
|
935
|
+
* Creates the pending new fields via the active `dataSource` branch's callback
|
|
936
|
+
* — `updateSchema` (CMS) or `createFields` (DataExtension) — building the
|
|
937
|
+
* source-specific field def from the shared, already-deduped pending columns.
|
|
938
|
+
* The callback may throw; `startImport` surfaces that as the error state.
|
|
939
|
+
*/
|
|
940
|
+
async _createPendingFields(pending) {
|
|
941
|
+
const updateSchema = this.updateSchema;
|
|
942
|
+
const createDataExtensionFields = this.createDataExtensionFields;
|
|
943
|
+
let updated;
|
|
944
|
+
if (updateSchema) {
|
|
945
|
+
updated = await updateSchema(pending.map(({ key, displayName, mapping }) => ({
|
|
946
|
+
key,
|
|
947
|
+
type: mapping.fieldType,
|
|
948
|
+
displayName,
|
|
949
|
+
...(mapping.config && { config: mapping.config }),
|
|
950
|
+
})));
|
|
951
|
+
}
|
|
952
|
+
else if (createDataExtensionFields) {
|
|
953
|
+
// The DataExtension source builds its own field shape from the neutral
|
|
954
|
+
// pending columns — no DataExtension-specific def-building in core.
|
|
955
|
+
updated = await createDataExtensionFields(pending);
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
return undefined;
|
|
959
|
+
}
|
|
960
|
+
// Guard the empty map: SchemaState.mergeFields is a full replace, so merging
|
|
961
|
+
// `{}` would wipe every column — treat a missing/empty return as "nothing to
|
|
962
|
+
// merge".
|
|
963
|
+
return updated && Object.keys(updated).length > 0 ? updated : undefined;
|
|
964
|
+
}
|
|
788
965
|
async startImport() {
|
|
789
966
|
if (!this.hasMappings) {
|
|
790
967
|
return;
|
|
791
968
|
}
|
|
969
|
+
// Block import while any required destination field is unmapped and tell the
|
|
970
|
+
// user which (CAIRO-4452). The footer already flags this; the Import button
|
|
971
|
+
// stays enabled, so this is where the click is stopped.
|
|
972
|
+
if (this.hasMissingRequiredFields) {
|
|
973
|
+
this.showToast?.({
|
|
974
|
+
type: 'ERROR',
|
|
975
|
+
message: this.translate('cairo.import.missingRequiredMessage', {
|
|
976
|
+
fields: this._missingRequiredFieldNames(),
|
|
977
|
+
}),
|
|
978
|
+
});
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
792
981
|
this._cancelled = false;
|
|
793
982
|
this._abortController = new AbortController();
|
|
794
983
|
// One uploaded row per CSV row (the rewrite drops columns, not rows), so the
|
|
@@ -805,6 +994,8 @@ export class ImportState {
|
|
|
805
994
|
this.result = {
|
|
806
995
|
succeeded: 0,
|
|
807
996
|
failed: 0,
|
|
997
|
+
created: 0,
|
|
998
|
+
updated: 0,
|
|
808
999
|
};
|
|
809
1000
|
this._jobId = null;
|
|
810
1001
|
// Drop any fields map from a prior attempt so a no-new-fields "Try again"
|
|
@@ -857,28 +1048,30 @@ export class ImportState {
|
|
|
857
1048
|
...queryStatus.success,
|
|
858
1049
|
data: this.result,
|
|
859
1050
|
};
|
|
1051
|
+
// The job reports counts only when it finishes, so the bar sat at 0 for
|
|
1052
|
+
// the whole import (CAIRO-4476). Snap it to 100% and hold it (below)
|
|
1053
|
+
// before the summary step replaces the progress step, so the user sees
|
|
1054
|
+
// the import reach completion.
|
|
1055
|
+
this.processedCount = this.totalCount;
|
|
860
1056
|
});
|
|
861
|
-
//
|
|
862
|
-
//
|
|
863
|
-
// the
|
|
864
|
-
|
|
865
|
-
if (this.
|
|
866
|
-
|
|
867
|
-
if (this._cancelled) {
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
1057
|
+
// Keep the completed bar on screen briefly before moving on. `importStatus`
|
|
1058
|
+
// is already success, so a close during this window just dismisses the
|
|
1059
|
+
// modal (the `close()` guard doesn't treat it as a cancel).
|
|
1060
|
+
await this._lingerOnCompletion();
|
|
1061
|
+
if (this._cancelled) {
|
|
1062
|
+
return;
|
|
870
1063
|
}
|
|
871
1064
|
// Success path only. `_pendingFieldsMap` holds this run's returned map — it's
|
|
872
1065
|
// cleared at the start of `startImport` (the stale-guard) and set by
|
|
873
1066
|
// `_resolveFieldMapping`; snapshotting here keeps the merge success-gated.
|
|
874
1067
|
createdFields = this._pendingFieldsMap;
|
|
875
1068
|
runInAction(() => {
|
|
876
|
-
//
|
|
877
|
-
//
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
this.
|
|
1069
|
+
// Import finished: show the summary step (created / updated / failed
|
|
1070
|
+
// counts, plus a "Download CSV" of the failed rows when any failed).
|
|
1071
|
+
// The modal stays open; the user dismisses it from the summary via
|
|
1072
|
+
// `done`. Refresh the collection now so the table behind the modal is
|
|
1073
|
+
// up to date by the time it closes.
|
|
1074
|
+
this.currentStep = 'summary';
|
|
882
1075
|
this.collection.clearResultAndMoveToStart({ force: true });
|
|
883
1076
|
});
|
|
884
1077
|
}
|