@wix/bex-core 2.331.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 +11 -0
- package/dist/cjs/assets/locale/messages_bg.json +11 -0
- package/dist/cjs/assets/locale/messages_ca.json +11 -0
- 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 +11 -0
- package/dist/cjs/assets/locale/messages_en.json +11 -0
- package/dist/cjs/assets/locale/messages_es.json +11 -0
- package/dist/cjs/assets/locale/messages_fi.json +11 -0
- package/dist/cjs/assets/locale/messages_fr.json +11 -0
- package/dist/cjs/assets/locale/messages_he.json +11 -0
- package/dist/cjs/assets/locale/messages_hi.json +11 -0
- package/dist/cjs/assets/locale/messages_hu.json +11 -0
- 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 +11 -0
- package/dist/cjs/assets/locale/messages_ms.json +11 -0
- 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 +11 -0
- package/dist/cjs/assets/locale/messages_ru.json +11 -0
- package/dist/cjs/assets/locale/messages_sk.json +11 -0
- package/dist/cjs/assets/locale/messages_sl.json +11 -0
- 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 +11 -0
- 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 +0 -6
- package/dist/cjs/hooks/index.js.map +1 -1
- package/dist/cjs/hooks/useImport.js +3 -13
- package/dist/cjs/hooks/useImport.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 +401 -133
- package/dist/cjs/state/ImportState/ImportState.js.map +1 -1
- package/dist/cjs/state/ImportState/__tests__/ImportState.engine.spec.js +671 -62
- 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/state/TagsState/TagsManagementState.js +17 -44
- package/dist/cjs/state/TagsState/TagsManagementState.js.map +1 -1
- package/dist/cjs/state/TagsState/TagsSearchState.js +0 -6
- package/dist/cjs/state/TagsState/TagsSearchState.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/FieldsSource.js.map +1 -1
- package/dist/cjs/types/Schema.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 +11 -0
- package/dist/esm/assets/locale/messages_bg.json +11 -0
- package/dist/esm/assets/locale/messages_ca.json +11 -0
- 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 +11 -0
- package/dist/esm/assets/locale/messages_en.json +11 -0
- package/dist/esm/assets/locale/messages_es.json +11 -0
- package/dist/esm/assets/locale/messages_fi.json +11 -0
- package/dist/esm/assets/locale/messages_fr.json +11 -0
- package/dist/esm/assets/locale/messages_he.json +11 -0
- package/dist/esm/assets/locale/messages_hi.json +11 -0
- package/dist/esm/assets/locale/messages_hu.json +11 -0
- 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 +11 -0
- package/dist/esm/assets/locale/messages_ms.json +11 -0
- 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 +11 -0
- package/dist/esm/assets/locale/messages_ru.json +11 -0
- package/dist/esm/assets/locale/messages_sk.json +11 -0
- package/dist/esm/assets/locale/messages_sl.json +11 -0
- 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 +11 -0
- 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 +0 -1
- package/dist/esm/hooks/index.js.map +1 -1
- package/dist/esm/hooks/useImport.js +2 -7
- package/dist/esm/hooks/useImport.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 +299 -108
- package/dist/esm/state/ImportState/ImportState.js.map +1 -1
- package/dist/esm/state/ImportState/__tests__/ImportState.engine.spec.js +442 -55
- 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/state/TagsState/TagsManagementState.js +17 -33
- package/dist/esm/state/TagsState/TagsManagementState.js.map +1 -1
- package/dist/esm/state/TagsState/TagsSearchState.js +0 -6
- package/dist/esm/state/TagsState/TagsSearchState.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/hooks/useImport.d.ts +3 -18
- package/dist/types/hooks/useImport.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 +20 -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 +201 -62
- 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/state/TagsState/TagsManagementState.d.ts.map +1 -1
- package/dist/types/state/TagsState/TagsSearchState.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/FieldsSource.d.ts +5 -1
- package/dist/types/types/FieldsSource.d.ts.map +1 -1
- package/dist/types/types/Schema.d.ts +6 -0
- package/dist/types/types/Schema.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 -10
- 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 -21
- package/dist/types/hooks/useImportConfig.d.ts.map +0 -1
|
@@ -17,33 +17,39 @@ var _detectFieldType = require("./detectFieldType");
|
|
|
17
17
|
var _fieldEdit = require("./fieldEdit");
|
|
18
18
|
/** Outcome of a server-side import job, derived from the job's progress. */
|
|
19
19
|
|
|
20
|
+
/** A rejected row: its index into `csvRows` and the server's failure reason. */
|
|
21
|
+
|
|
20
22
|
/**
|
|
21
23
|
* Optional snapshot taken before the import runs, returning a `restore` used to
|
|
22
24
|
* undo the import on cancel. See {@link BackupResult.restore} for the revert
|
|
23
25
|
* contract under upsert.
|
|
24
26
|
*/
|
|
25
27
|
|
|
28
|
+
/** The application error from a failed import job, handed to a resolver. */
|
|
29
|
+
|
|
26
30
|
/**
|
|
27
|
-
* Consumer
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* (the v2 errorHandler shape, like `getExportAsyncJobErrors`):
|
|
31
|
+
* Consumer-supplied map from a failed import job's application-error code to a
|
|
32
|
+
* resolver that returns the message to show. An import runs server-side, so a
|
|
33
|
+
* failure arrives on a 200 (the poll succeeds; the job doesn't) with the
|
|
34
|
+
* application error in the job status. The consumer supplies only the
|
|
35
|
+
* code→message map (their codes, their translations) — ImportState applies it
|
|
36
|
+
* through the container's `errorHandler` internally, so the progress step can
|
|
37
|
+
* resolve the message via `getResolvedError`. The resolver receives the job's
|
|
38
|
+
* application error (its `code`/`description`/`data`):
|
|
36
39
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* })
|
|
40
|
+
* jobApplicationErrorMap: {
|
|
41
|
+
* [jobErrorCode]: (error) => ({ message: translate(..., error.data) }),
|
|
42
|
+
* }
|
|
41
43
|
*
|
|
42
|
-
*
|
|
43
|
-
* the progress step resolves a consumer-specific message via `getResolvedError`.
|
|
44
|
-
* When omitted, the error still surfaces but resolves to the generic message.
|
|
44
|
+
* When omitted, a failed job falls back to the generic error message.
|
|
45
45
|
*/
|
|
46
46
|
|
|
47
|
+
// `required` is added on top of the ambassador descriptor (which carries only
|
|
48
|
+
// `id`/`header`): a field the destination mandates. Consumers set it — the
|
|
49
|
+
// schema-backed config forwards `validation.required`, manual/platformized
|
|
50
|
+
// configs set it per field — and the mapping step blocks import while any
|
|
51
|
+
// required field has no column mapped to it (CAIRO-4452).
|
|
52
|
+
|
|
47
53
|
// `NewFieldType`, `detectColumnType`, etc. are re-exported from the package via
|
|
48
54
|
// the `ImportState` barrel (`./index`), which also exports `./detectFieldType`.
|
|
49
55
|
|
|
@@ -56,8 +62,16 @@ var _fieldEdit = require("./fieldEdit");
|
|
|
56
62
|
// Per-type configuration for a new field, captured from the "Edit field" editor
|
|
57
63
|
// (CAIRO-4398). All optional — an unedited field carries none and is created
|
|
58
64
|
// from detected defaults only. `displayName` is intentionally not here: the
|
|
59
|
-
// (possibly user-overridden) name is deduped and surfaced as
|
|
60
|
-
// `
|
|
65
|
+
// (possibly user-overridden) name is deduped and surfaced as the field def's
|
|
66
|
+
// `displayName`.
|
|
67
|
+
//
|
|
68
|
+
// This is core-internal storage on the column mapping, shared by both sources —
|
|
69
|
+
// the CMS editor fills `validation`/`isPii`/`helpText`, the DataExtension editor
|
|
70
|
+
// fills `permissions`/`isPii`/`helpText`/`deType`. The two never cross: which
|
|
71
|
+
// subset is read is decided by the config's `dataSource` branch when the field
|
|
72
|
+
// def is built (`_resolveFieldMapping`). The consumer-facing create callbacks
|
|
73
|
+
// still receive distinct def types (`NewFieldDefinition` vs
|
|
74
|
+
// `NewDataExtensionFieldDef`), so this superset is not part of the public API.
|
|
61
75
|
|
|
62
76
|
// A new field to add to the collection schema during import. `key` is decided by
|
|
63
77
|
// Cairo (derived from the column header — or the user's name override — deduped)
|
|
@@ -72,6 +86,23 @@ var _fieldEdit = require("./fieldEdit");
|
|
|
72
86
|
* the consumer performs one schema mutation (e.g. `updateSchema(collectionId,
|
|
73
87
|
* ...addField(key, config))`). Its presence is also the opt-in for the "create
|
|
74
88
|
* new field" default — see `getDefaultMapping`.
|
|
89
|
+
*
|
|
90
|
+
* Optionally returns the collection's updated fields map, mirroring the schema's
|
|
91
|
+
* `FieldActions`. When it does, the import merges that map into the table's field
|
|
92
|
+
* source on success so the new columns appear with no manual reload (CAIRO-4400).
|
|
93
|
+
* Returning `void` still imports fine — the columns just wait for the next schema
|
|
94
|
+
* load.
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Creates the pending new DataExtension custom fields before the rows are
|
|
99
|
+
* uploaded — the `destination` branch's counterpart to `updateSchema`. Its
|
|
100
|
+
* presence is the "create new field" opt-in for a DataExtension import. Receives
|
|
101
|
+
* the source-neutral pending columns (Cairo-decided keys + names + the edited
|
|
102
|
+
* config); the DataExtension source (`DataExtensionImportRef`) builds its own
|
|
103
|
+
* field shape from them, so no DataExtension-specific shape leaks into core.
|
|
104
|
+
* Optionally returns the updated fields map to merge into the table (CAIRO-4400);
|
|
105
|
+
* DataExtension refreshes its own schema in place, so returning `void` is fine.
|
|
75
106
|
*/
|
|
76
107
|
|
|
77
108
|
/**
|
|
@@ -85,10 +116,31 @@ var _fieldEdit = require("./fieldEdit");
|
|
|
85
116
|
* - `null` — don't import (skip)
|
|
86
117
|
*/
|
|
87
118
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
119
|
+
/** A `new`-mapped column with its deduped key + display name, handed to a
|
|
120
|
+
* source's create callback so it can build its own field shape. */
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Where the import writes and how it creates new fields, split per source. The
|
|
124
|
+
* discriminant (`collectionId` vs `destination`) makes the two mutually
|
|
125
|
+
* exclusive and pairs each target with its own create-fields callback, so a CMS
|
|
126
|
+
* target can't be paired with a DataExtension create callback (or vice versa).
|
|
127
|
+
* Each branch's create members are optional — a plain import supplies none and
|
|
128
|
+
* is mapping-only.
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The import inputs a consumer supplies. Satisfied both by a plain object (the
|
|
133
|
+
* manual case) and by a live reference (`useSchemaImport` in `@wix/patterns`, or
|
|
134
|
+
* the internally-built `DataExtensionImportRef`) whose members are getters read
|
|
135
|
+
* on demand — so the async schema load needn't resolve before `ImportState` is
|
|
136
|
+
* built. `ImportState` holds it by reference (never spreads it) so those getters
|
|
137
|
+
* stay live.
|
|
138
|
+
*/
|
|
139
|
+
|
|
140
|
+
// How long to hold the completed (100%) progress bar on screen before moving to
|
|
141
|
+
// the summary step (CAIRO-4476). The job reports counts only when it finishes,
|
|
142
|
+
// so the bar sits at 0 for the whole import and would otherwise flip straight to
|
|
143
|
+
// the summary without the user ever seeing it reach completion.
|
|
92
144
|
const COMPLETION_LINGER_MS = 600;
|
|
93
145
|
|
|
94
146
|
// The Wix Data collection's id field key. A CSV column mapped to this field lets
|
|
@@ -131,18 +183,15 @@ function uniqueFieldNameAndKey(header, usedNames, usedKeys) {
|
|
|
131
183
|
}
|
|
132
184
|
class ImportState {
|
|
133
185
|
constructor(props) {
|
|
134
|
-
|
|
135
|
-
(
|
|
136
|
-
(0, _defineProperty2.default)(this, "
|
|
137
|
-
(0, _defineProperty2.default)(this, "backup", void 0);
|
|
138
|
-
(0, _defineProperty2.default)(this, "updateSchema", void 0);
|
|
186
|
+
// The import inputs, held by reference so a live schema-backed config's getters
|
|
187
|
+
// stay live (never spread). The public getters below read from it on demand.
|
|
188
|
+
(0, _defineProperty2.default)(this, "config", void 0);
|
|
139
189
|
(0, _defineProperty2.default)(this, "collection", void 0);
|
|
140
190
|
(0, _defineProperty2.default)(this, "importModalState", void 0);
|
|
141
191
|
(0, _defineProperty2.default)(this, "translate", void 0);
|
|
142
192
|
(0, _defineProperty2.default)(this, "errorHandler", void 0);
|
|
143
193
|
(0, _defineProperty2.default)(this, "showToast", void 0);
|
|
144
194
|
(0, _defineProperty2.default)(this, "downloadCsv", void 0);
|
|
145
|
-
(0, _defineProperty2.default)(this, "onJobError", void 0);
|
|
146
195
|
(0, _defineProperty2.default)(this, "hubJobClient", void 0);
|
|
147
196
|
(0, _defineProperty2.default)(this, "_detectColumnType", void 0);
|
|
148
197
|
(0, _defineProperty2.default)(this, "discardChangesModal", new _ConditionalModalState.ConditionalModalState());
|
|
@@ -174,7 +223,9 @@ class ImportState {
|
|
|
174
223
|
(0, _defineProperty2.default)(this, "totalCount", 0);
|
|
175
224
|
(0, _defineProperty2.default)(this, "result", {
|
|
176
225
|
succeeded: 0,
|
|
177
|
-
failed: 0
|
|
226
|
+
failed: 0,
|
|
227
|
+
created: 0,
|
|
228
|
+
updated: 0
|
|
178
229
|
});
|
|
179
230
|
(0, _defineProperty2.default)(this, "isBackingUp", false);
|
|
180
231
|
(0, _defineProperty2.default)(this, "isRestoring", false);
|
|
@@ -184,22 +235,22 @@ class ImportState {
|
|
|
184
235
|
// Id of the running data-movement job, set once submitted — used to cancel it
|
|
185
236
|
// (`terminateJob`) if the user closes mid-import.
|
|
186
237
|
(0, _defineProperty2.default)(this, "_jobId", null);
|
|
187
|
-
// The column whose pending new field the field editor is currently editing
|
|
188
|
-
//
|
|
189
|
-
// `
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
this
|
|
238
|
+
// The column whose pending new field the field editor is currently editing, or
|
|
239
|
+
// `null` when the editor is closed. Observable so the source's field-editor
|
|
240
|
+
// component (rendered via `renderFieldEditor`) opens/prefills itself when this
|
|
241
|
+
// is set and closes when it clears — the editor is decoupled from the layout.
|
|
242
|
+
(0, _defineProperty2.default)(this, "editingHeader", null);
|
|
243
|
+
// Holds `updateSchema`'s returned fields map between field creation (pre-upload)
|
|
244
|
+
// and success. Kept private/non-observable so it can't update columns mid-import;
|
|
245
|
+
// it's promoted to the observable `createdFieldsMap` only on success.
|
|
246
|
+
(0, _defineProperty2.default)(this, "_pendingFieldsMap", void 0);
|
|
247
|
+
this.config = props.config;
|
|
196
248
|
this.collection = props.collection;
|
|
197
249
|
this.importModalState = props.importModalState;
|
|
198
250
|
this.translate = props.translate;
|
|
199
251
|
this.errorHandler = props.errorHandler;
|
|
200
252
|
this.showToast = props.showToast;
|
|
201
253
|
this.downloadCsv = props.downloadCsv;
|
|
202
|
-
this.onJobError = props.onJobError;
|
|
203
254
|
this._detectColumnType = props.detectColumnType ?? _detectFieldType.detectColumnType;
|
|
204
255
|
this.hubJobClient = (0, _hubJobClient.createHubJobClient)({
|
|
205
256
|
httpClient: props.httpClient,
|
|
@@ -223,7 +274,12 @@ class ImportState {
|
|
|
223
274
|
hasMappings: _mobx.computed,
|
|
224
275
|
hasNoMappedData: _mobx.computed,
|
|
225
276
|
selectedFieldIds: _mobx.computed,
|
|
277
|
+
missingRequiredFields: _mobx.computed,
|
|
278
|
+
hasMissingRequiredFields: _mobx.computed,
|
|
226
279
|
idMappedHeader: _mobx.computed,
|
|
280
|
+
editingHeader: _mobx.observable.ref,
|
|
281
|
+
editingMapping: _mobx.computed,
|
|
282
|
+
endFieldEdit: _mobx.action.bound,
|
|
227
283
|
setWritePolicy: _mobx.action.bound,
|
|
228
284
|
onUploadContinue: _mobx.action.bound,
|
|
229
285
|
goToUpload: _mobx.action.bound,
|
|
@@ -232,12 +288,63 @@ class ImportState {
|
|
|
232
288
|
setNewFieldConfig: _mobx.action.bound,
|
|
233
289
|
beginFieldEdit: _mobx.action.bound,
|
|
234
290
|
applyFieldEdit: _mobx.action.bound,
|
|
291
|
+
applyDataExtensionFieldEdit: _mobx.action.bound,
|
|
235
292
|
close: _mobx.action.bound,
|
|
293
|
+
done: _mobx.action.bound,
|
|
294
|
+
downloadFailedRows: _mobx.action.bound,
|
|
236
295
|
onFileSelected: _mobx.action.bound,
|
|
237
296
|
startImport: _mobx.action.bound,
|
|
238
297
|
reset: _mobx.action.bound
|
|
239
298
|
});
|
|
240
299
|
}
|
|
300
|
+
|
|
301
|
+
// Import inputs read live from `config` — a live schema-backed config exposes
|
|
302
|
+
// these as getters, a manual config as plain values; both read the same way.
|
|
303
|
+
|
|
304
|
+
/** Existing collection fields, for column auto-mapping. */
|
|
305
|
+
get fields() {
|
|
306
|
+
return this.config.fields;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Wix Data collection the import writes into (unset for platformized). */
|
|
310
|
+
get collectionId() {
|
|
311
|
+
const ds = this.config.dataSource;
|
|
312
|
+
return 'collectionId' in ds ? ds.collectionId : undefined;
|
|
313
|
+
}
|
|
314
|
+
get destination() {
|
|
315
|
+
const ds = this.config.dataSource;
|
|
316
|
+
return 'destination' in ds ? ds.destination : undefined;
|
|
317
|
+
}
|
|
318
|
+
get backup() {
|
|
319
|
+
return this.config.backup;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** CMS create-fields callback, present only on the `collectionId` branch. */
|
|
323
|
+
get updateSchema() {
|
|
324
|
+
const ds = this.config.dataSource;
|
|
325
|
+
return 'collectionId' in ds ? ds.updateSchema : undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** DataExtension create-fields callback, present only on the `destination` branch. */
|
|
329
|
+
get createDataExtensionFields() {
|
|
330
|
+
const ds = this.config.dataSource;
|
|
331
|
+
return 'destination' in ds ? ds.createFields : undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** DataExtension field-management for the editor, on the `destination` branch. */
|
|
335
|
+
get dataExtensionFieldManagement() {
|
|
336
|
+
const ds = this.config.dataSource;
|
|
337
|
+
return 'destination' in ds ? ds.fieldManagement : undefined;
|
|
338
|
+
}
|
|
339
|
+
get jobApplicationErrorMap() {
|
|
340
|
+
return this.config.jobApplicationErrorMap;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// The collection's id-field key; a schema-backed config may report a non-`_id`
|
|
344
|
+
// identifier. Defaults to the Wix Data `_id`.
|
|
345
|
+
get idFieldKey() {
|
|
346
|
+
return this.config.idFieldKey ?? ID_FIELD_KEY;
|
|
347
|
+
}
|
|
241
348
|
init() {
|
|
242
349
|
return () => {
|
|
243
350
|
// Unmounting mid-import stops the poll loop (it checks `_cancelled`) so it
|
|
@@ -279,6 +386,27 @@ class ImportState {
|
|
|
279
386
|
return new Set(Object.values(this.mappingByHeader).filter(m => (m == null ? void 0 : m.kind) === 'existing').map(m => m.fieldId));
|
|
280
387
|
}
|
|
281
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Required destination fields that no CSV column is mapped to (CAIRO-4452).
|
|
391
|
+
* A required field is an existing collection field, so only an `existing`
|
|
392
|
+
* mapping can satisfy it — a "create new field" column can't. Covers both
|
|
393
|
+
* ways a required field ends up unmapped: the CSV has no matching column at
|
|
394
|
+
* all, or the user set its column to "don't import". Drives the footer
|
|
395
|
+
* indicator and blocks `startImport`.
|
|
396
|
+
*/
|
|
397
|
+
get missingRequiredFields() {
|
|
398
|
+
return this.fields.filter(field => field.required && field.id != null && !this.selectedFieldIds.has(field.id));
|
|
399
|
+
}
|
|
400
|
+
get hasMissingRequiredFields() {
|
|
401
|
+
return this.missingRequiredFields.length > 0;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Comma-separated display names of the missing required fields, for the block
|
|
405
|
+
// toast. Falls back to the field id when a field carries no header.
|
|
406
|
+
_missingRequiredFieldNames() {
|
|
407
|
+
return this.missingRequiredFields.map(field => field.header || field.id || '').filter(Boolean).join(', ');
|
|
408
|
+
}
|
|
409
|
+
|
|
282
410
|
/**
|
|
283
411
|
* The CSV header currently mapped to the collection's `_id` field, or `null`
|
|
284
412
|
* when no column targets it. Drives both the "When items share the same ID"
|
|
@@ -286,7 +414,8 @@ class ImportState {
|
|
|
286
414
|
* with no `_id` mapped, every row is a fresh insert and collisions can't occur.
|
|
287
415
|
*/
|
|
288
416
|
get idMappedHeader() {
|
|
289
|
-
const
|
|
417
|
+
const idFieldKey = this.idFieldKey;
|
|
418
|
+
const entry = Object.entries(this.mappingByHeader).find(([, mapping]) => (mapping == null ? void 0 : mapping.kind) === 'existing' && mapping.fieldId === idFieldKey);
|
|
290
419
|
return entry ? entry[0] : null;
|
|
291
420
|
}
|
|
292
421
|
setWritePolicy(policy) {
|
|
@@ -295,12 +424,13 @@ class ImportState {
|
|
|
295
424
|
|
|
296
425
|
/**
|
|
297
426
|
* Whether columns may be mapped to a brand-new field created during import.
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
427
|
+
* The opt-in is the presence of the active `dataSource` branch's create
|
|
428
|
+
* callback — `updateSchema` (CMS) or `createFields` (DataExtension). A source
|
|
429
|
+
* that can't create fields (e.g. no create permission) supplies neither, so
|
|
430
|
+
* columns fall back to "don't import" (see `getDefaultMapping`).
|
|
301
431
|
*/
|
|
302
432
|
get canCreateNewField() {
|
|
303
|
-
return this.updateSchema != null;
|
|
433
|
+
return this.updateSchema != null || this.createDataExtensionFields != null;
|
|
304
434
|
}
|
|
305
435
|
|
|
306
436
|
/**
|
|
@@ -333,8 +463,11 @@ class ImportState {
|
|
|
333
463
|
this.totalCount = 0;
|
|
334
464
|
this.result = {
|
|
335
465
|
succeeded: 0,
|
|
336
|
-
failed: 0
|
|
466
|
+
failed: 0,
|
|
467
|
+
created: 0,
|
|
468
|
+
updated: 0
|
|
337
469
|
};
|
|
470
|
+
this._pendingFieldsMap = undefined;
|
|
338
471
|
this.isBackingUp = false;
|
|
339
472
|
this.isRestoring = false;
|
|
340
473
|
this.restoreFn = null;
|
|
@@ -343,11 +476,10 @@ class ImportState {
|
|
|
343
476
|
}
|
|
344
477
|
async close() {
|
|
345
478
|
// While on the progress step we treat close as a cancel — terminate the job
|
|
346
|
-
// and restore the backup — UNLESS the import already succeeded. On
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
// lets the success path do its collection refresh.
|
|
479
|
+
// and restore the backup — UNLESS the import already succeeded. On success
|
|
480
|
+
// `startImport` moves straight to the 'summary' step, so a close from there
|
|
481
|
+
// just closes the modal (the summary's own `done` is the normal exit); this
|
|
482
|
+
// guard covers only closing mid-import.
|
|
351
483
|
if (this.currentStep === 'progress' && !this.importStatus.isSuccess) {
|
|
352
484
|
var _this$_abortControlle2;
|
|
353
485
|
this._cancelled = true;
|
|
@@ -371,6 +503,14 @@ class ImportState {
|
|
|
371
503
|
}
|
|
372
504
|
this.importModalState.close();
|
|
373
505
|
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Closes the summary step at the end of a completed import. The collection was
|
|
509
|
+
* already refreshed when the summary opened, so this only dismisses the modal.
|
|
510
|
+
*/
|
|
511
|
+
done() {
|
|
512
|
+
this.importModalState.close();
|
|
513
|
+
}
|
|
374
514
|
async _restoreBackup() {
|
|
375
515
|
const restoreFn = this.restoreFn;
|
|
376
516
|
if (!restoreFn) {
|
|
@@ -541,51 +681,44 @@ class ImportState {
|
|
|
541
681
|
}
|
|
542
682
|
});
|
|
543
683
|
}
|
|
544
|
-
_showPartialSuccessToast() {
|
|
545
|
-
var _this$showToast3;
|
|
546
|
-
(_this$showToast3 = this.showToast) == null || _this$showToast3.call(this, {
|
|
547
|
-
type: 'WARNING',
|
|
548
|
-
message: this.translate('cairo.import.partialSuccessToast', {
|
|
549
|
-
failed: String(this.result.failed),
|
|
550
|
-
imported: String(this.result.succeeded)
|
|
551
|
-
}),
|
|
552
|
-
action: {
|
|
553
|
-
text: this.translate('cairo.import.downloadFailedRows'),
|
|
554
|
-
onClick: () => this._downloadFailedRows(),
|
|
555
|
-
removeToastOnClick: true
|
|
556
|
-
}
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
684
|
|
|
560
685
|
/**
|
|
561
|
-
* Builds a
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
686
|
+
* Builds a re-importable CSV of the rows the server rejected and hands it to
|
|
687
|
+
* `downloadCsv`. Each failed row is prefixed with a leading "why it failed"
|
|
688
|
+
* column holding the server's error message, so the user can see and fix each
|
|
689
|
+
* reason. The failed rows (and their reasons) come from the job's movement
|
|
690
|
+
* logs, mapped back to the parsed `csvRows` we still hold in memory.
|
|
565
691
|
*/
|
|
566
|
-
async
|
|
692
|
+
async downloadFailedRows() {
|
|
567
693
|
var _this$downloadCsv;
|
|
568
694
|
const jobId = this._jobId;
|
|
569
695
|
if (!jobId) {
|
|
570
696
|
return;
|
|
571
697
|
}
|
|
572
|
-
let
|
|
698
|
+
let failures;
|
|
573
699
|
try {
|
|
574
|
-
|
|
700
|
+
failures = await this._fetchFailedRows(jobId);
|
|
575
701
|
} catch (error) {
|
|
576
702
|
console.error('Fetching failed rows report failed', error);
|
|
577
703
|
return;
|
|
578
704
|
}
|
|
579
|
-
const
|
|
580
|
-
|
|
705
|
+
const rows = failures.map(({
|
|
706
|
+
index,
|
|
707
|
+
reason
|
|
708
|
+
}) => {
|
|
709
|
+
const row = this.csvRows[index];
|
|
710
|
+
return row != null ? [reason, ...row] : null;
|
|
711
|
+
}).filter(row => row != null);
|
|
712
|
+
if (rows.length === 0) {
|
|
581
713
|
return;
|
|
582
714
|
}
|
|
583
|
-
const
|
|
715
|
+
const headers = [this.translate('cairo.import.failureReasonColumn'), ...this.csvHeaders];
|
|
716
|
+
const content = (0, _parseCsv.unparseCsv)(headers, rows);
|
|
584
717
|
(_this$downloadCsv = this.downloadCsv) == null || _this$downloadCsv.call(this, content, `failed-${this.fileName || 'import.csv'}`);
|
|
585
718
|
}
|
|
586
719
|
|
|
587
|
-
// Pages through the job's movement logs and returns
|
|
588
|
-
// the
|
|
720
|
+
// Pages through the job's movement logs and returns, for each row that failed,
|
|
721
|
+
// its `csvRows` index and the server's failure reason. data-hub's CSV decoder
|
|
589
722
|
// (wix-data-hub: packages/wix-data-hub-executor-c/src/plugins/formats/csv/
|
|
590
723
|
// csv-binary-decoder.ts) parses with the `csv-parse` library (`columns: true`)
|
|
591
724
|
// and sets `sourceItemId = "row ${++index + 1}"`, incrementing once **per
|
|
@@ -594,8 +727,8 @@ class ImportState {
|
|
|
594
727
|
// `csvRows[0]`, i.e. `csvRows[N - 2]`. Because `csv-parse` is RFC-4180,
|
|
595
728
|
// quoted cells containing embedded newlines are a single record, so they do
|
|
596
729
|
// NOT shift the numbering. Out-of-range indices are guarded below.
|
|
597
|
-
async
|
|
598
|
-
const
|
|
730
|
+
async _fetchFailedRows(jobId) {
|
|
731
|
+
const failures = [];
|
|
599
732
|
let cursor;
|
|
600
733
|
do {
|
|
601
734
|
var _pagingMetadata$curso;
|
|
@@ -613,12 +746,15 @@ class ImportState {
|
|
|
613
746
|
const fileLine = Number((_exec = /\d+/.exec(log.sourceItemId)) == null ? void 0 : _exec[0]);
|
|
614
747
|
const index = fileLine - 2;
|
|
615
748
|
if (Number.isInteger(index) && index >= 0 && index < this.csvRows.length) {
|
|
616
|
-
|
|
749
|
+
failures.push({
|
|
750
|
+
index,
|
|
751
|
+
reason: log.failure.description ?? ''
|
|
752
|
+
});
|
|
617
753
|
}
|
|
618
754
|
}
|
|
619
755
|
cursor = (pagingMetadata == null ? void 0 : pagingMetadata.hasNext) && ((_pagingMetadata$curso = pagingMetadata.cursors) == null ? void 0 : _pagingMetadata$curso.next) || undefined;
|
|
620
756
|
} while (cursor);
|
|
621
|
-
return
|
|
757
|
+
return failures;
|
|
622
758
|
}
|
|
623
759
|
setMapping(header, fieldId) {
|
|
624
760
|
this.mappingByHeader = {
|
|
@@ -668,24 +804,56 @@ class ImportState {
|
|
|
668
804
|
}
|
|
669
805
|
|
|
670
806
|
/**
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
807
|
+
* Opens the field editor for a column's pending new field: sets
|
|
808
|
+
* `editingHeader`, which the source's editor component watches to open and
|
|
809
|
+
* prefill itself. No-op if the column isn't mapped to a new field.
|
|
674
810
|
*/
|
|
675
811
|
beginFieldEdit(header) {
|
|
676
|
-
|
|
812
|
+
var _this$mappingByHeader;
|
|
813
|
+
if (((_this$mappingByHeader = this.mappingByHeader[header]) == null ? void 0 : _this$mappingByHeader.kind) === 'new') {
|
|
814
|
+
this.editingHeader = header;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** Closes the field editor (clears `editingHeader`). */
|
|
819
|
+
endFieldEdit() {
|
|
820
|
+
this.editingHeader = null;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* The `new` mapping currently being edited, or `null` when the editor is
|
|
825
|
+
* closed. The editor component reads this to prefill its form.
|
|
826
|
+
*/
|
|
827
|
+
get editingMapping() {
|
|
828
|
+
if (this.editingHeader == null) {
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
const mapping = this.mappingByHeader[this.editingHeader];
|
|
832
|
+
return (mapping == null ? void 0 : mapping.kind) === 'new' ? mapping : null;
|
|
677
833
|
}
|
|
678
834
|
|
|
679
835
|
/**
|
|
680
|
-
* `onSubmit` handler for the field editor: maps the edited field spec into
|
|
681
|
-
* column being edited (set via `beginFieldEdit`). No-op if nothing is being
|
|
836
|
+
* `onSubmit` handler for the CMS field editor: maps the edited field spec into
|
|
837
|
+
* the column being edited (set via `beginFieldEdit`). No-op if nothing is being
|
|
682
838
|
* edited.
|
|
683
839
|
*/
|
|
684
840
|
applyFieldEdit(field) {
|
|
685
|
-
if (this.
|
|
841
|
+
if (this.editingHeader == null) {
|
|
686
842
|
return;
|
|
687
843
|
}
|
|
688
|
-
this.setNewFieldConfig(this.
|
|
844
|
+
this.setNewFieldConfig(this.editingHeader, (0, _fieldEdit.toFieldEdit)(field));
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* `onSubmit` handler for the DataExtension field editor: maps the edited
|
|
849
|
+
* DataExtension draft (which carries per-audience permissions) into the column
|
|
850
|
+
* being edited. The `destination`-branch counterpart to `applyFieldEdit`.
|
|
851
|
+
*/
|
|
852
|
+
applyDataExtensionFieldEdit(draft) {
|
|
853
|
+
if (this.editingHeader == null) {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
this.setNewFieldConfig(this.editingHeader, (0, _fieldEdit.toDataExtensionFieldEdit)(draft));
|
|
689
857
|
}
|
|
690
858
|
|
|
691
859
|
/**
|
|
@@ -733,15 +901,15 @@ class ImportState {
|
|
|
733
901
|
// The job ran server-side, so a failure arrives on a 200 (the poll
|
|
734
902
|
// succeeded; the job didn't) with no HTTP error to carry the application
|
|
735
903
|
// error. Build a synthetic, HTTP-error-shaped error (the error handler
|
|
736
|
-
// reads the code from `response.data.details.applicationError`)
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
//
|
|
743
|
-
// Without a
|
|
744
|
-
// generic message.
|
|
904
|
+
// reads the code from `response.data.details.applicationError`), then
|
|
905
|
+
// resolve it through the consumer's per-code map via `withErrorHandler` —
|
|
906
|
+
// the same way as `fetchData` etc. `job.error` is the consumer's
|
|
907
|
+
// application error (their import business logic), so only they can map
|
|
908
|
+
// its codes; they supply just the code→message map and we do the
|
|
909
|
+
// wrapping. `withErrorHandler` attaches the map to the error and
|
|
910
|
+
// re-rejects it, so the progress step resolves a consumer-specific
|
|
911
|
+
// message via `getResolvedError`. Without a map the same error still
|
|
912
|
+
// throws, falling back to the generic message.
|
|
745
913
|
const error = Object.assign(new Error(`ImportJob.${((_job$error = job.error) == null ? void 0 : _job$error.code) ?? 'failed'}`), {
|
|
746
914
|
response: {
|
|
747
915
|
data: {
|
|
@@ -751,11 +919,17 @@ class ImportState {
|
|
|
751
919
|
}
|
|
752
920
|
}
|
|
753
921
|
});
|
|
754
|
-
const
|
|
755
|
-
if (
|
|
756
|
-
//
|
|
757
|
-
// `
|
|
758
|
-
|
|
922
|
+
const jobApplicationErrorMap = this.jobApplicationErrorMap;
|
|
923
|
+
if (jobApplicationErrorMap) {
|
|
924
|
+
// Adapt the consumer's code→message map into the error handler's
|
|
925
|
+
// `errorCodesMap`: each entry, when its code matches the job's
|
|
926
|
+
// application error, resolves with that error. `withErrorHandler`
|
|
927
|
+
// attaches the resolved message to `error` and re-rejects it, so the
|
|
928
|
+
// progress step surfaces it via `getResolvedError` — the consumer only
|
|
929
|
+
// supplies the map, not the wrapping.
|
|
930
|
+
const appError = job.error ?? {};
|
|
931
|
+
const errorCodesMap = Object.fromEntries(Object.entries(jobApplicationErrorMap).map(([code, resolve]) => [code, () => resolve(appError)]));
|
|
932
|
+
await this.errorHandler.withErrorHandler(() => Promise.reject(error), errorCodesMap);
|
|
759
933
|
}
|
|
760
934
|
throw error;
|
|
761
935
|
}
|
|
@@ -783,10 +957,16 @@ class ImportState {
|
|
|
783
957
|
};
|
|
784
958
|
}
|
|
785
959
|
_applyJobProgress(job) {
|
|
960
|
+
var _progress$itemsSucces, _progress$itemsSucces2;
|
|
786
961
|
const progress = job.progress;
|
|
787
962
|
const succeeded = (progress == null ? void 0 : progress.itemsSuccessful) ?? 0;
|
|
788
963
|
const failed = (progress == null ? void 0 : progress.itemsRejected) ?? 0;
|
|
789
964
|
const current = (progress == null ? void 0 : progress.current) ?? succeeded + failed;
|
|
965
|
+
// The server splits successful items into created (new) and updated
|
|
966
|
+
// (existing) — the summary step reports each. Absent on older jobs, in
|
|
967
|
+
// which case both stay 0 and only the total `succeeded` is shown.
|
|
968
|
+
const created = (progress == null || (_progress$itemsSucces = progress.itemsSuccessfulInfo) == null ? void 0 : _progress$itemsSucces.itemsCreated) ?? 0;
|
|
969
|
+
const updated = (progress == null || (_progress$itemsSucces2 = progress.itemsSuccessfulInfo) == null ? void 0 : _progress$itemsSucces2.itemsUpdated) ?? 0;
|
|
790
970
|
|
|
791
971
|
// `totalCount` is fixed up front in `startImport` from the CSV row count
|
|
792
972
|
// (one uploaded row per CSV row); polling only advances `processedCount`.
|
|
@@ -794,10 +974,15 @@ class ImportState {
|
|
|
794
974
|
this.processedCount = Math.min(current, this.totalCount);
|
|
795
975
|
this.result = {
|
|
796
976
|
succeeded,
|
|
797
|
-
failed
|
|
977
|
+
failed,
|
|
978
|
+
created,
|
|
979
|
+
updated
|
|
798
980
|
};
|
|
799
981
|
});
|
|
800
982
|
}
|
|
983
|
+
_lingerOnCompletion() {
|
|
984
|
+
return new Promise(resolve => setTimeout(resolve, COMPLETION_LINGER_MS));
|
|
985
|
+
}
|
|
801
986
|
async _runBackup() {
|
|
802
987
|
const backup = this.backup;
|
|
803
988
|
if (!backup) {
|
|
@@ -829,9 +1014,6 @@ class ImportState {
|
|
|
829
1014
|
});
|
|
830
1015
|
return false;
|
|
831
1016
|
}
|
|
832
|
-
_lingerOnCompletion() {
|
|
833
|
-
return new Promise(resolve => setTimeout(resolve, COMPLETION_LINGER_MS));
|
|
834
|
-
}
|
|
835
1017
|
|
|
836
1018
|
/**
|
|
837
1019
|
* Resolves every column to the final destination key used in the upload
|
|
@@ -842,18 +1024,21 @@ class ImportState {
|
|
|
842
1024
|
*/
|
|
843
1025
|
async _resolveFieldMapping() {
|
|
844
1026
|
const resolved = {};
|
|
845
|
-
|
|
1027
|
+
// Pending new columns with their deduped key + display name. Source-neutral;
|
|
1028
|
+
// the per-source field def is built from these in `_createPendingFields`.
|
|
1029
|
+
const pending = [];
|
|
846
1030
|
// Seed with existing field ids/display names so derived keys and names never
|
|
847
1031
|
// collide with them or with each other.
|
|
848
1032
|
const usedKeys = new Set(this.fields.map(field => field.id).filter(id => !!id));
|
|
849
1033
|
const usedNames = new Set(this.fields.map(field => field.header).filter(header => !!header));
|
|
1034
|
+
const canCreate = this.canCreateNewField;
|
|
850
1035
|
for (const header of this.csvHeaders) {
|
|
851
1036
|
const mapping = this.mappingByHeader[header];
|
|
852
1037
|
if (mapping == null) {
|
|
853
1038
|
resolved[header] = null;
|
|
854
1039
|
} else if (mapping.kind === 'existing') {
|
|
855
1040
|
resolved[header] = mapping.fieldId;
|
|
856
|
-
} else if (
|
|
1041
|
+
} else if (canCreate) {
|
|
857
1042
|
var _mapping$displayName;
|
|
858
1043
|
// The user's name override (from the field editor) seeds the unique
|
|
859
1044
|
// name/key derivation; otherwise fall back to the column header.
|
|
@@ -864,29 +1049,79 @@ class ImportState {
|
|
|
864
1049
|
} = uniqueFieldNameAndKey(baseName, usedNames, usedKeys);
|
|
865
1050
|
usedKeys.add(key);
|
|
866
1051
|
usedNames.add(displayName);
|
|
867
|
-
|
|
1052
|
+
pending.push({
|
|
868
1053
|
key,
|
|
869
|
-
type: mapping.fieldType,
|
|
870
1054
|
displayName,
|
|
871
|
-
|
|
872
|
-
config: mapping.config
|
|
873
|
-
})
|
|
1055
|
+
mapping
|
|
874
1056
|
});
|
|
875
1057
|
resolved[header] = key;
|
|
876
1058
|
} else {
|
|
877
|
-
// "new" mapping
|
|
1059
|
+
// "new" mapping with no create callback on this source — nothing to map into.
|
|
878
1060
|
resolved[header] = null;
|
|
879
1061
|
}
|
|
880
1062
|
}
|
|
881
|
-
if (
|
|
882
|
-
|
|
1063
|
+
if (pending.length > 0) {
|
|
1064
|
+
// Stash the returned fields map to merge into the table on success; kept off
|
|
1065
|
+
// the observable until then so columns don't update mid-import.
|
|
1066
|
+
this._pendingFieldsMap = await this._createPendingFields(pending);
|
|
883
1067
|
}
|
|
884
1068
|
return resolved;
|
|
885
1069
|
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* Creates the pending new fields via the active `dataSource` branch's callback
|
|
1073
|
+
* — `updateSchema` (CMS) or `createFields` (DataExtension) — building the
|
|
1074
|
+
* source-specific field def from the shared, already-deduped pending columns.
|
|
1075
|
+
* The callback may throw; `startImport` surfaces that as the error state.
|
|
1076
|
+
*/
|
|
1077
|
+
async _createPendingFields(pending) {
|
|
1078
|
+
const updateSchema = this.updateSchema;
|
|
1079
|
+
const createDataExtensionFields = this.createDataExtensionFields;
|
|
1080
|
+
let updated;
|
|
1081
|
+
if (updateSchema) {
|
|
1082
|
+
updated = await updateSchema(pending.map(({
|
|
1083
|
+
key,
|
|
1084
|
+
displayName,
|
|
1085
|
+
mapping
|
|
1086
|
+
}) => ({
|
|
1087
|
+
key,
|
|
1088
|
+
type: mapping.fieldType,
|
|
1089
|
+
displayName,
|
|
1090
|
+
...(mapping.config && {
|
|
1091
|
+
config: mapping.config
|
|
1092
|
+
})
|
|
1093
|
+
})));
|
|
1094
|
+
} else if (createDataExtensionFields) {
|
|
1095
|
+
// The DataExtension source builds its own field shape from the neutral
|
|
1096
|
+
// pending columns — no DataExtension-specific def-building in core.
|
|
1097
|
+
updated = await createDataExtensionFields(pending);
|
|
1098
|
+
} else {
|
|
1099
|
+
return undefined;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Guard the empty map: SchemaState.mergeFields is a full replace, so merging
|
|
1103
|
+
// `{}` would wipe every column — treat a missing/empty return as "nothing to
|
|
1104
|
+
// merge".
|
|
1105
|
+
return updated && Object.keys(updated).length > 0 ? updated : undefined;
|
|
1106
|
+
}
|
|
886
1107
|
async startImport() {
|
|
887
1108
|
if (!this.hasMappings) {
|
|
888
1109
|
return;
|
|
889
1110
|
}
|
|
1111
|
+
|
|
1112
|
+
// Block import while any required destination field is unmapped and tell the
|
|
1113
|
+
// user which (CAIRO-4452). The footer already flags this; the Import button
|
|
1114
|
+
// stays enabled, so this is where the click is stopped.
|
|
1115
|
+
if (this.hasMissingRequiredFields) {
|
|
1116
|
+
var _this$showToast3;
|
|
1117
|
+
(_this$showToast3 = this.showToast) == null || _this$showToast3.call(this, {
|
|
1118
|
+
type: 'ERROR',
|
|
1119
|
+
message: this.translate('cairo.import.missingRequiredMessage', {
|
|
1120
|
+
fields: this._missingRequiredFieldNames()
|
|
1121
|
+
})
|
|
1122
|
+
});
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
890
1125
|
this._cancelled = false;
|
|
891
1126
|
this._abortController = new AbortController();
|
|
892
1127
|
|
|
@@ -905,9 +1140,14 @@ class ImportState {
|
|
|
905
1140
|
};
|
|
906
1141
|
this.result = {
|
|
907
1142
|
succeeded: 0,
|
|
908
|
-
failed: 0
|
|
1143
|
+
failed: 0,
|
|
1144
|
+
created: 0,
|
|
1145
|
+
updated: 0
|
|
909
1146
|
};
|
|
910
1147
|
this._jobId = null;
|
|
1148
|
+
// Drop any fields map from a prior attempt so a no-new-fields "Try again"
|
|
1149
|
+
// can't merge stale fields.
|
|
1150
|
+
this._pendingFieldsMap = undefined;
|
|
911
1151
|
});
|
|
912
1152
|
const backupFailed = await this._runBackup();
|
|
913
1153
|
if (backupFailed) {
|
|
@@ -940,6 +1180,12 @@ class ImportState {
|
|
|
940
1180
|
..._model.queryStatus.loading,
|
|
941
1181
|
promise
|
|
942
1182
|
};
|
|
1183
|
+
|
|
1184
|
+
// Assigned only on the success path below, so the post-import merge (after the
|
|
1185
|
+
// try/catch) fires only for a completed import — without re-checking status
|
|
1186
|
+
// there. Reading `_pendingFieldsMap` directly wouldn't do: it's set pre-upload,
|
|
1187
|
+
// so it stays truthy even when the job later fails.
|
|
1188
|
+
let createdFields;
|
|
943
1189
|
try {
|
|
944
1190
|
await promise;
|
|
945
1191
|
if (this._cancelled) {
|
|
@@ -950,25 +1196,32 @@ class ImportState {
|
|
|
950
1196
|
..._model.queryStatus.success,
|
|
951
1197
|
data: this.result
|
|
952
1198
|
};
|
|
1199
|
+
// The job reports counts only when it finishes, so the bar sat at 0 for
|
|
1200
|
+
// the whole import (CAIRO-4476). Snap it to 100% and hold it (below)
|
|
1201
|
+
// before the summary step replaces the progress step, so the user sees
|
|
1202
|
+
// the import reach completion.
|
|
1203
|
+
this.processedCount = this.totalCount;
|
|
953
1204
|
});
|
|
954
1205
|
|
|
955
|
-
//
|
|
956
|
-
//
|
|
957
|
-
// the
|
|
958
|
-
|
|
959
|
-
if (this.
|
|
960
|
-
|
|
961
|
-
if (this._cancelled) {
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
1206
|
+
// Keep the completed bar on screen briefly before moving on. `importStatus`
|
|
1207
|
+
// is already success, so a close during this window just dismisses the
|
|
1208
|
+
// modal (the `close()` guard doesn't treat it as a cancel).
|
|
1209
|
+
await this._lingerOnCompletion();
|
|
1210
|
+
if (this._cancelled) {
|
|
1211
|
+
return;
|
|
964
1212
|
}
|
|
1213
|
+
|
|
1214
|
+
// Success path only. `_pendingFieldsMap` holds this run's returned map — it's
|
|
1215
|
+
// cleared at the start of `startImport` (the stale-guard) and set by
|
|
1216
|
+
// `_resolveFieldMapping`; snapshotting here keeps the merge success-gated.
|
|
1217
|
+
createdFields = this._pendingFieldsMap;
|
|
965
1218
|
(0, _mobx.runInAction)(() => {
|
|
966
|
-
//
|
|
967
|
-
//
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
this.
|
|
1219
|
+
// Import finished: show the summary step (created / updated / failed
|
|
1220
|
+
// counts, plus a "Download CSV" of the failed rows when any failed).
|
|
1221
|
+
// The modal stays open; the user dismisses it from the summary via
|
|
1222
|
+
// `done`. Refresh the collection now so the table behind the modal is
|
|
1223
|
+
// up to date by the time it closes.
|
|
1224
|
+
this.currentStep = 'summary';
|
|
972
1225
|
this.collection.clearResultAndMoveToStart({
|
|
973
1226
|
force: true
|
|
974
1227
|
});
|
|
@@ -981,6 +1234,21 @@ class ImportState {
|
|
|
981
1234
|
};
|
|
982
1235
|
});
|
|
983
1236
|
}
|
|
1237
|
+
|
|
1238
|
+
// Merge the created fields into the table so the new columns appear with no
|
|
1239
|
+
// manual reload (CAIRO-4400). Called imperatively here, outside the try/catch
|
|
1240
|
+
// above, so a merge failure can't flip a completed import into the error state
|
|
1241
|
+
// (whose "Try again" re-imports every row). `createdFields` is set only on the
|
|
1242
|
+
// success path and only when fields were created.
|
|
1243
|
+
if (createdFields) {
|
|
1244
|
+
try {
|
|
1245
|
+
var _this$config$mergeFie, _this$config;
|
|
1246
|
+
(_this$config$mergeFie = (_this$config = this.config).mergeFields) == null || _this$config$mergeFie.call(_this$config, createdFields);
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
// eslint-disable-next-line no-console
|
|
1249
|
+
console.error('ImportState config.mergeFields failed', error);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
984
1252
|
}
|
|
985
1253
|
}
|
|
986
1254
|
exports.ImportState = ImportState;
|