pict-section-dataimport 1.0.0 → 1.1.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pict-section-dataimport",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Embeddable, configurable data-import wizard for Pict apps — upload a CSV/TSV/Excel/fixed-width file, validate + detect its schema, map columns to a Meadow (or host-config) entity schema, generate comprehensions, and push them (records via the browser EntityProvider, or one POST to a Comprehension endpoint). Composes pict-section-upload + pict-section-accordion + pict-section-form, reusing meadow-integration's browser-safe transform/push engine.",
|
|
5
5
|
"main": "source/Pict-Section-DataImport.js",
|
|
6
6
|
"files": [
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"pict-view": "^1.0.68"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"meadow-integration": ">=1.
|
|
47
|
+
"meadow-integration": ">=1.1.1",
|
|
48
48
|
"pict": ">=1.0.300",
|
|
49
49
|
"pict-section-accordion": ">=1.0.0",
|
|
50
50
|
"pict-section-form": ">=1.0.0",
|
|
@@ -70,4 +70,15 @@ module.exports = /*css*/`
|
|
|
70
70
|
.psd-progress { height: 8px; border-radius: var(--theme-radius-pill, 999px); overflow: hidden; background: var(--theme-color-background-tertiary, #e9edf2); }
|
|
71
71
|
.psd-progress-fill { height: 100%; background: var(--theme-color-brand-primary, #156dd1); transition: width var(--theme-duration-normal, 0.2s) ease; }
|
|
72
72
|
.psd-row-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
|
|
73
|
+
|
|
74
|
+
.psd-guid { display: flex; flex-direction: column; gap: 0.4rem; margin: 0.3rem 0 0.6rem; padding: 0.5rem 0.65rem;
|
|
75
|
+
border: 1px solid var(--theme-color-border-light, #e6e9ee); border-radius: var(--theme-radius-md, 6px); background: var(--theme-color-background-secondary, #f7f9fb); }
|
|
76
|
+
.psd-guid-line { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
|
|
77
|
+
.psd-guid-label { font-size: var(--theme-typography-size-sm, 0.82rem); font-weight: 600; color: var(--theme-color-text-primary, #1f2733); }
|
|
78
|
+
.psd-guid-sub { font-size: var(--theme-typography-size-sm, 0.8rem); color: var(--theme-color-text-secondary, #6b7686); }
|
|
79
|
+
.psd-guid-parent { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; padding-left: 0.5rem; }
|
|
80
|
+
.psd-guid-rm { padding: 0 0.45rem; line-height: 1.4; }
|
|
81
|
+
.psd-guid-add { padding-left: 0.5rem; }
|
|
82
|
+
.psd-guid-preview { font-size: var(--theme-typography-size-sm, 0.82rem); color: var(--theme-color-text-secondary, #6b7686); }
|
|
83
|
+
.psd-guid-warn { display: flex; align-items: center; gap: 0.4rem; font-size: var(--theme-typography-size-sm, 0.82rem); color: var(--theme-color-status-warning, #b8860b); }
|
|
73
84
|
`;
|
|
@@ -70,6 +70,15 @@ class PictProviderDataImport extends libPictProvider
|
|
|
70
70
|
pConfig || {},
|
|
71
71
|
{ WizardHash: pWizardHash });
|
|
72
72
|
|
|
73
|
+
// When a context-aware GUID strategy is active the composer owns the ENTIRE GUID (the desired
|
|
74
|
+
// `UI_..._...` format is not the engine's prefix-marshal format), so the marshaling prefixes must
|
|
75
|
+
// be empty — otherwise the push would double-prefix the already-composed GUID.
|
|
76
|
+
if (tmpConfig.GUIDStrategy)
|
|
77
|
+
{
|
|
78
|
+
tmpConfig.GUIDPrefix = '';
|
|
79
|
+
tmpConfig.EntityGUIDPrefix = '';
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
// Resolve the seams onto the config — the wizard view reads these directly.
|
|
74
83
|
tmpConfig.ResolvedParsers = this._resolveParsers(tmpConfig);
|
|
75
84
|
tmpConfig.ResolvedSchemaProvider = this._resolveSchemaProvider(tmpConfig);
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// DataImport-GuidStrategyApply — wires the meadow-integration GUID strategy compiler into a wizard
|
|
2
|
+
// mapping. Given the per-entity strategy config + the host context catalog + the resolved schema (for
|
|
3
|
+
// the GUID column widths), it compiles the strategy and attaches it to each entity mapping, then strips
|
|
4
|
+
// the GUID / FK fields the strategy now owns out of each entity's data Mappings.
|
|
5
|
+
//
|
|
6
|
+
// Thin glue over meadow-integration's engine (the composer + compiler live there, reused by the CLI +
|
|
7
|
+
// server endpoints too); this only knows the dataimport mapping shape. Pure + unit-testable.
|
|
8
|
+
|
|
9
|
+
const libEngine = require('meadow-integration/source/Meadow-Integration-Engine.js');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build { <entity>: <GUID column width> } from a resolved dataimport schema (Entities[].Fields[].Size,
|
|
13
|
+
* IsGUID), so the composer can keep each entity's GUID inside its column.
|
|
14
|
+
* @param {Record<string, any>} pSchema
|
|
15
|
+
* @returns {Record<string, number>}
|
|
16
|
+
*/
|
|
17
|
+
function schemaSizes(pSchema)
|
|
18
|
+
{
|
|
19
|
+
const tmpSizes = {};
|
|
20
|
+
((pSchema && pSchema.Entities) || []).forEach((pEntity) =>
|
|
21
|
+
{
|
|
22
|
+
const tmpGUIDField = (pEntity.Fields || []).find((pField) => pField.IsGUID);
|
|
23
|
+
if (tmpGUIDField && (Number(tmpGUIDField.Size) > 0))
|
|
24
|
+
{
|
|
25
|
+
tmpSizes[pEntity.Entity] = Number(tmpGUIDField.Size);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
return tmpSizes;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Compile + attach GUID strategies to a mapping.
|
|
33
|
+
* @param {Record<string, any>} pMapping - { Order, Entities:{ <name>: <entityMapping> } }
|
|
34
|
+
* @param {Record<string, any>} pStrategyConfig - { Prefix?, Entities:{ <name>: <entityConfig> } }
|
|
35
|
+
* @param {Record<string, any>} pCatalog - { <entity>:{ Abbrev, KeyField } }
|
|
36
|
+
* @param {Record<string, any>} pSchema - resolved dataimport schema (for GUID column sizes)
|
|
37
|
+
* @returns {{Mapping:Record<string, any>, Warnings:Array<string>}}
|
|
38
|
+
*/
|
|
39
|
+
function applyStrategy(pMapping, pStrategyConfig, pCatalog, pSchema)
|
|
40
|
+
{
|
|
41
|
+
const tmpCompiled = libEngine.compileGUIDStrategy(pStrategyConfig || {}, { Catalog: pCatalog || {}, SchemaSizes: schemaSizes(pSchema) });
|
|
42
|
+
const tmpEntities = (pMapping && pMapping.Entities) || {};
|
|
43
|
+
|
|
44
|
+
Object.keys(tmpEntities).forEach((pEntityName) =>
|
|
45
|
+
{
|
|
46
|
+
const tmpStrategy = tmpCompiled.Strategies[pEntityName];
|
|
47
|
+
if (!tmpStrategy) { return; }
|
|
48
|
+
const tmpEntityMapping = tmpEntities[pEntityName];
|
|
49
|
+
tmpEntityMapping.GUIDStrategy = tmpStrategy;
|
|
50
|
+
|
|
51
|
+
// Strip the fields the strategy now stamps (own GUID + every FK field) out of the data Mappings,
|
|
52
|
+
// so they aren't double-written by the flat-template Mappings loop.
|
|
53
|
+
const tmpStrategyFields = {};
|
|
54
|
+
if (tmpStrategy.Own && tmpStrategy.Own.FieldName) { tmpStrategyFields[tmpStrategy.Own.FieldName] = true; }
|
|
55
|
+
(tmpStrategy.Joins || []).forEach((pJoin) => { if (pJoin.FieldName) { tmpStrategyFields[pJoin.FieldName] = true; } });
|
|
56
|
+
if (tmpEntityMapping.Mappings)
|
|
57
|
+
{
|
|
58
|
+
Object.keys(tmpEntityMapping.Mappings).forEach((pField) =>
|
|
59
|
+
{
|
|
60
|
+
if (tmpStrategyFields[pField]) { delete tmpEntityMapping.Mappings[pField]; }
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return { Mapping: pMapping, Warnings: tmpCompiled.Warnings || [] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build a meadow-integration strategy config from the simplified per-entity UI model the wizard panel
|
|
70
|
+
* edits. Each "parent link" becomes BOTH a context segment in the entity's own GUID (when prefixed) AND
|
|
71
|
+
* a foreign-key join — which is the user's mental model (`UI_C10_P01278_LI8675309` embeds Contract +
|
|
72
|
+
* Project context and the Line Item carries GUIDContract / GUIDProject FKs).
|
|
73
|
+
* @param {Record<string, any>} pUIEntities - { <entity>: { Mode, OwnKeyColumn, Parents:[{Entity,KeyColumn,Mode,CrossSession}] } }
|
|
74
|
+
* @param {string} pPrefix - the GUID prefix (e.g. 'UI')
|
|
75
|
+
* @returns {{Prefix:string, Entities:Record<string, any>}}
|
|
76
|
+
*/
|
|
77
|
+
function buildStrategyConfig(pUIEntities, pPrefix)
|
|
78
|
+
{
|
|
79
|
+
const tmpEntities = {};
|
|
80
|
+
Object.keys(pUIEntities || {}).forEach((pEntityName) =>
|
|
81
|
+
{
|
|
82
|
+
const tmpUI = pUIEntities[pEntityName] || {};
|
|
83
|
+
const tmpParents = Array.isArray(tmpUI.Parents) ? tmpUI.Parents : [];
|
|
84
|
+
// Only prefixed parents contribute a context segment to the own GUID (raw / rawid carry an external
|
|
85
|
+
// GUID / ID that has no composable key for the prefix).
|
|
86
|
+
const tmpContextEntities = [];
|
|
87
|
+
const tmpContextKeyColumns = {};
|
|
88
|
+
tmpParents.forEach((pParent) =>
|
|
89
|
+
{
|
|
90
|
+
if ((pParent.Mode || 'prefixed') === 'prefixed' && pParent.Entity)
|
|
91
|
+
{
|
|
92
|
+
tmpContextEntities.push(pParent.Entity);
|
|
93
|
+
tmpContextKeyColumns[pParent.Entity] = pParent.KeyColumn;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
tmpEntities[pEntityName] = {
|
|
97
|
+
Mode: tmpUI.Mode || 'prefixed',
|
|
98
|
+
OwnKeyColumn: tmpUI.OwnKeyColumn,
|
|
99
|
+
OwnGUIDColumn: tmpUI.OwnGUIDColumn,
|
|
100
|
+
ContextEntities: tmpContextEntities,
|
|
101
|
+
ContextKeyColumns: tmpContextKeyColumns,
|
|
102
|
+
Joins: tmpParents.filter((pParent) => !!pParent.Entity).map((pParent) =>
|
|
103
|
+
{
|
|
104
|
+
const tmpMode = pParent.Mode || 'prefixed';
|
|
105
|
+
return {
|
|
106
|
+
ParentEntity: pParent.Entity,
|
|
107
|
+
Mode: tmpMode,
|
|
108
|
+
KeyColumn: pParent.KeyColumn,
|
|
109
|
+
GUIDColumn: pParent.KeyColumn,
|
|
110
|
+
IDColumn: pParent.KeyColumn,
|
|
111
|
+
CrossSession: (pParent.CrossSession !== false),
|
|
112
|
+
};
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
return { Prefix: (pPrefix !== undefined) ? pPrefix : 'UI', Entities: tmpEntities };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Preview the GUID a compose spec would produce for a sample row (simple `{~D:Record.X~}` resolution, no
|
|
121
|
+
* fable needed) — used by the wizard panel to show a live example as the user configures the strategy.
|
|
122
|
+
* @param {Record<string, any>} pComposeSpec @param {Record<string, any>} pSampleRow
|
|
123
|
+
* @returns {string}
|
|
124
|
+
*/
|
|
125
|
+
function previewGUID(pComposeSpec, pSampleRow)
|
|
126
|
+
{
|
|
127
|
+
if (!pComposeSpec || !Array.isArray(pComposeSpec.segments)) { return ''; }
|
|
128
|
+
const tmpRow = pSampleRow || {};
|
|
129
|
+
const tmpSegments = pComposeSpec.segments.map((pSegment) =>
|
|
130
|
+
{
|
|
131
|
+
const tmpMatch = /^\{~D:Record\.(.+)~\}$/.exec(pSegment.valueTemplate || '');
|
|
132
|
+
let tmpValue = '';
|
|
133
|
+
if (tmpMatch)
|
|
134
|
+
{
|
|
135
|
+
tmpValue = (tmpRow[tmpMatch[1]] !== undefined && tmpRow[tmpMatch[1]] !== null) ? tmpRow[tmpMatch[1]] : '';
|
|
136
|
+
}
|
|
137
|
+
else if (pSegment.valueTemplate)
|
|
138
|
+
{
|
|
139
|
+
tmpValue = pSegment.valueTemplate;
|
|
140
|
+
}
|
|
141
|
+
return { abbrev: pSegment.abbrev, value: tmpValue };
|
|
142
|
+
});
|
|
143
|
+
return libEngine.composeGUID({ prefix: pComposeSpec.prefix, separator: pComposeSpec.separator, maxLength: pComposeSpec.maxLength, hashLength: pComposeSpec.hashLength, segments: tmpSegments });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* One-call preview for the wizard panel: build the config from the UI model, compile it, and preview the
|
|
148
|
+
* given entity's OWN GUID on a sample row.
|
|
149
|
+
* @returns {string}
|
|
150
|
+
*/
|
|
151
|
+
function previewEntityGUID(pUIEntities, pPrefix, pCatalog, pSchema, pEntityName, pSampleRow)
|
|
152
|
+
{
|
|
153
|
+
const tmpConfig = buildStrategyConfig(pUIEntities, pPrefix);
|
|
154
|
+
const tmpCompiled = libEngine.compileGUIDStrategy(tmpConfig, { Catalog: pCatalog || {}, SchemaSizes: schemaSizes(pSchema) });
|
|
155
|
+
const tmpStrategy = tmpCompiled.Strategies[pEntityName];
|
|
156
|
+
if (!tmpStrategy || !tmpStrategy.Own || !tmpStrategy.Own.Compose) { return ''; }
|
|
157
|
+
return previewGUID(tmpStrategy.Own.Compose, pSampleRow);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
applyStrategy,
|
|
162
|
+
schemaSizes,
|
|
163
|
+
buildStrategyConfig,
|
|
164
|
+
previewGUID,
|
|
165
|
+
previewEntityGUID,
|
|
166
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const libPictView = require('pict-view');
|
|
2
2
|
const libSession = require('../services/DataImport-Session.js');
|
|
3
|
+
const libStrategyApply = require('../services/DataImport-GuidStrategyApply.js');
|
|
3
4
|
|
|
4
5
|
// Required sibling controls (peers). The wizard registers their providers if the host hasn't.
|
|
5
6
|
const libPictSectionAccordion = require('pict-section-accordion');
|
|
@@ -93,10 +94,31 @@ const _DEFAULT_CONFIGURATION =
|
|
|
93
94
|
Hash: 'DI-Map-Entity',
|
|
94
95
|
Template: /*html*/`
|
|
95
96
|
<div class="psd-entity">
|
|
96
|
-
<div class="psd-entity-head"><span class="psd-entity-name">{~D:Record.Entity~}</span
|
|
97
|
+
<div class="psd-entity-head"><span class="psd-entity-name">{~D:Record.Entity~}</span>{~TS:DI-Map-GUID-Legacy:Record.LegacyGUIDSlot~}</div>
|
|
98
|
+
{~TS:DI-GUID-Panel:Record.StrategySlot~}
|
|
97
99
|
{~TS:DI-Map-Row:Record.Fields~}
|
|
98
100
|
</div>`
|
|
99
101
|
},
|
|
102
|
+
{ Hash: 'DI-Map-GUID-Legacy', Template: /*html*/`<span class="psd-entity-count">GUID: <span class="psd-mono">{~D:Record.GUIDTemplate~}</span></span>` },
|
|
103
|
+
{
|
|
104
|
+
Hash: 'DI-GUID-Panel',
|
|
105
|
+
Template: /*html*/`
|
|
106
|
+
<div class="psd-guid">
|
|
107
|
+
<div class="psd-guid-line"><span class="psd-guid-label">Identifier</span><select class="psd-select" onchange="_Pict.views['{~D:Record.WizardHash~}'].setStrategyMode('{~D:Record.Entity~}',this.value)">{~TS:DI-Opt:Record.ModeOptions~}</select>{~TS:DI-GUID-OwnKey:Record.OwnKeySlot~}</div>
|
|
108
|
+
{~TS:DI-GUID-Parent:Record.Parents~}
|
|
109
|
+
{~TS:DI-GUID-AddParent:Record.AddParentSlot~}
|
|
110
|
+
{~TS:DI-GUID-Preview:Record.PreviewSlot~}
|
|
111
|
+
{~TS:DI-GUID-Warn:Record.Warnings~}
|
|
112
|
+
</div>`
|
|
113
|
+
},
|
|
114
|
+
{ Hash: 'DI-GUID-OwnKey', Template: /*html*/`<span class="psd-guid-sub">keyed by</span><select class="psd-select" onchange="_Pict.views['{~D:Record.WizardHash~}'].setStrategyOwnKey('{~D:Record.Entity~}',this.value)">{~TS:DI-Opt:Record.Options~}</select>` },
|
|
115
|
+
{
|
|
116
|
+
Hash: 'DI-GUID-Parent',
|
|
117
|
+
Template: /*html*/`<div class="psd-guid-parent"><span class="psd-guid-sub">related to</span><select class="psd-select" onchange="_Pict.views['{~D:Record.WizardHash~}'].setParentEntity('{~D:Record.Entity~}',{~D:Record.Index~},this.value)">{~TS:DI-Opt:Record.EntityOptions~}</select><span class="psd-guid-sub">via</span><select class="psd-select" onchange="_Pict.views['{~D:Record.WizardHash~}'].setParentKey('{~D:Record.Entity~}',{~D:Record.Index~},this.value)">{~TS:DI-Opt:Record.KeyOptions~}</select><select class="psd-select" onchange="_Pict.views['{~D:Record.WizardHash~}'].setParentMode('{~D:Record.Entity~}',{~D:Record.Index~},this.value)">{~TS:DI-Opt:Record.ModeOptions~}</select><button type="button" class="psd-btn psd-btn-ghost psd-guid-rm" onclick="_Pict.views['{~D:Record.WizardHash~}'].removeStrategyParent('{~D:Record.Entity~}',{~D:Record.Index~})" title="Remove">×</button></div>`
|
|
118
|
+
},
|
|
119
|
+
{ Hash: 'DI-GUID-AddParent', Template: /*html*/`<div class="psd-guid-add"><select class="psd-select" onchange="if(this.value){_Pict.views['{~D:Record.WizardHash~}'].addStrategyParent('{~D:Record.Entity~}',this.value);this.value='';}">{~TS:DI-Opt:Record.Options~}</select></div>` },
|
|
120
|
+
{ Hash: 'DI-GUID-Preview', Template: /*html*/`<div class="psd-guid-preview">Example: <span class="psd-mono">{~D:Record.Preview~}</span></div>` },
|
|
121
|
+
{ Hash: 'DI-GUID-Warn', Template: /*html*/`<div class="psd-guid-warn">{~I:Warning~} <span>{~D:Record.Message~}</span></div>` },
|
|
100
122
|
{
|
|
101
123
|
Hash: 'DI-Map-Row',
|
|
102
124
|
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>`
|
|
@@ -205,7 +227,13 @@ class PictViewDataImportWizard extends libPictView
|
|
|
205
227
|
onAfterRender(pRenderable)
|
|
206
228
|
{
|
|
207
229
|
if (this.pict.CSSMap && typeof this.pict.CSSMap.injectCSS === 'function') { this.pict.CSSMap.injectCSS(); }
|
|
208
|
-
|
|
230
|
+
// Build the accordion + step bodies on first render, and REBUILD whenever a host re-render has wiped
|
|
231
|
+
// our mount. When the wizard is embedded in a routed host that repaints its container (on navigation,
|
|
232
|
+
// or after a push), the DI-Root template recreates an EMPTY `#DI_Acc_<WizardHash>` while `_wired` stays
|
|
233
|
+
// true — which would otherwise leave a bare accordion shell. createAccordion / createUploader are keyed
|
|
234
|
+
// by hash (reconfigure-or-create) and the accordion keeps its serializable nav state, so the rebuild
|
|
235
|
+
// restores the current step + the host's step bodies without losing progress.
|
|
236
|
+
if (!this._wired || !this._accordionMounted())
|
|
209
237
|
{
|
|
210
238
|
this._wired = true;
|
|
211
239
|
this._buildWizard();
|
|
@@ -213,7 +241,21 @@ class PictViewDataImportWizard extends libPictView
|
|
|
213
241
|
return super.onAfterRender(pRenderable);
|
|
214
242
|
}
|
|
215
243
|
|
|
216
|
-
/**
|
|
244
|
+
/**
|
|
245
|
+
* Is the accordion currently rendered into its mount, or has a host re-render wiped it? The DI-Root
|
|
246
|
+
* template recreates an empty `#DI_Acc_<WizardHash>` on every paint, so an empty mount means the
|
|
247
|
+
* accordion (and its step bodies) need (re)building.
|
|
248
|
+
* @return {boolean}
|
|
249
|
+
*/
|
|
250
|
+
_accordionMounted()
|
|
251
|
+
{
|
|
252
|
+
// No DOM to inspect (server-side render) — defer to the `_wired` one-time gate.
|
|
253
|
+
if (typeof document === 'undefined') { return true; }
|
|
254
|
+
const tmpElement = document.getElementById(`DI_Acc_${this.options.WizardHash}`);
|
|
255
|
+
return !!(tmpElement && tmpElement.children && (tmpElement.children.length > 0));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Create the accordion + upload view and render all step bodies; safe to re-run (idempotent rebuild). */
|
|
217
259
|
_buildWizard()
|
|
218
260
|
{
|
|
219
261
|
const tmpAccordionProvider = this.pict.providers['Pict-Section-Accordion'];
|
|
@@ -395,12 +437,129 @@ class PictViewDataImportWizard extends libPictView
|
|
|
395
437
|
if (typeof this.options.OnSchemaLoaded === 'function') { try { this.options.OnSchemaLoaded(this._schema); } catch (pError) { /* host */ } }
|
|
396
438
|
// Seed the mapping from the schema if the host gave no default mapping yet.
|
|
397
439
|
if (!tmpSession.Mapping.Order || tmpSession.Mapping.Order.length === 0) { tmpSession.Mapping = this._autoBuildMapping(this._schema); }
|
|
440
|
+
this._seedStrategyUI();
|
|
441
|
+
this._applyGUIDStrategy();
|
|
398
442
|
this._persist();
|
|
399
443
|
this._renderStepBody('mapping');
|
|
400
444
|
this._refreshAccordionChrome();
|
|
401
445
|
}).catch((pError) => { this._reportError(pError); });
|
|
402
446
|
}
|
|
403
447
|
|
|
448
|
+
/** Is the context-aware GUID strategy panel + composition enabled for this wizard? */
|
|
449
|
+
_strategyActive()
|
|
450
|
+
{
|
|
451
|
+
return !!this.options.GUIDStrategy;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Seed the editable per-entity GUID strategy UI model (host default, else prefixed + first column). */
|
|
455
|
+
_seedStrategyUI()
|
|
456
|
+
{
|
|
457
|
+
if (!this._strategyActive()) { return; }
|
|
458
|
+
const tmpSession = this._session();
|
|
459
|
+
tmpSession.GUIDStrategyUI = tmpSession.GUIDStrategyUI || {};
|
|
460
|
+
const tmpDefaults = this.options.GUIDStrategyDefault || {};
|
|
461
|
+
const tmpFirstColumn = ((tmpSession.DetectedColumns || [])[0] || {}).SourceName || '';
|
|
462
|
+
(tmpSession.Mapping.Order || []).forEach((pEntityName) =>
|
|
463
|
+
{
|
|
464
|
+
if (!tmpSession.GUIDStrategyUI[pEntityName])
|
|
465
|
+
{
|
|
466
|
+
tmpSession.GUIDStrategyUI[pEntityName] = tmpDefaults[pEntityName]
|
|
467
|
+
? JSON.parse(JSON.stringify(tmpDefaults[pEntityName]))
|
|
468
|
+
: { Mode: 'prefixed', OwnKeyColumn: tmpFirstColumn, Parents: [] };
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Compile the editable GUID strategy UI model (meadow-integration) against the schema's GUID column
|
|
475
|
+
* sizes + attach it to each entity mapping. The transform then composes stable, length-safe GUIDs +
|
|
476
|
+
* foreign-key fields instead of the flat GUIDTemplate.
|
|
477
|
+
*/
|
|
478
|
+
_applyGUIDStrategy()
|
|
479
|
+
{
|
|
480
|
+
if (!this._strategyActive()) { return; }
|
|
481
|
+
const tmpSession = this._session();
|
|
482
|
+
const tmpConfig = libStrategyApply.buildStrategyConfig(tmpSession.GUIDStrategyUI || {}, this.options.GUIDStrategyPrefix || 'UI');
|
|
483
|
+
const tmpResult = libStrategyApply.applyStrategy(tmpSession.Mapping, tmpConfig, this.options.ContextEntityCatalog, this._schema);
|
|
484
|
+
this._strategyWarnings = tmpResult.Warnings || [];
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Mutable per-entity UI model accessor (creates a default if absent). */
|
|
488
|
+
_strategyUIEntity(pEntity)
|
|
489
|
+
{
|
|
490
|
+
const tmpSession = this._session();
|
|
491
|
+
tmpSession.GUIDStrategyUI = tmpSession.GUIDStrategyUI || {};
|
|
492
|
+
if (!tmpSession.GUIDStrategyUI[pEntity]) { tmpSession.GUIDStrategyUI[pEntity] = { Mode: 'prefixed', OwnKeyColumn: '', Parents: [] }; }
|
|
493
|
+
return tmpSession.GUIDStrategyUI[pEntity];
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Recompile + re-render after any strategy panel edit. */
|
|
497
|
+
_afterStrategyEdit()
|
|
498
|
+
{
|
|
499
|
+
this._applyGUIDStrategy();
|
|
500
|
+
this._persist();
|
|
501
|
+
this._renderStepBody('mapping');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
setStrategyMode(pEntity, pMode) { this._strategyUIEntity(pEntity).Mode = pMode; this._afterStrategyEdit(); }
|
|
505
|
+
setStrategyOwnKey(pEntity, pColumn) { this._strategyUIEntity(pEntity).OwnKeyColumn = pColumn; this._afterStrategyEdit(); }
|
|
506
|
+
addStrategyParent(pEntity, pParentEntity)
|
|
507
|
+
{
|
|
508
|
+
const tmpEntity = this._strategyUIEntity(pEntity);
|
|
509
|
+
tmpEntity.Parents = tmpEntity.Parents || [];
|
|
510
|
+
tmpEntity.Parents.push({ Entity: pParentEntity, KeyColumn: '', Mode: 'prefixed', CrossSession: true });
|
|
511
|
+
this._afterStrategyEdit();
|
|
512
|
+
}
|
|
513
|
+
removeStrategyParent(pEntity, pIndex) { const tmpEntity = this._strategyUIEntity(pEntity); if (tmpEntity.Parents) { tmpEntity.Parents.splice(Number(pIndex), 1); } this._afterStrategyEdit(); }
|
|
514
|
+
setParentEntity(pEntity, pIndex, pParentEntity) { const tmpParent = (this._strategyUIEntity(pEntity).Parents || [])[Number(pIndex)]; if (tmpParent) { tmpParent.Entity = pParentEntity; } this._afterStrategyEdit(); }
|
|
515
|
+
setParentKey(pEntity, pIndex, pColumn) { const tmpParent = (this._strategyUIEntity(pEntity).Parents || [])[Number(pIndex)]; if (tmpParent) { tmpParent.KeyColumn = pColumn; } this._afterStrategyEdit(); }
|
|
516
|
+
setParentMode(pEntity, pIndex, pMode) { const tmpParent = (this._strategyUIEntity(pEntity).Parents || [])[Number(pIndex)]; if (tmpParent) { tmpParent.Mode = pMode; } this._afterStrategyEdit(); }
|
|
517
|
+
|
|
518
|
+
/** Build the strategy-panel render data for one entity (mode + own-key + parents + live preview). */
|
|
519
|
+
_strategyEntityState(pEntityName, pSourceNames)
|
|
520
|
+
{
|
|
521
|
+
const tmpSession = this._session();
|
|
522
|
+
const tmpUI = (tmpSession.GUIDStrategyUI || {})[pEntityName] || { Mode: 'prefixed', OwnKeyColumn: '', Parents: [] };
|
|
523
|
+
const tmpCatalog = this.options.ContextEntityCatalog || {};
|
|
524
|
+
const tmpCatalogEntities = Object.keys(tmpCatalog);
|
|
525
|
+
const tmpModeOptions = (pSelected) => [ { Value: 'prefixed', Label: 'Prefixed (auto)', Selected: pSelected === 'prefixed' }, { Value: 'raw', Label: 'Raw GUID', Selected: pSelected === 'raw' }, { Value: 'rawid', Label: 'Raw ID', Selected: pSelected === 'rawid' } ];
|
|
526
|
+
const tmpColumnOptions = (pSelected) => [ { Value: '', Label: '(column)', Selected: !pSelected } ].concat(pSourceNames.map((pName) => ({ Value: pName, Label: pName, Selected: pName === pSelected })));
|
|
527
|
+
|
|
528
|
+
const tmpOwnKeySlot = ((tmpUI.Mode || 'prefixed') === 'prefixed')
|
|
529
|
+
? [ { WizardHash: this.options.WizardHash, Entity: pEntityName, Options: tmpColumnOptions(tmpUI.OwnKeyColumn) } ]
|
|
530
|
+
: [];
|
|
531
|
+
|
|
532
|
+
const tmpParents = (tmpUI.Parents || []).map((pParent, pIndex) => (
|
|
533
|
+
{
|
|
534
|
+
WizardHash: this.options.WizardHash, Entity: pEntityName, Index: pIndex,
|
|
535
|
+
EntityOptions: [ { Value: '', Label: '(entity)', Selected: !pParent.Entity } ].concat(tmpCatalogEntities.map((pName) => ({ Value: pName, Label: pName, Selected: pName === pParent.Entity }))),
|
|
536
|
+
KeyOptions: tmpColumnOptions(pParent.KeyColumn),
|
|
537
|
+
ModeOptions: tmpModeOptions(pParent.Mode || 'prefixed'),
|
|
538
|
+
}));
|
|
539
|
+
|
|
540
|
+
const tmpUsed = {};
|
|
541
|
+
(tmpUI.Parents || []).forEach((pParent) => { if (pParent.Entity) { tmpUsed[pParent.Entity] = true; } });
|
|
542
|
+
const tmpAddOptions = [ { Value: '', Label: '+ Related entity', Selected: true } ].concat(tmpCatalogEntities.filter((pName) => !tmpUsed[pName] && (pName !== pEntityName)).map((pName) => ({ Value: pName, Label: pName, Selected: false })));
|
|
543
|
+
const tmpAddParentSlot = (tmpAddOptions.length > 1) ? [ { WizardHash: this.options.WizardHash, Entity: pEntityName, Options: tmpAddOptions } ] : [];
|
|
544
|
+
|
|
545
|
+
let tmpPreviewSlot = [];
|
|
546
|
+
try
|
|
547
|
+
{
|
|
548
|
+
const tmpPreview = libStrategyApply.previewEntityGUID(tmpSession.GUIDStrategyUI || {}, this.options.GUIDStrategyPrefix || 'UI', tmpCatalog, this._schema, pEntityName, (tmpSession.SampleRows || [])[0] || {});
|
|
549
|
+
if (tmpPreview) { tmpPreviewSlot = [ { Preview: tmpPreview } ]; }
|
|
550
|
+
}
|
|
551
|
+
catch (pError) { /* preview is best-effort */ }
|
|
552
|
+
|
|
553
|
+
const tmpWarnings = (this._strategyWarnings || []).filter((pWarning) => pWarning.indexOf(`"${pEntityName}"`) >= 0).map((pWarning) => ({ Message: pWarning }));
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
WizardHash: this.options.WizardHash, Entity: pEntityName,
|
|
557
|
+
ModeOptions: tmpModeOptions(tmpUI.Mode || 'prefixed'),
|
|
558
|
+
OwnKeySlot: tmpOwnKeySlot, Parents: tmpParents, AddParentSlot: tmpAddParentSlot,
|
|
559
|
+
PreviewSlot: tmpPreviewSlot, Warnings: tmpWarnings,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
404
563
|
/** Auto-build a starting mapping from the schema: same-named source columns + a GUID template guess. */
|
|
405
564
|
_autoBuildMapping(pSchema)
|
|
406
565
|
{
|
|
@@ -472,7 +631,11 @@ class PictViewDataImportWizard extends libPictView
|
|
|
472
631
|
const tmpRequired = tmpSchemaEntity ? !!(tmpSchemaEntity.Fields.find((pField) => pField.Name === pFieldName) || {}).Required : false;
|
|
473
632
|
return { WizardHash: this.options.WizardHash, Entity: pEntityName, Name: pFieldName, Required: tmpRequired, Options: tmpOptions };
|
|
474
633
|
});
|
|
475
|
-
return {
|
|
634
|
+
return {
|
|
635
|
+
Entity: pEntityName, GUIDTemplate: tmpEntityMapping.GUIDTemplate || '', Fields: tmpFields,
|
|
636
|
+
LegacyGUIDSlot: this._strategyActive() ? [] : [ { GUIDTemplate: tmpEntityMapping.GUIDTemplate || '' } ],
|
|
637
|
+
StrategySlot: this._strategyActive() ? [ this._strategyEntityState(pEntityName, tmpSourceNames) ] : [],
|
|
638
|
+
};
|
|
476
639
|
});
|
|
477
640
|
|
|
478
641
|
return { WizardHash: this.options.WizardHash, Entities: tmpEntities, RawJSON: JSON.stringify(tmpMapping, null, 2) };
|
|
@@ -571,6 +734,11 @@ class PictViewDataImportWizard extends libPictView
|
|
|
571
734
|
URLPrefix: this.options.URLPrefix,
|
|
572
735
|
ComprehensionPushURL: this.options.ComprehensionPushURL,
|
|
573
736
|
AllowGUIDTruncation: this.options.AllowGUIDTruncation,
|
|
737
|
+
// When the GUID strategy owns length, the composer has already sized every GUID to its real
|
|
738
|
+
// column width. Hand the push target those widths so a marshaling adapter doesn't re-truncate a
|
|
739
|
+
// composed (already-fitting) GUID down to a smaller default — which would clip the entity's own
|
|
740
|
+
// distinguishing segment and collide. Only sent for the strategy path (no behavior change otherwise).
|
|
741
|
+
GUIDColumnSizes: (this.options.GUIDStrategy && this._schema) ? libStrategyApply.schemaSizes(this._schema) : undefined,
|
|
574
742
|
onProgress: (pPushed, pTotal) => { tmpSession.Push.Progress = { Pushed: pPushed, Total: pTotal }; this._renderStepBody('push'); },
|
|
575
743
|
};
|
|
576
744
|
Promise.resolve(tmpTarget.push(tmpSession.Comprehension, tmpContext)).then((pResult) =>
|