pict-section-dataimport 1.0.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/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +80 -0
- package/source/Pict-Section-DataImport-CSS.js +73 -0
- package/source/Pict-Section-DataImport.js +54 -0
- package/source/providers/Pict-Provider-DataImport-Meadow.js +192 -0
- package/source/providers/Pict-Provider-DataImport.js +149 -0
- package/source/seams/DataImport-Parser-Delimited.js +128 -0
- package/source/seams/DataImport-Parser-FixedWidth.js +87 -0
- package/source/seams/DataImport-Parser-Xlsx.js +81 -0
- package/source/seams/DataImport-ParserProvider.js +123 -0
- package/source/seams/DataImport-PushTarget.js +82 -0
- package/source/seams/DataImport-SchemaProvider.js +72 -0
- package/source/seams/DataImport-StateStore.js +123 -0
- package/source/services/DataImport-ComprehensionBuilder.js +123 -0
- package/source/services/DataImport-Session.js +101 -0
- package/source/views/PictView-DataImport-Wizard.js +657 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
const libPictView = require('pict-view');
|
|
2
|
+
const libSession = require('../services/DataImport-Session.js');
|
|
3
|
+
|
|
4
|
+
// Required sibling controls (peers). The wizard registers their providers if the host hasn't.
|
|
5
|
+
const libPictSectionAccordion = require('pict-section-accordion');
|
|
6
|
+
const libPictSectionUpload = require('pict-section-upload');
|
|
7
|
+
|
|
8
|
+
const _STEP_DEFS =
|
|
9
|
+
[
|
|
10
|
+
{ Hash: 'upload', Title: 'Upload' },
|
|
11
|
+
{ Hash: 'detect', Title: 'Validate' },
|
|
12
|
+
{ Hash: 'mapping', Title: 'Map' },
|
|
13
|
+
{ Hash: 'generate', Title: 'Generate' },
|
|
14
|
+
{ Hash: 'push', Title: 'Push' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
/** @type {Record<string, any>} */
|
|
18
|
+
const _DEFAULT_CONFIGURATION =
|
|
19
|
+
{
|
|
20
|
+
ViewIdentifier: 'Pict-Section-DataImport-Wizard',
|
|
21
|
+
|
|
22
|
+
AutoInitialize: true,
|
|
23
|
+
AutoRender: false,
|
|
24
|
+
AutoSolveWithApp: false,
|
|
25
|
+
|
|
26
|
+
DefaultRenderable: 'DI-Root',
|
|
27
|
+
|
|
28
|
+
WizardHash: false,
|
|
29
|
+
DestinationAddress: false,
|
|
30
|
+
RenderMode: 'wizard',
|
|
31
|
+
URLPrefix: '/1.0/',
|
|
32
|
+
GUIDPrefix: 'INTG-DEF',
|
|
33
|
+
EntityGUIDPrefix: '',
|
|
34
|
+
AllowGUIDTruncation: false,
|
|
35
|
+
AllowedFileTypes: [ 'csv', 'tsv', 'xlsx', 'fixedwidth' ],
|
|
36
|
+
SchemaSource: 'meadow',
|
|
37
|
+
PushMode: 'entityprovider',
|
|
38
|
+
// A host can ship a starting mapping (entities + GUID templates + fan-out solvers); the UI edits it.
|
|
39
|
+
DefaultMapping: null,
|
|
40
|
+
// Resolved seams (injected by the provider's createImportWizard):
|
|
41
|
+
ResolvedParsers: null,
|
|
42
|
+
ResolvedSchemaProvider: null,
|
|
43
|
+
ResolvedPushTarget: null,
|
|
44
|
+
ResolvedStateStore: null,
|
|
45
|
+
ComprehensionBuilder: null,
|
|
46
|
+
// Callbacks:
|
|
47
|
+
OnStepChange: false, OnSchemaLoaded: false, OnComprehension: false, OnPushComplete: false, OnError: false,
|
|
48
|
+
// Allow a JSON download of the generated comprehension (handy when there's no server to push to).
|
|
49
|
+
AllowDownload: true,
|
|
50
|
+
|
|
51
|
+
Templates:
|
|
52
|
+
[
|
|
53
|
+
{ Hash: 'DI-Root', Template: /*html*/`<div class="psd" id="DI_{~D:Record.WizardHash~}"><div id="DI_Acc_{~D:Record.WizardHash~}"></div></div>` },
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
Hash: 'DI-Body-Upload',
|
|
57
|
+
Template: /*html*/`<div class="psd-step"><p class="psd-hint">Upload a CSV, TSV, Excel, or fixed-width file. Its bytes are read in your browser for the next steps.</p><div id="DI_Upload_{~D:Record.WizardHash~}"></div></div>`
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
{
|
|
61
|
+
Hash: 'DI-Body-Detect',
|
|
62
|
+
Template: /*html*/`
|
|
63
|
+
<div class="psd-step">
|
|
64
|
+
<div class="psd-options">
|
|
65
|
+
<div class="psd-field"><label>Format</label>
|
|
66
|
+
<select class="psd-select" onchange="_Pict.views['{~D:Record.WizardHash~}'].setParseKind(this.value)">{~TS:DI-Opt:Record.KindOptions~}</select></div>
|
|
67
|
+
{~TS:DI-Detect-Delimited:Record.DelimitedSlot~}
|
|
68
|
+
</div>
|
|
69
|
+
{~TS:DI-Detect-Ruler:Record.RulerSlot~}
|
|
70
|
+
<p class="psd-section-title">Detected columns <span class="psd-type">({~D:Record.ColumnCount~} columns · ~{~D:Record.RowCountEstimate~} rows)</span></p>
|
|
71
|
+
<div class="psd-table-wrap"><table class="psd-table"><thead><tr>{~TS:DI-Col-Head:Record.SampleHeader~}</tr></thead><tbody>{~TS:DI-Sample-Row:Record.SampleRows~}</tbody></table></div>
|
|
72
|
+
</div>`
|
|
73
|
+
},
|
|
74
|
+
{ Hash: 'DI-Opt', Template: /*html*/`<option value="{~D:Record.Value~}"{~NE:Record.Selected^ selected~}>{~D:Record.Label~}</option>` },
|
|
75
|
+
{
|
|
76
|
+
Hash: 'DI-Detect-Delimited',
|
|
77
|
+
Template: /*html*/`<div class="psd-field"><label>Delimiter</label><select class="psd-select" onchange="_Pict.views['{~D:Record.WizardHash~}'].setDelimiter(this.value)">{~TS:DI-Opt:Record.DelimiterOptions~}</select></div><div class="psd-field"><label><input type="checkbox"{~NE:Record.HasHeader^ checked~} onchange="_Pict.views['{~D:Record.WizardHash~}'].setHasHeader(this.checked)" /> First row is a header</label></div>` },
|
|
78
|
+
{ Hash: 'DI-Detect-Ruler', Template: /*html*/`<div><p class="psd-hint">Fixed-width: enter column boundaries as JSON (Name/Start/End), then re-detect.</p><div class="psd-ruler">{~D:Record.Ruler~}</div><textarea class="psd-textarea" id="DI_FW_{~D:Record.WizardHash~}" placeholder='[{"Name":"code","Start":0,"End":3}]'>{~D:Record.ColumnsJSON~}</textarea><div class="psd-row-actions"><button type="button" class="psd-btn psd-btn-ghost" onclick="_Pict.views['{~D:Record.WizardHash~}'].applyFixedWidthColumns()">Apply boundaries</button></div></div>` },
|
|
79
|
+
{ Hash: 'DI-Col-Head', Template: /*html*/`<th>{~D:Record.SourceName~}<div class="psd-type">{~D:Record.InferredType~}</div></th>` },
|
|
80
|
+
{ Hash: 'DI-Sample-Row', Template: /*html*/`<tr>{~TS:DI-Sample-Cell:Record.Cells~}</tr>` },
|
|
81
|
+
{ Hash: 'DI-Sample-Cell', Template: /*html*/`<td title="{~D:Record.Value~}">{~D:Record.Value~}</td>` },
|
|
82
|
+
|
|
83
|
+
{
|
|
84
|
+
Hash: 'DI-Body-Mapping',
|
|
85
|
+
Template: /*html*/`
|
|
86
|
+
<div class="psd-step">
|
|
87
|
+
<p class="psd-hint">Map each target field to a source column. Foreign-key + fan-out rules come from the starting mapping (edit the raw JSON for advanced control).</p>
|
|
88
|
+
{~TS:DI-Map-Entity:Record.Entities~}
|
|
89
|
+
<details class="psd-raw"><summary class="psd-hint">Advanced — raw mapping JSON</summary><textarea class="psd-textarea" id="DI_RawMap_{~D:Record.WizardHash~}">{~D:Record.RawJSON~}</textarea><div class="psd-row-actions"><button type="button" class="psd-btn psd-btn-ghost" onclick="_Pict.views['{~D:Record.WizardHash~}'].applyRawMapping()">Apply raw mapping</button></div></details>
|
|
90
|
+
</div>`
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
Hash: 'DI-Map-Entity',
|
|
94
|
+
Template: /*html*/`
|
|
95
|
+
<div class="psd-entity">
|
|
96
|
+
<div class="psd-entity-head"><span class="psd-entity-name">{~D:Record.Entity~}</span><span class="psd-entity-count">GUID: <span class="psd-mono">{~D:Record.GUIDTemplate~}</span></span></div>
|
|
97
|
+
{~TS:DI-Map-Row:Record.Fields~}
|
|
98
|
+
</div>`
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
Hash: 'DI-Map-Row',
|
|
102
|
+
Template: /*html*/`<div class="psd-map-row"><span class="psd-map-field">{~D:Record.Name~}{~NE:Record.Required^ <span class="psd-map-required">*</span>~}</span><select class="psd-select" onchange="_Pict.views['{~D:Record.WizardHash~}'].setMappingBinding('{~D:Record.Entity~}','{~D:Record.Name~}',this.value)">{~TS:DI-Opt:Record.Options~}</select></div>`
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
{
|
|
106
|
+
Hash: 'DI-Body-Generate',
|
|
107
|
+
Template: /*html*/`
|
|
108
|
+
<div class="psd-step">
|
|
109
|
+
<div class="psd-row-actions"><button type="button" class="psd-btn" onclick="_Pict.views['{~D:Record.WizardHash~}'].generate()">Generate comprehension</button>{~TS:DI-Download:Record.DownloadSlot~}</div>
|
|
110
|
+
{~TS:DI-Warnings:Record.Warnings~}
|
|
111
|
+
{~TS:DI-Gen-Report:Record.ReportSlot~}
|
|
112
|
+
</div>`
|
|
113
|
+
},
|
|
114
|
+
{ Hash: 'DI-Download', Template: /*html*/`<button type="button" class="psd-btn psd-btn-ghost" onclick="_Pict.views['{~D:Record.WizardHash~}'].downloadComprehension()">Download JSON</button>` },
|
|
115
|
+
{ Hash: 'DI-Warnings', Template: /*html*/`<div class="psd-banner psd-banner-warn">{~I:Warning~} <span>{~D:Record.Message~}</span></div>` },
|
|
116
|
+
{
|
|
117
|
+
Hash: 'DI-Gen-Report',
|
|
118
|
+
Template: /*html*/`<div><div class="psd-banner psd-banner-ok">{~I:Check~} <span>Generated {~D:Record.ParsedRowCount~} rows into {~D:Record.EntityTotal~} record(s).</span></div><div class="psd-report">{~TS:DI-Count-Chip:Record.EntityCounts~}</div>{~TS:DI-Preview:Record.PreviewSlot~}</div>` },
|
|
119
|
+
{ Hash: 'DI-Count-Chip', Template: /*html*/`<span class="psd-chip"><strong>{~D:Record.Count~}</strong> {~D:Record.Entity~}</span>` },
|
|
120
|
+
{ Hash: 'DI-Preview', Template: /*html*/`<div><p class="psd-section-title">Preview <span class="psd-type">({~D:Record.Entity~}, first {~D:Record.ShownCount~})</span></p><div class="psd-table-wrap"><table class="psd-table"><thead><tr>{~TS:DI-Col-Head2:Record.Header~}</tr></thead><tbody>{~TS:DI-Sample-Row:Record.Rows~}</tbody></table></div></div>` },
|
|
121
|
+
{ Hash: 'DI-Col-Head2', Template: /*html*/`<th>{~D:Record.Name~}</th>` },
|
|
122
|
+
|
|
123
|
+
{
|
|
124
|
+
Hash: 'DI-Body-Push',
|
|
125
|
+
Template: /*html*/`
|
|
126
|
+
<div class="psd-step">
|
|
127
|
+
<p class="psd-hint">Push mode: <span class="psd-mono">{~D:Record.Mode~}</span></p>
|
|
128
|
+
<div class="psd-row-actions"><button type="button" class="psd-btn" onclick="_Pict.views['{~D:Record.WizardHash~}'].push()"{~NE:Record.PushDisabled^ disabled~}>{~D:Record.PushLabel~}</button></div>
|
|
129
|
+
{~TS:DI-Push-Progress:Record.ProgressSlot~}
|
|
130
|
+
{~TS:DI-Push-Result:Record.ResultSlot~}
|
|
131
|
+
{~TS:DI-Push-Error:Record.ErrorSlot~}
|
|
132
|
+
</div>`
|
|
133
|
+
},
|
|
134
|
+
{ Hash: 'DI-Push-Progress', Template: /*html*/`<div class="psd-progress"><div class="psd-progress-fill" style="width: {~D:Record.Pct~}%;"></div></div>` },
|
|
135
|
+
{ Hash: 'DI-Push-Result', Template: /*html*/`<div class="psd-banner psd-banner-ok">{~I:Check~} <span>{~D:Record.Message~}</span></div>` },
|
|
136
|
+
{ Hash: 'DI-Push-Error', Template: /*html*/`<div class="psd-banner psd-banner-error">{~I:Error~} <span>{~D:Record.Message~}</span></div>` },
|
|
137
|
+
],
|
|
138
|
+
|
|
139
|
+
Renderables:
|
|
140
|
+
[
|
|
141
|
+
{ RenderableHash: 'DI-Root', TemplateHash: 'DI-Root', RenderMethod: 'replace' },
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
class PictViewDataImportWizard extends libPictView
|
|
146
|
+
{
|
|
147
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
148
|
+
{
|
|
149
|
+
let tmpOptions = Object.assign({}, _DEFAULT_CONFIGURATION, pOptions);
|
|
150
|
+
super(pFable, tmpOptions, pServiceHash);
|
|
151
|
+
|
|
152
|
+
this.options.DefaultDestinationAddress = this.options.DestinationAddress || `#${this.options.WizardHash}`;
|
|
153
|
+
this._StateAddress = `AppData.DataImport.Sessions.${this.options.WizardHash}`;
|
|
154
|
+
this.options.DefaultTemplateRecordAddress = `AppData.DataImport._WizardConfig.${this.options.WizardHash}`;
|
|
155
|
+
if (Array.isArray(this.options.Renderables) && this.options.Renderables[0])
|
|
156
|
+
{
|
|
157
|
+
this.options.Renderables[0].ContentDestinationAddress = this.options.DefaultDestinationAddress;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this._accHash = `${this.options.WizardHash}-Accordion`;
|
|
161
|
+
this._uploadHash = `${this.options.WizardHash}-Upload`;
|
|
162
|
+
this._accordion = null;
|
|
163
|
+
this._uploadView = null;
|
|
164
|
+
this._fileHandle = null; // the live upload handle (off AppData)
|
|
165
|
+
this._parsedRows = null; // cache of parsed rows
|
|
166
|
+
this._schema = null; // resolved target schema
|
|
167
|
+
this._wired = false;
|
|
168
|
+
|
|
169
|
+
// The render record for DI-Root just needs WizardHash.
|
|
170
|
+
this.pict.AppData.DataImport = this.pict.AppData.DataImport || {};
|
|
171
|
+
this.pict.AppData.DataImport._WizardConfig = this.pict.AppData.DataImport._WizardConfig || {};
|
|
172
|
+
this.pict.AppData.DataImport._WizardConfig[this.options.WizardHash] = { WizardHash: this.options.WizardHash };
|
|
173
|
+
|
|
174
|
+
this._ensureProviders();
|
|
175
|
+
this._session(); // seed the session
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Register the sibling providers if the host hasn't already. */
|
|
179
|
+
_ensureProviders()
|
|
180
|
+
{
|
|
181
|
+
if (!this.pict.providers['Pict-Section-Accordion'])
|
|
182
|
+
{
|
|
183
|
+
this.pict.addProvider('Pict-Section-Accordion', libPictSectionAccordion.default_configuration, libPictSectionAccordion);
|
|
184
|
+
}
|
|
185
|
+
if (!this.pict.providers['Pict-Section-Upload'])
|
|
186
|
+
{
|
|
187
|
+
this.pict.addProvider('Pict-Section-Upload', libPictSectionUpload.default_configuration, libPictSectionUpload);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** @return {Record<string, any>} The session state slot (created on first access). */
|
|
192
|
+
_session()
|
|
193
|
+
{
|
|
194
|
+
this.pict.AppData.DataImport = this.pict.AppData.DataImport || {};
|
|
195
|
+
this.pict.AppData.DataImport.Sessions = this.pict.AppData.DataImport.Sessions || {};
|
|
196
|
+
if (!this.pict.AppData.DataImport.Sessions[this.options.WizardHash])
|
|
197
|
+
{
|
|
198
|
+
const tmpSession = libSession.newImportSession(this.options.WizardHash, { Title: this.options.Title, SchemaSource: this.options.SchemaSource, PushMode: this.options.PushMode });
|
|
199
|
+
if (this.options.DefaultMapping) { tmpSession.Mapping = JSON.parse(JSON.stringify(this.options.DefaultMapping)); tmpSession.Mapping.TargetSchemaSource = this.options.SchemaSource; }
|
|
200
|
+
this.pict.AppData.DataImport.Sessions[this.options.WizardHash] = tmpSession;
|
|
201
|
+
}
|
|
202
|
+
return this.pict.AppData.DataImport.Sessions[this.options.WizardHash];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
onAfterRender(pRenderable)
|
|
206
|
+
{
|
|
207
|
+
if (this.pict.CSSMap && typeof this.pict.CSSMap.injectCSS === 'function') { this.pict.CSSMap.injectCSS(); }
|
|
208
|
+
if (!this._wired)
|
|
209
|
+
{
|
|
210
|
+
this._wired = true;
|
|
211
|
+
this._buildWizard();
|
|
212
|
+
}
|
|
213
|
+
return super.onAfterRender(pRenderable);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** One-time: create the accordion + upload view, render all step bodies. */
|
|
217
|
+
_buildWizard()
|
|
218
|
+
{
|
|
219
|
+
const tmpAccordionProvider = this.pict.providers['Pict-Section-Accordion'];
|
|
220
|
+
const tmpSteps = _STEP_DEFS.map((pStep) => Object.assign({}, pStep, { CanAdvance: () => this._canAdvance(pStep.Hash) }));
|
|
221
|
+
this._accordion = tmpAccordionProvider.createAccordion(this._accHash,
|
|
222
|
+
{
|
|
223
|
+
DestinationAddress: `#DI_Acc_${this.options.WizardHash}`,
|
|
224
|
+
RenderMode: this.options.RenderMode,
|
|
225
|
+
Steps: tmpSteps,
|
|
226
|
+
OnStepChange: (pTo) => this._onEnterStep(pTo && pTo.Hash),
|
|
227
|
+
OnComplete: () => { /* last step (push) Next: no-op; push is an explicit button */ },
|
|
228
|
+
});
|
|
229
|
+
this.pict.views[this._accHash].render();
|
|
230
|
+
|
|
231
|
+
// Render each step's body into its accordion container.
|
|
232
|
+
_STEP_DEFS.forEach((pStep) => this._renderStepBody(pStep.Hash));
|
|
233
|
+
|
|
234
|
+
// Create the upload control into the upload step body.
|
|
235
|
+
this._createUploader();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** @return {boolean} Per-step Next gate. */
|
|
239
|
+
_canAdvance(pStepHash)
|
|
240
|
+
{
|
|
241
|
+
const tmpSession = this._session();
|
|
242
|
+
switch (pStepHash)
|
|
243
|
+
{
|
|
244
|
+
case 'upload': return !!tmpSession.File;
|
|
245
|
+
case 'detect': return Array.isArray(tmpSession.DetectedColumns) && tmpSession.DetectedColumns.length > 0;
|
|
246
|
+
case 'mapping': return !!(tmpSession.Mapping && tmpSession.Mapping.Order && tmpSession.Mapping.Order.length > 0);
|
|
247
|
+
case 'generate': return !!tmpSession.Comprehension;
|
|
248
|
+
default: return true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Step-entry side effects: parse on entering detect, load schema on entering mapping. */
|
|
253
|
+
_onEnterStep(pStepHash)
|
|
254
|
+
{
|
|
255
|
+
if (typeof this.options.OnStepChange === 'function') { try { this.options.OnStepChange(pStepHash); } catch (pError) { /* host callback */ } }
|
|
256
|
+
if (pStepHash === 'detect') { this._runDetect(); }
|
|
257
|
+
else if (pStepHash === 'mapping') { this._loadSchemaAndMapping(); }
|
|
258
|
+
else if (pStepHash === 'generate') { this._renderStepBody('generate'); }
|
|
259
|
+
else if (pStepHash === 'push') { this._renderStepBody('push'); }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_createUploader()
|
|
263
|
+
{
|
|
264
|
+
const tmpUploadProvider = this.pict.providers['Pict-Section-Upload'];
|
|
265
|
+
const tmpAccept = this._acceptString();
|
|
266
|
+
tmpUploadProvider.createUploader(this._uploadHash,
|
|
267
|
+
{
|
|
268
|
+
DestinationAddress: `#DI_Upload_${this.options.WizardHash}`,
|
|
269
|
+
Accept: tmpAccept,
|
|
270
|
+
DropLabel: 'Drop your data file here, or',
|
|
271
|
+
OnComplete: (pHandle) => this._onFileReady(pHandle),
|
|
272
|
+
OnRemove: () => { this._fileHandle = null; this._parsedRows = null; this._session().File = null; this._refreshAccordionChrome(); },
|
|
273
|
+
});
|
|
274
|
+
this._uploadView = this.pict.views[this._uploadHash];
|
|
275
|
+
this._uploadView.render();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** @return {string} The accept filter derived from AllowedFileTypes. */
|
|
279
|
+
_acceptString()
|
|
280
|
+
{
|
|
281
|
+
const tmpMap = { csv: '.csv,text/csv', tsv: '.tsv,.tab', xlsx: '.xlsx,.xls', fixedwidth: '.txt,.dat,.fw' };
|
|
282
|
+
return (this.options.AllowedFileTypes || []).map((pKind) => tmpMap[pKind] || '').filter(Boolean).join(',');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
_onFileReady(pHandle)
|
|
286
|
+
{
|
|
287
|
+
this._fileHandle = pHandle;
|
|
288
|
+
this._parsedRows = null;
|
|
289
|
+
const tmpKind = this._inferKind(pHandle);
|
|
290
|
+
const tmpSession = this._session();
|
|
291
|
+
tmpSession.File = { Ref: `upload:${this._uploadHash}:${pHandle.Hash}`, Name: pHandle.Name, Size: pHandle.Size, Type: pHandle.Type, Kind: tmpKind, Stored: false, StorageRef: null };
|
|
292
|
+
tmpSession.ParseConfig.Kind = tmpKind;
|
|
293
|
+
tmpSession.StepStatus.upload = 'complete';
|
|
294
|
+
if (this._accordion) { this._accordion.setStepComplete('upload', true); }
|
|
295
|
+
this._persist();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** @return {string} Infer the parser kind from the file name/type. */
|
|
299
|
+
_inferKind(pHandle)
|
|
300
|
+
{
|
|
301
|
+
const tmpName = (pHandle.Name || '').toLowerCase();
|
|
302
|
+
const tmpType = (pHandle.Type || '').toLowerCase();
|
|
303
|
+
if (/\.(xlsx|xls)$/.test(tmpName) || tmpType.indexOf('spreadsheet') >= 0 || tmpType.indexOf('excel') >= 0) { return 'xlsx'; }
|
|
304
|
+
if (/\.(tsv|tab)$/.test(tmpName) || tmpType.indexOf('tab-separated') >= 0) { return 'tsv'; }
|
|
305
|
+
if (/\.(fw|dat)$/.test(tmpName)) { return 'fixedwidth'; }
|
|
306
|
+
return 'csv';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** @return {any} The active parser for the session's file kind. */
|
|
310
|
+
_activeParser()
|
|
311
|
+
{
|
|
312
|
+
const tmpKind = this._session().ParseConfig.Kind || 'csv';
|
|
313
|
+
const tmpParsers = this.options.ResolvedParsers || {};
|
|
314
|
+
return tmpParsers[tmpKind] || tmpParsers.csv;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// --- Step 2: detect ---
|
|
318
|
+
|
|
319
|
+
_runDetect()
|
|
320
|
+
{
|
|
321
|
+
const tmpParser = this._activeParser();
|
|
322
|
+
const tmpSession = this._session();
|
|
323
|
+
if (!this._fileHandle || !tmpParser) { this._renderStepBody('detect'); return; }
|
|
324
|
+
tmpParser.detect(this._fileHandle, tmpSession.ParseConfig).then((pDetection) =>
|
|
325
|
+
{
|
|
326
|
+
tmpSession.DetectedColumns = pDetection.Columns || [];
|
|
327
|
+
tmpSession.SampleRows = pDetection.SampleRows || [];
|
|
328
|
+
tmpSession.RowCountEstimate = pDetection.RowCountEstimate || 0;
|
|
329
|
+
tmpSession.RawLines = pDetection.RawLines || [];
|
|
330
|
+
tmpSession.StepStatus.detect = (tmpSession.DetectedColumns.length > 0) ? 'complete' : 'active';
|
|
331
|
+
this._parsedRows = null;
|
|
332
|
+
this._persist();
|
|
333
|
+
this._renderStepBody('detect');
|
|
334
|
+
this._refreshAccordionChrome();
|
|
335
|
+
}).catch((pError) => { this._reportError(pError); });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
setParseKind(pKind) { this._session().ParseConfig.Kind = pKind; this._runDetect(); }
|
|
339
|
+
setDelimiter(pValue) { this._session().ParseConfig.Delimited.Delimiter = (pValue === 'tab') ? '\t' : pValue; this._runDetect(); }
|
|
340
|
+
setHasHeader(pChecked) { this._session().ParseConfig.Delimited.HasHeader = !!pChecked; this._runDetect(); }
|
|
341
|
+
applyFixedWidthColumns()
|
|
342
|
+
{
|
|
343
|
+
const tmpElement = (typeof document !== 'undefined') ? document.getElementById(`DI_FW_${this.options.WizardHash}`) : null;
|
|
344
|
+
if (!tmpElement) { return; }
|
|
345
|
+
try { this._session().ParseConfig.FixedWidth.Columns = JSON.parse(tmpElement.value || '[]'); this._runDetect(); }
|
|
346
|
+
catch (pError) { this._reportError(new Error('Fixed-width columns must be valid JSON.')); }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
_detectRenderState()
|
|
350
|
+
{
|
|
351
|
+
const tmpSession = this._session();
|
|
352
|
+
const tmpKind = tmpSession.ParseConfig.Kind || 'csv';
|
|
353
|
+
const tmpKindOptions = [ 'csv', 'tsv', 'xlsx', 'fixedwidth' ].map((pK) => ({ Value: pK, Label: pK.toUpperCase(), Selected: (pK === tmpKind) }));
|
|
354
|
+
const tmpHeader = (tmpSession.DetectedColumns || []).map((pColumn) => ({ SourceName: pColumn.SourceName, InferredType: pColumn.InferredType }));
|
|
355
|
+
const tmpSampleRows = (tmpSession.SampleRows || []).slice(0, 12).map((pRow) =>
|
|
356
|
+
({ Cells: (tmpSession.DetectedColumns || []).map((pColumn) => ({ Value: pRow[pColumn.SourceName] })) }));
|
|
357
|
+
const tmpIsDelimited = (tmpKind === 'csv' || tmpKind === 'tsv');
|
|
358
|
+
const tmpIsFixed = (tmpKind === 'fixedwidth');
|
|
359
|
+
return {
|
|
360
|
+
WizardHash: this.options.WizardHash,
|
|
361
|
+
KindOptions: tmpKindOptions,
|
|
362
|
+
DelimitedSlot: tmpIsDelimited ? [ {
|
|
363
|
+
WizardHash: this.options.WizardHash,
|
|
364
|
+
HasHeader: (tmpSession.ParseConfig.Delimited.HasHeader !== false),
|
|
365
|
+
DelimiterOptions: [ { Value: ',', Label: 'Comma', Selected: tmpSession.ParseConfig.Delimited.Delimiter === ',' }, { Value: 'tab', Label: 'Tab', Selected: tmpSession.ParseConfig.Delimited.Delimiter === '\t' }, { Value: ';', Label: 'Semicolon', Selected: tmpSession.ParseConfig.Delimited.Delimiter === ';' }, { Value: '|', Label: 'Pipe', Selected: tmpSession.ParseConfig.Delimited.Delimiter === '|' } ],
|
|
366
|
+
} ] : [],
|
|
367
|
+
RulerSlot: tmpIsFixed ? [ { WizardHash: this.options.WizardHash, Ruler: this._buildRuler(tmpSession.RawLines), ColumnsJSON: JSON.stringify(tmpSession.ParseConfig.FixedWidth.Columns || []) } ] : [],
|
|
368
|
+
ColumnCount: (tmpSession.DetectedColumns || []).length,
|
|
369
|
+
RowCountEstimate: tmpSession.RowCountEstimate || 0,
|
|
370
|
+
SampleHeader: tmpHeader,
|
|
371
|
+
SampleRows: tmpSampleRows,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
_buildRuler(pLines)
|
|
376
|
+
{
|
|
377
|
+
const tmpLines = Array.isArray(pLines) ? pLines.slice(0, 8) : [];
|
|
378
|
+
if (tmpLines.length === 0) { return ''; }
|
|
379
|
+
const tmpWidth = Math.max.apply(null, tmpLines.map((pLine) => pLine.length).concat([ 0 ]));
|
|
380
|
+
let tmpRuler = '';
|
|
381
|
+
for (let i = 0; i < tmpWidth; i++) { tmpRuler += (i % 10 === 0) ? String((i / 10) % 10) : ((i % 5 === 0) ? '+' : '.'); }
|
|
382
|
+
return tmpRuler + '\n' + tmpLines.join('\n');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// --- Step 3: mapping ---
|
|
386
|
+
|
|
387
|
+
_loadSchemaAndMapping()
|
|
388
|
+
{
|
|
389
|
+
const tmpSession = this._session();
|
|
390
|
+
const tmpProvider = this.options.ResolvedSchemaProvider;
|
|
391
|
+
if (!tmpProvider) { this._renderStepBody('mapping'); return; }
|
|
392
|
+
Promise.resolve(tmpProvider.getSchema({ URLPrefix: this.options.URLPrefix })).then((pSchema) =>
|
|
393
|
+
{
|
|
394
|
+
this._schema = pSchema || { Entities: [], Order: [] };
|
|
395
|
+
if (typeof this.options.OnSchemaLoaded === 'function') { try { this.options.OnSchemaLoaded(this._schema); } catch (pError) { /* host */ } }
|
|
396
|
+
// Seed the mapping from the schema if the host gave no default mapping yet.
|
|
397
|
+
if (!tmpSession.Mapping.Order || tmpSession.Mapping.Order.length === 0) { tmpSession.Mapping = this._autoBuildMapping(this._schema); }
|
|
398
|
+
this._persist();
|
|
399
|
+
this._renderStepBody('mapping');
|
|
400
|
+
this._refreshAccordionChrome();
|
|
401
|
+
}).catch((pError) => { this._reportError(pError); });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Auto-build a starting mapping from the schema: same-named source columns + a GUID template guess. */
|
|
405
|
+
_autoBuildMapping(pSchema)
|
|
406
|
+
{
|
|
407
|
+
const tmpSourceNames = (this._session().DetectedColumns || []).map((pColumn) => pColumn.SourceName);
|
|
408
|
+
const tmpEntities = {};
|
|
409
|
+
const tmpOrder = pSchema.Order || pSchema.Entities.map((pEntity) => pEntity.Entity);
|
|
410
|
+
pSchema.Entities.forEach((pEntity) =>
|
|
411
|
+
{
|
|
412
|
+
const tmpGUIDColumn = tmpSourceNames[0] || 'id';
|
|
413
|
+
const tmpMappings = {};
|
|
414
|
+
const tmpBindings = {};
|
|
415
|
+
pEntity.Fields.forEach((pField) =>
|
|
416
|
+
{
|
|
417
|
+
if (pField.IsGUID) { return; }
|
|
418
|
+
const tmpMatch = tmpSourceNames.find((pName) => pName.toLowerCase() === pField.Name.toLowerCase());
|
|
419
|
+
if (tmpMatch) { tmpMappings[pField.Name] = `{~D:Record.${tmpMatch}~}`; tmpBindings[pField.Name] = tmpMatch; }
|
|
420
|
+
});
|
|
421
|
+
tmpEntities[pEntity.Entity] = {
|
|
422
|
+
Entity: pEntity.Entity,
|
|
423
|
+
GUIDName: pEntity.GUIDName,
|
|
424
|
+
GUIDTemplate: pEntity.GUIDTemplateDefault || `${pEntity.Entity}_{~D:Record.${tmpGUIDColumn}~}`,
|
|
425
|
+
Mappings: tmpMappings,
|
|
426
|
+
_ColumnBindings: tmpBindings,
|
|
427
|
+
};
|
|
428
|
+
});
|
|
429
|
+
return { TargetSchemaSource: this.options.SchemaSource, Order: tmpOrder, Entities: tmpEntities };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
setMappingBinding(pEntity, pField, pSourceColumn)
|
|
433
|
+
{
|
|
434
|
+
const tmpSession = this._session();
|
|
435
|
+
const tmpEntityMapping = tmpSession.Mapping.Entities[pEntity];
|
|
436
|
+
if (!tmpEntityMapping) { return; }
|
|
437
|
+
tmpEntityMapping._ColumnBindings = tmpEntityMapping._ColumnBindings || {};
|
|
438
|
+
if (pSourceColumn) { tmpEntityMapping._ColumnBindings[pField] = pSourceColumn; tmpEntityMapping.Mappings[pField] = `{~D:Record.${pSourceColumn}~}`; }
|
|
439
|
+
else { delete tmpEntityMapping._ColumnBindings[pField]; delete tmpEntityMapping.Mappings[pField]; }
|
|
440
|
+
this._persist();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
applyRawMapping()
|
|
444
|
+
{
|
|
445
|
+
const tmpElement = (typeof document !== 'undefined') ? document.getElementById(`DI_RawMap_${this.options.WizardHash}`) : null;
|
|
446
|
+
if (!tmpElement) { return; }
|
|
447
|
+
try { this._session().Mapping = JSON.parse(tmpElement.value); this._persist(); this._renderStepBody('mapping'); this._refreshAccordionChrome(); }
|
|
448
|
+
catch (pError) { this._reportError(new Error('Mapping JSON is invalid.')); }
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
_mappingRenderState()
|
|
452
|
+
{
|
|
453
|
+
const tmpSession = this._session();
|
|
454
|
+
const tmpSourceNames = (tmpSession.DetectedColumns || []).map((pColumn) => pColumn.SourceName);
|
|
455
|
+
const tmpMapping = tmpSession.Mapping || { Order: [], Entities: {} };
|
|
456
|
+
const tmpSchemaByEntity = {};
|
|
457
|
+
if (this._schema) { (this._schema.Entities || []).forEach((pEntity) => { tmpSchemaByEntity[pEntity.Entity] = pEntity; }); }
|
|
458
|
+
|
|
459
|
+
const tmpEntities = (tmpMapping.Order || []).map((pEntityName) =>
|
|
460
|
+
{
|
|
461
|
+
const tmpEntityMapping = tmpMapping.Entities[pEntityName] || { Entity: pEntityName, Mappings: {}, _ColumnBindings: {} };
|
|
462
|
+
const tmpSchemaEntity = tmpSchemaByEntity[pEntityName];
|
|
463
|
+
// Field rows: prefer the schema's fields; else the mapping's own field keys.
|
|
464
|
+
const tmpFieldNames = tmpSchemaEntity
|
|
465
|
+
? tmpSchemaEntity.Fields.filter((pField) => !pField.IsGUID).map((pField) => pField.Name)
|
|
466
|
+
: Object.keys(tmpEntityMapping.Mappings || {});
|
|
467
|
+
const tmpBindings = tmpEntityMapping._ColumnBindings || {};
|
|
468
|
+
const tmpFields = tmpFieldNames.map((pFieldName) =>
|
|
469
|
+
{
|
|
470
|
+
const tmpBound = tmpBindings[pFieldName] || '';
|
|
471
|
+
const tmpOptions = [ { Value: '', Label: '(none)', Selected: !tmpBound } ].concat(tmpSourceNames.map((pName) => ({ Value: pName, Label: pName, Selected: pName === tmpBound })));
|
|
472
|
+
const tmpRequired = tmpSchemaEntity ? !!(tmpSchemaEntity.Fields.find((pField) => pField.Name === pFieldName) || {}).Required : false;
|
|
473
|
+
return { WizardHash: this.options.WizardHash, Entity: pEntityName, Name: pFieldName, Required: tmpRequired, Options: tmpOptions };
|
|
474
|
+
});
|
|
475
|
+
return { Entity: pEntityName, GUIDTemplate: tmpEntityMapping.GUIDTemplate || '', Fields: tmpFields };
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
return { WizardHash: this.options.WizardHash, Entities: tmpEntities, RawJSON: JSON.stringify(tmpMapping, null, 2) };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// --- Step 4: generate ---
|
|
482
|
+
|
|
483
|
+
_ensureParsedRows()
|
|
484
|
+
{
|
|
485
|
+
if (this._parsedRows) { return Promise.resolve(this._parsedRows); }
|
|
486
|
+
const tmpParser = this._activeParser();
|
|
487
|
+
if (!this._fileHandle || !tmpParser) { return Promise.resolve([]); }
|
|
488
|
+
return Promise.resolve(tmpParser.parse(this._fileHandle, this._session().ParseConfig)).then((pRows) => { this._parsedRows = pRows || []; return this._parsedRows; });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
generate()
|
|
492
|
+
{
|
|
493
|
+
const tmpSession = this._session();
|
|
494
|
+
const tmpBuilder = this.options.ComprehensionBuilder;
|
|
495
|
+
if (!tmpBuilder) { this._reportError(new Error('No comprehension builder available.')); return; }
|
|
496
|
+
this._ensureParsedRows().then((pRows) =>
|
|
497
|
+
{
|
|
498
|
+
const tmpResult = tmpBuilder.build(tmpSession.Mapping, pRows);
|
|
499
|
+
tmpSession.Comprehension = tmpResult.Comprehension;
|
|
500
|
+
tmpSession.GenerationReport = tmpResult.Report;
|
|
501
|
+
tmpSession.StepStatus.generate = 'complete';
|
|
502
|
+
this._mappingWarnings = tmpBuilder.validateForeignKeyOrder(tmpSession.Mapping);
|
|
503
|
+
if (typeof this.options.OnComprehension === 'function') { try { this.options.OnComprehension(tmpSession.Comprehension, tmpResult.Report); } catch (pError) { /* host */ } }
|
|
504
|
+
this._persist();
|
|
505
|
+
this._renderStepBody('generate');
|
|
506
|
+
this._refreshAccordionChrome();
|
|
507
|
+
}).catch((pError) => { this._reportError(pError); });
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
_generateRenderState()
|
|
511
|
+
{
|
|
512
|
+
const tmpSession = this._session();
|
|
513
|
+
const tmpReport = tmpSession.GenerationReport;
|
|
514
|
+
const tmpWarnings = this._mappingWarnings || [];
|
|
515
|
+
const tmpState = {
|
|
516
|
+
WizardHash: this.options.WizardHash,
|
|
517
|
+
Warnings: tmpWarnings.map((pWarning) => ({ Message: pWarning.Message })),
|
|
518
|
+
DownloadSlot: (this.options.AllowDownload && tmpSession.Comprehension) ? [ { WizardHash: this.options.WizardHash } ] : [],
|
|
519
|
+
ReportSlot: [],
|
|
520
|
+
};
|
|
521
|
+
if (tmpReport && tmpSession.Comprehension)
|
|
522
|
+
{
|
|
523
|
+
const tmpEntityCounts = Object.keys(tmpReport.EntityCounts || {}).map((pEntity) => ({ Entity: pEntity, Count: tmpReport.EntityCounts[pEntity] }));
|
|
524
|
+
const tmpEntityTotal = tmpEntityCounts.reduce((pSum, pEntry) => pSum + pEntry.Count, 0);
|
|
525
|
+
// Preview the first generated entity (first few records).
|
|
526
|
+
const tmpFirstEntity = Object.keys(tmpSession.Comprehension)[0];
|
|
527
|
+
let tmpPreviewSlot = [];
|
|
528
|
+
if (tmpFirstEntity)
|
|
529
|
+
{
|
|
530
|
+
const tmpRecords = Object.keys(tmpSession.Comprehension[tmpFirstEntity]).slice(0, 8).map((pKey) => tmpSession.Comprehension[tmpFirstEntity][pKey]);
|
|
531
|
+
const tmpHeaderNames = tmpRecords.length > 0 ? Object.keys(tmpRecords[0]) : [];
|
|
532
|
+
tmpPreviewSlot = [ {
|
|
533
|
+
Entity: tmpFirstEntity, ShownCount: tmpRecords.length,
|
|
534
|
+
Header: tmpHeaderNames.map((pName) => ({ Name: pName })),
|
|
535
|
+
Rows: tmpRecords.map((pRecord) => ({ Cells: tmpHeaderNames.map((pName) => ({ Value: String(pRecord[pName] === undefined ? '' : pRecord[pName]) })) })),
|
|
536
|
+
} ];
|
|
537
|
+
}
|
|
538
|
+
tmpState.ReportSlot = [ { ParsedRowCount: tmpReport.ParsedRowCount, EntityTotal: tmpEntityTotal, EntityCounts: tmpEntityCounts, PreviewSlot: tmpPreviewSlot } ];
|
|
539
|
+
}
|
|
540
|
+
return tmpState;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
downloadComprehension()
|
|
544
|
+
{
|
|
545
|
+
const tmpSession = this._session();
|
|
546
|
+
if (!tmpSession.Comprehension || typeof document === 'undefined') { return; }
|
|
547
|
+
const tmpBlob = new Blob([ JSON.stringify(tmpSession.Comprehension, null, 2) ], { type: 'application/json' });
|
|
548
|
+
const tmpURL = URL.createObjectURL(tmpBlob);
|
|
549
|
+
const tmpAnchor = document.createElement('a');
|
|
550
|
+
tmpAnchor.href = tmpURL;
|
|
551
|
+
tmpAnchor.download = `${this.options.WizardHash}-comprehension.json`;
|
|
552
|
+
tmpAnchor.click();
|
|
553
|
+
URL.revokeObjectURL(tmpURL);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// --- Step 5: push ---
|
|
557
|
+
|
|
558
|
+
push()
|
|
559
|
+
{
|
|
560
|
+
const tmpSession = this._session();
|
|
561
|
+
const tmpTarget = this.options.ResolvedPushTarget;
|
|
562
|
+
if (!tmpTarget || !tmpSession.Comprehension) { this._reportError(new Error('Nothing to push — generate a comprehension first.')); return; }
|
|
563
|
+
tmpSession.Push.Status = 'running';
|
|
564
|
+
tmpSession.Push.Error = null;
|
|
565
|
+
this._renderStepBody('push');
|
|
566
|
+
const tmpContext = {
|
|
567
|
+
Order: tmpSession.Mapping.Order,
|
|
568
|
+
GUIDPrefix: this.options.GUIDPrefix,
|
|
569
|
+
EntityGUIDPrefix: this.options.EntityGUIDPrefix,
|
|
570
|
+
ServerURL: this.options.ServerURL,
|
|
571
|
+
URLPrefix: this.options.URLPrefix,
|
|
572
|
+
ComprehensionPushURL: this.options.ComprehensionPushURL,
|
|
573
|
+
AllowGUIDTruncation: this.options.AllowGUIDTruncation,
|
|
574
|
+
onProgress: (pPushed, pTotal) => { tmpSession.Push.Progress = { Pushed: pPushed, Total: pTotal }; this._renderStepBody('push'); },
|
|
575
|
+
};
|
|
576
|
+
Promise.resolve(tmpTarget.push(tmpSession.Comprehension, tmpContext)).then((pResult) =>
|
|
577
|
+
{
|
|
578
|
+
tmpSession.Push.Status = 'complete';
|
|
579
|
+
tmpSession.Push.Result = pResult;
|
|
580
|
+
tmpSession.StepStatus.push = 'complete';
|
|
581
|
+
if (typeof this.options.OnPushComplete === 'function') { try { this.options.OnPushComplete(pResult); } catch (pError) { /* host */ } }
|
|
582
|
+
this._persist();
|
|
583
|
+
this._renderStepBody('push');
|
|
584
|
+
this._refreshAccordionChrome();
|
|
585
|
+
}).catch((pError) =>
|
|
586
|
+
{
|
|
587
|
+
tmpSession.Push.Status = 'error';
|
|
588
|
+
tmpSession.Push.Error = pError.message || String(pError);
|
|
589
|
+
this._renderStepBody('push');
|
|
590
|
+
this._reportError(pError);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
_pushRenderState()
|
|
595
|
+
{
|
|
596
|
+
const tmpSession = this._session();
|
|
597
|
+
const tmpPush = tmpSession.Push;
|
|
598
|
+
const tmpRunning = (tmpPush.Status === 'running');
|
|
599
|
+
const tmpPct = (tmpPush.Progress && tmpPush.Progress.Total > 0) ? Math.round((tmpPush.Progress.Pushed / tmpPush.Progress.Total) * 100) : (tmpPush.Status === 'complete' ? 100 : 0);
|
|
600
|
+
return {
|
|
601
|
+
WizardHash: this.options.WizardHash,
|
|
602
|
+
Mode: tmpPush.Mode,
|
|
603
|
+
PushLabel: tmpRunning ? 'Pushing…' : 'Push to server',
|
|
604
|
+
PushDisabled: tmpRunning || !tmpSession.Comprehension,
|
|
605
|
+
ProgressSlot: (tmpRunning || tmpPush.Status === 'complete') ? [ { Pct: tmpPct } ] : [],
|
|
606
|
+
ResultSlot: (tmpPush.Status === 'complete' && tmpPush.Result) ? [ { Message: tmpPush.Result.Message || 'Pushed.' } ] : [],
|
|
607
|
+
ErrorSlot: (tmpPush.Status === 'error' && tmpPush.Error) ? [ { Message: tmpPush.Error } ] : [],
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// --- Shared ---
|
|
612
|
+
|
|
613
|
+
_renderStepBody(pStepHash)
|
|
614
|
+
{
|
|
615
|
+
const tmpDestination = `#PSA_Body_${this._accHash}_${pStepHash}`;
|
|
616
|
+
let tmpHTML = '';
|
|
617
|
+
if (pStepHash === 'upload') { tmpHTML = this.pict.parseTemplateByHash('DI-Body-Upload', { WizardHash: this.options.WizardHash }); }
|
|
618
|
+
else if (pStepHash === 'detect') { tmpHTML = this.pict.parseTemplateByHash('DI-Body-Detect', this._detectRenderState()); }
|
|
619
|
+
else if (pStepHash === 'mapping') { tmpHTML = this.pict.parseTemplateByHash('DI-Body-Mapping', this._mappingRenderState()); }
|
|
620
|
+
else if (pStepHash === 'generate') { tmpHTML = this.pict.parseTemplateByHash('DI-Body-Generate', this._generateRenderState()); }
|
|
621
|
+
else if (pStepHash === 'push') { tmpHTML = this.pict.parseTemplateByHash('DI-Body-Push', this._pushRenderState()); }
|
|
622
|
+
this.pict.ContentAssignment.assignContent(tmpDestination, tmpHTML);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Reflect step completion on the accordion chrome (so Next-gates + medallions update). */
|
|
626
|
+
_refreshAccordionChrome()
|
|
627
|
+
{
|
|
628
|
+
if (!this._accordion) { return; }
|
|
629
|
+
const tmpSession = this._session();
|
|
630
|
+
[ 'upload', 'detect', 'mapping', 'generate' ].forEach((pStep) =>
|
|
631
|
+
{
|
|
632
|
+
if (this._canAdvance(pStep)) { this._accordion.setStepComplete(pStep, true); }
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Persist the session via the configured StateStore (best-effort). */
|
|
637
|
+
_persist()
|
|
638
|
+
{
|
|
639
|
+
const tmpStore = this.options.ResolvedStateStore;
|
|
640
|
+
if (!tmpStore) { return; }
|
|
641
|
+
try { Promise.resolve(tmpStore.save(this.options.WizardHash, libSession.serialize(this._session()))).catch(() => {}); }
|
|
642
|
+
catch (pError) { /* non-fatal */ }
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
_reportError(pError)
|
|
646
|
+
{
|
|
647
|
+
this.pict.log.warn(`pict-section-dataimport [${this.options.WizardHash}] ${pError.message || pError}`);
|
|
648
|
+
if (typeof this.options.OnError === 'function') { try { this.options.OnError(pError); } catch (pInner) { /* host */ } }
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** @return {Record<string, any>} The current session (for host inspection). */
|
|
652
|
+
getSession() { return this._session(); }
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
module.exports = PictViewDataImportWizard;
|
|
656
|
+
|
|
657
|
+
module.exports.default_configuration = _DEFAULT_CONFIGURATION;
|