pict-section-dataimport 1.0.1 → 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.0
|
|
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>`
|
|
@@ -415,12 +437,129 @@ class PictViewDataImportWizard extends libPictView
|
|
|
415
437
|
if (typeof this.options.OnSchemaLoaded === 'function') { try { this.options.OnSchemaLoaded(this._schema); } catch (pError) { /* host */ } }
|
|
416
438
|
// Seed the mapping from the schema if the host gave no default mapping yet.
|
|
417
439
|
if (!tmpSession.Mapping.Order || tmpSession.Mapping.Order.length === 0) { tmpSession.Mapping = this._autoBuildMapping(this._schema); }
|
|
440
|
+
this._seedStrategyUI();
|
|
441
|
+
this._applyGUIDStrategy();
|
|
418
442
|
this._persist();
|
|
419
443
|
this._renderStepBody('mapping');
|
|
420
444
|
this._refreshAccordionChrome();
|
|
421
445
|
}).catch((pError) => { this._reportError(pError); });
|
|
422
446
|
}
|
|
423
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
|
+
|
|
424
563
|
/** Auto-build a starting mapping from the schema: same-named source columns + a GUID template guess. */
|
|
425
564
|
_autoBuildMapping(pSchema)
|
|
426
565
|
{
|
|
@@ -492,7 +631,11 @@ class PictViewDataImportWizard extends libPictView
|
|
|
492
631
|
const tmpRequired = tmpSchemaEntity ? !!(tmpSchemaEntity.Fields.find((pField) => pField.Name === pFieldName) || {}).Required : false;
|
|
493
632
|
return { WizardHash: this.options.WizardHash, Entity: pEntityName, Name: pFieldName, Required: tmpRequired, Options: tmpOptions };
|
|
494
633
|
});
|
|
495
|
-
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
|
+
};
|
|
496
639
|
});
|
|
497
640
|
|
|
498
641
|
return { WizardHash: this.options.WizardHash, Entities: tmpEntities, RawJSON: JSON.stringify(tmpMapping, null, 2) };
|
|
@@ -591,6 +734,11 @@ class PictViewDataImportWizard extends libPictView
|
|
|
591
734
|
URLPrefix: this.options.URLPrefix,
|
|
592
735
|
ComprehensionPushURL: this.options.ComprehensionPushURL,
|
|
593
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,
|
|
594
742
|
onProgress: (pPushed, pTotal) => { tmpSession.Push.Progress = { Pushed: pPushed, Total: pTotal }; this._renderStepBody('push'); },
|
|
595
743
|
};
|
|
596
744
|
Promise.resolve(tmpTarget.push(tmpSession.Comprehension, tmpContext)).then((pResult) =>
|