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.0.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.0.43",
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><span class="psd-entity-count">GUID: <span class="psd-mono">{~D:Record.GUIDTemplate~}</span></span></div>
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">&times;</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
- if (!this._wired)
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
- /** One-time: create the accordion + upload view, render all step bodies. */
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 { Entity: pEntityName, GUIDTemplate: tmpEntityMapping.GUIDTemplate || '', Fields: tmpFields };
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) =>