pict-section-dataimport 1.0.1 → 1.1.1

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.1",
3
+ "version": "1.1.1",
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,19 @@ 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-owncol { display: inline-flex; align-items: center; gap: 0.15rem; }
83
+ .psd-guid-owadd { max-width: 8rem; opacity: 0.85; }
84
+ .psd-guid-tpl { min-width: 16rem; font-family: var(--theme-typography-family-mono, monospace); }
85
+ .psd-guid-own-fx { padding: 0 0.5rem; font-size: var(--theme-typography-size-sm, 0.78rem); }
86
+ .psd-guid-preview { font-size: var(--theme-typography-size-sm, 0.82rem); color: var(--theme-color-text-secondary, #6b7686); }
87
+ .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
88
  `;
@@ -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,168 @@
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
+ // Combinatorial own key: several columns concatenated, or a user-typed pict template. The engine's
100
+ // ownValueTemplate() prefers OwnKeyTemplate, then OwnKeyColumns, then the single OwnKeyColumn.
101
+ OwnKeyColumns: Array.isArray(tmpUI.OwnKeyColumns) ? tmpUI.OwnKeyColumns : undefined,
102
+ OwnKeyTemplate: tmpUI.OwnKeyTemplate,
103
+ OwnGUIDColumn: tmpUI.OwnGUIDColumn,
104
+ ContextEntities: tmpContextEntities,
105
+ ContextKeyColumns: tmpContextKeyColumns,
106
+ Joins: tmpParents.filter((pParent) => !!pParent.Entity).map((pParent) =>
107
+ {
108
+ const tmpMode = pParent.Mode || 'prefixed';
109
+ return {
110
+ ParentEntity: pParent.Entity,
111
+ Mode: tmpMode,
112
+ KeyColumn: pParent.KeyColumn,
113
+ GUIDColumn: pParent.KeyColumn,
114
+ IDColumn: pParent.KeyColumn,
115
+ CrossSession: (pParent.CrossSession !== false),
116
+ };
117
+ }),
118
+ };
119
+ });
120
+ return { Prefix: (pPrefix !== undefined) ? pPrefix : 'UI', Entities: tmpEntities };
121
+ }
122
+
123
+ /**
124
+ * Preview the GUID a compose spec would produce for a sample row (simple `{~D:Record.X~}` resolution, no
125
+ * fable needed) — used by the wizard panel to show a live example as the user configures the strategy.
126
+ * @param {Record<string, any>} pComposeSpec @param {Record<string, any>} pSampleRow
127
+ * @returns {string}
128
+ */
129
+ function previewGUID(pComposeSpec, pSampleRow)
130
+ {
131
+ if (!pComposeSpec || !Array.isArray(pComposeSpec.segments)) { return ''; }
132
+ const tmpRow = pSampleRow || {};
133
+ const tmpSegments = pComposeSpec.segments.map((pSegment) =>
134
+ {
135
+ // Resolve EVERY `{~D:Record.<col>~}` tag in the segment (a segment can now be multiple columns or a
136
+ // typed template, not just one column) — a lightweight parseTemplate stand-in for the live preview.
137
+ // Any non-tag literal text in the template is kept verbatim.
138
+ const tmpValue = String(pSegment.valueTemplate || '').replace(/\{~D:Record\.([^~}]+)~\}/g, (pFull, pColumn) =>
139
+ {
140
+ const tmpCell = tmpRow[pColumn];
141
+ return (tmpCell !== undefined && tmpCell !== null) ? String(tmpCell) : '';
142
+ });
143
+ return { abbrev: pSegment.abbrev, value: tmpValue };
144
+ });
145
+ return libEngine.composeGUID({ prefix: pComposeSpec.prefix, separator: pComposeSpec.separator, maxLength: pComposeSpec.maxLength, hashLength: pComposeSpec.hashLength, segments: tmpSegments });
146
+ }
147
+
148
+ /**
149
+ * One-call preview for the wizard panel: build the config from the UI model, compile it, and preview the
150
+ * given entity's OWN GUID on a sample row.
151
+ * @returns {string}
152
+ */
153
+ function previewEntityGUID(pUIEntities, pPrefix, pCatalog, pSchema, pEntityName, pSampleRow)
154
+ {
155
+ const tmpConfig = buildStrategyConfig(pUIEntities, pPrefix);
156
+ const tmpCompiled = libEngine.compileGUIDStrategy(tmpConfig, { Catalog: pCatalog || {}, SchemaSizes: schemaSizes(pSchema) });
157
+ const tmpStrategy = tmpCompiled.Strategies[pEntityName];
158
+ if (!tmpStrategy || !tmpStrategy.Own || !tmpStrategy.Own.Compose) { return ''; }
159
+ return previewGUID(tmpStrategy.Own.Compose, pSampleRow);
160
+ }
161
+
162
+ module.exports = {
163
+ applyStrategy,
164
+ schemaSizes,
165
+ buildStrategyConfig,
166
+ previewGUID,
167
+ previewEntityGUID,
168
+ };
@@ -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,35 @@ 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-Own: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-Own', Template: /*html*/`<span class="psd-guid-sub">keyed by</span>{~TS:DI-GUID-OwnCol:Record.ColumnItems~}{~TS:DI-GUID-OwnAdd:Record.AddColumnSlot~}{~TS:DI-GUID-OwnTpl:Record.TemplateSlot~}<button type="button" class="psd-btn psd-btn-ghost psd-guid-own-fx" onclick="_Pict.views['{~D:Record.WizardHash~}'].toggleStrategyOwnMode('{~D:Record.Entity~}')" title="Switch between columns and a typed template">{~D:Record.ToggleLabel~}</button>` },
115
+ { Hash: 'DI-GUID-OwnCol', Template: /*html*/`<span class="psd-guid-owncol"><select class="psd-select" onchange="_Pict.views['{~D:Record.WizardHash~}'].setStrategyOwnKeyAt('{~D:Record.Entity~}',{~D:Record.Index~},this.value)">{~TS:DI-Opt:Record.Options~}</select>{~TS:DI-GUID-OwnColRm:Record.RemoveSlot~}</span>` },
116
+ { Hash: 'DI-GUID-OwnColRm', Template: /*html*/`<button type="button" class="psd-btn psd-btn-ghost psd-guid-rm" onclick="_Pict.views['{~D:Record.WizardHash~}'].removeStrategyOwnKey('{~D:Record.Entity~}',{~D:Record.Index~})" title="Remove column">&times;</button>` },
117
+ { Hash: 'DI-GUID-OwnAdd', Template: /*html*/`<select class="psd-select psd-guid-owadd" onchange="if(this.value){_Pict.views['{~D:Record.WizardHash~}'].addStrategyOwnKey('{~D:Record.Entity~}',this.value);this.value='';}">{~TS:DI-Opt:Record.Options~}</select>` },
118
+ { Hash: 'DI-GUID-OwnTpl', Template: /*html*/`<input type="text" class="psd-input psd-guid-tpl" placeholder="{~D:Record.Placeholder~}" value="{~D:Record.Template~}" onchange="_Pict.views['{~D:Record.WizardHash~}'].setStrategyOwnKeyTemplate('{~D:Record.Entity~}',this.value)" />` },
119
+ {
120
+ Hash: 'DI-GUID-Parent',
121
+ 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>`
122
+ },
123
+ { 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>` },
124
+ { Hash: 'DI-GUID-Preview', Template: /*html*/`<div class="psd-guid-preview">Example: <span class="psd-mono">{~D:Record.Preview~}</span></div>` },
125
+ { Hash: 'DI-GUID-Warn', Template: /*html*/`<div class="psd-guid-warn">{~I:Warning~} <span>{~D:Record.Message~}</span></div>` },
100
126
  {
101
127
  Hash: 'DI-Map-Row',
102
128
  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 +441,188 @@ class PictViewDataImportWizard extends libPictView
415
441
  if (typeof this.options.OnSchemaLoaded === 'function') { try { this.options.OnSchemaLoaded(this._schema); } catch (pError) { /* host */ } }
416
442
  // Seed the mapping from the schema if the host gave no default mapping yet.
417
443
  if (!tmpSession.Mapping.Order || tmpSession.Mapping.Order.length === 0) { tmpSession.Mapping = this._autoBuildMapping(this._schema); }
444
+ this._seedStrategyUI();
445
+ this._applyGUIDStrategy();
418
446
  this._persist();
419
447
  this._renderStepBody('mapping');
420
448
  this._refreshAccordionChrome();
421
449
  }).catch((pError) => { this._reportError(pError); });
422
450
  }
423
451
 
452
+ /** Is the context-aware GUID strategy panel + composition enabled for this wizard? */
453
+ _strategyActive()
454
+ {
455
+ return !!this.options.GUIDStrategy;
456
+ }
457
+
458
+ /** Seed the editable per-entity GUID strategy UI model (host default, else prefixed + first column). */
459
+ _seedStrategyUI()
460
+ {
461
+ if (!this._strategyActive()) { return; }
462
+ const tmpSession = this._session();
463
+ tmpSession.GUIDStrategyUI = tmpSession.GUIDStrategyUI || {};
464
+ const tmpDefaults = this.options.GUIDStrategyDefault || {};
465
+ const tmpFirstColumn = ((tmpSession.DetectedColumns || [])[0] || {}).SourceName || '';
466
+ (tmpSession.Mapping.Order || []).forEach((pEntityName) =>
467
+ {
468
+ if (!tmpSession.GUIDStrategyUI[pEntityName])
469
+ {
470
+ tmpSession.GUIDStrategyUI[pEntityName] = tmpDefaults[pEntityName]
471
+ ? JSON.parse(JSON.stringify(tmpDefaults[pEntityName]))
472
+ : { Mode: 'prefixed', OwnKeyColumn: tmpFirstColumn, OwnKeyColumns: tmpFirstColumn ? [ tmpFirstColumn ] : [], OwnKeyMode: 'columns', OwnKeyTemplate: '', Parents: [] };
473
+ }
474
+ });
475
+ }
476
+
477
+ /**
478
+ * Compile the editable GUID strategy UI model (meadow-integration) against the schema's GUID column
479
+ * sizes + attach it to each entity mapping. The transform then composes stable, length-safe GUIDs +
480
+ * foreign-key fields instead of the flat GUIDTemplate.
481
+ */
482
+ _applyGUIDStrategy()
483
+ {
484
+ if (!this._strategyActive()) { return; }
485
+ const tmpSession = this._session();
486
+ const tmpConfig = libStrategyApply.buildStrategyConfig(tmpSession.GUIDStrategyUI || {}, this.options.GUIDStrategyPrefix || 'UI');
487
+ const tmpResult = libStrategyApply.applyStrategy(tmpSession.Mapping, tmpConfig, this.options.ContextEntityCatalog, this._schema);
488
+ this._strategyWarnings = tmpResult.Warnings || [];
489
+ }
490
+
491
+ /** Mutable per-entity UI model accessor (creates a default if absent). */
492
+ _strategyUIEntity(pEntity)
493
+ {
494
+ const tmpSession = this._session();
495
+ tmpSession.GUIDStrategyUI = tmpSession.GUIDStrategyUI || {};
496
+ if (!tmpSession.GUIDStrategyUI[pEntity]) { tmpSession.GUIDStrategyUI[pEntity] = { Mode: 'prefixed', OwnKeyColumn: '', OwnKeyColumns: [], OwnKeyMode: 'columns', OwnKeyTemplate: '', Parents: [] }; }
497
+ return tmpSession.GUIDStrategyUI[pEntity];
498
+ }
499
+
500
+ /** Recompile + re-render after any strategy panel edit. */
501
+ _afterStrategyEdit()
502
+ {
503
+ this._applyGUIDStrategy();
504
+ this._persist();
505
+ this._renderStepBody('mapping');
506
+ }
507
+
508
+ setStrategyMode(pEntity, pMode) { this._strategyUIEntity(pEntity).Mode = pMode; this._afterStrategyEdit(); }
509
+
510
+ // Combinatorial own key (#1): the entity's GUID can be keyed by ONE column, SEVERAL columns concatenated,
511
+ // or a user-typed pict template. OwnKeyColumns is the column list; OwnKeyTemplate (template mode) wins in
512
+ // the engine's ownValueTemplate(). OwnKeyColumn is kept in sync for backward compatibility.
513
+ /** The own-key column list (migrating a legacy single OwnKeyColumn on first access). */
514
+ _ownColumns(pEntity)
515
+ {
516
+ const tmpEntity = this._strategyUIEntity(pEntity);
517
+ if (!Array.isArray(tmpEntity.OwnKeyColumns))
518
+ {
519
+ tmpEntity.OwnKeyColumns = tmpEntity.OwnKeyColumn ? [ tmpEntity.OwnKeyColumn ] : [];
520
+ }
521
+ return tmpEntity.OwnKeyColumns;
522
+ }
523
+ _syncOwnSingle(pEntity) { const tmpEntity = this._strategyUIEntity(pEntity); tmpEntity.OwnKeyColumn = this._ownColumns(pEntity)[0] || ''; }
524
+ setStrategyOwnKey(pEntity, pColumn) { const tmpEntity = this._strategyUIEntity(pEntity); tmpEntity.OwnKeyColumns = pColumn ? [ pColumn ] : []; tmpEntity.OwnKeyColumn = pColumn; this._afterStrategyEdit(); }
525
+ setStrategyOwnKeyAt(pEntity, pIndex, pColumn)
526
+ {
527
+ const tmpColumns = this._ownColumns(pEntity);
528
+ if (pColumn) { tmpColumns[Number(pIndex)] = pColumn; } else { tmpColumns.splice(Number(pIndex), 1); }
529
+ this._syncOwnSingle(pEntity); this._afterStrategyEdit();
530
+ }
531
+ addStrategyOwnKey(pEntity, pColumn) { this._ownColumns(pEntity).push(pColumn); this._syncOwnSingle(pEntity); this._afterStrategyEdit(); }
532
+ removeStrategyOwnKey(pEntity, pIndex) { this._ownColumns(pEntity).splice(Number(pIndex), 1); this._syncOwnSingle(pEntity); this._afterStrategyEdit(); }
533
+ setStrategyOwnKeyTemplate(pEntity, pTemplate) { this._strategyUIEntity(pEntity).OwnKeyTemplate = pTemplate; this._afterStrategyEdit(); }
534
+ toggleStrategyOwnMode(pEntity)
535
+ {
536
+ const tmpEntity = this._strategyUIEntity(pEntity);
537
+ if (tmpEntity.OwnKeyMode === 'template')
538
+ {
539
+ tmpEntity.OwnKeyMode = 'columns';
540
+ tmpEntity.OwnKeyTemplate = ''; // columns win once template is cleared
541
+ }
542
+ else
543
+ {
544
+ tmpEntity.OwnKeyMode = 'template';
545
+ if (!tmpEntity.OwnKeyTemplate)
546
+ {
547
+ tmpEntity.OwnKeyTemplate = this._ownColumns(pEntity).map((pColumn) => `{~D:Record.${pColumn}~}`).join('');
548
+ }
549
+ }
550
+ this._afterStrategyEdit();
551
+ }
552
+ addStrategyParent(pEntity, pParentEntity)
553
+ {
554
+ const tmpEntity = this._strategyUIEntity(pEntity);
555
+ tmpEntity.Parents = tmpEntity.Parents || [];
556
+ tmpEntity.Parents.push({ Entity: pParentEntity, KeyColumn: '', Mode: 'prefixed', CrossSession: true });
557
+ this._afterStrategyEdit();
558
+ }
559
+ removeStrategyParent(pEntity, pIndex) { const tmpEntity = this._strategyUIEntity(pEntity); if (tmpEntity.Parents) { tmpEntity.Parents.splice(Number(pIndex), 1); } this._afterStrategyEdit(); }
560
+ setParentEntity(pEntity, pIndex, pParentEntity) { const tmpParent = (this._strategyUIEntity(pEntity).Parents || [])[Number(pIndex)]; if (tmpParent) { tmpParent.Entity = pParentEntity; } this._afterStrategyEdit(); }
561
+ setParentKey(pEntity, pIndex, pColumn) { const tmpParent = (this._strategyUIEntity(pEntity).Parents || [])[Number(pIndex)]; if (tmpParent) { tmpParent.KeyColumn = pColumn; } this._afterStrategyEdit(); }
562
+ setParentMode(pEntity, pIndex, pMode) { const tmpParent = (this._strategyUIEntity(pEntity).Parents || [])[Number(pIndex)]; if (tmpParent) { tmpParent.Mode = pMode; } this._afterStrategyEdit(); }
563
+
564
+ /** Build the strategy-panel render data for one entity (mode + own-key + parents + live preview). */
565
+ _strategyEntityState(pEntityName, pSourceNames)
566
+ {
567
+ const tmpSession = this._session();
568
+ const tmpUI = (tmpSession.GUIDStrategyUI || {})[pEntityName] || { Mode: 'prefixed', OwnKeyColumn: '', Parents: [] };
569
+ const tmpCatalog = this.options.ContextEntityCatalog || {};
570
+ const tmpCatalogEntities = Object.keys(tmpCatalog);
571
+ 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' } ];
572
+ const tmpColumnOptions = (pSelected) => [ { Value: '', Label: '(column)', Selected: !pSelected } ].concat(pSourceNames.map((pName) => ({ Value: pName, Label: pName, Selected: pName === pSelected })));
573
+
574
+ // Own-key slot: only prefixed mode composes its own GUID. It can be one column, several columns
575
+ // (concatenated), or a typed template — see #1 combinatorial GUID.
576
+ let tmpOwnKeySlot = [];
577
+ if ((tmpUI.Mode || 'prefixed') === 'prefixed')
578
+ {
579
+ const tmpIsTemplate = (tmpUI.OwnKeyMode === 'template');
580
+ const tmpOwnCols = Array.isArray(tmpUI.OwnKeyColumns) ? tmpUI.OwnKeyColumns : (tmpUI.OwnKeyColumn ? [ tmpUI.OwnKeyColumn ] : []);
581
+ const tmpColumnItems = tmpIsTemplate ? [] : tmpOwnCols.map((pColumn, pIndex) => (
582
+ {
583
+ WizardHash: this.options.WizardHash, Entity: pEntityName, Index: pIndex,
584
+ Options: tmpColumnOptions(pColumn),
585
+ RemoveSlot: (tmpOwnCols.length > 1) ? [ { WizardHash: this.options.WizardHash, Entity: pEntityName, Index: pIndex } ] : [],
586
+ }));
587
+ const tmpUsedOwn = {};
588
+ tmpOwnCols.forEach((pColumn) => { if (pColumn) { tmpUsedOwn[pColumn] = true; } });
589
+ const tmpAddOwnOptions = [ { Value: '', Label: '+ column', Selected: true } ].concat(pSourceNames.filter((pName) => !tmpUsedOwn[pName]).map((pName) => ({ Value: pName, Label: pName, Selected: false })));
590
+ const tmpAddColumnSlot = (!tmpIsTemplate && (tmpAddOwnOptions.length > 1)) ? [ { WizardHash: this.options.WizardHash, Entity: pEntityName, Options: tmpAddOwnOptions } ] : [];
591
+ const tmpTemplateSlot = tmpIsTemplate ? [ { WizardHash: this.options.WizardHash, Entity: pEntityName, Template: tmpUI.OwnKeyTemplate || '', Placeholder: 'e.g. {~D:Record.District~}-{~D:Record.Code~}' } ] : [];
592
+ tmpOwnKeySlot = [ { WizardHash: this.options.WizardHash, Entity: pEntityName, ColumnItems: tmpColumnItems, AddColumnSlot: tmpAddColumnSlot, TemplateSlot: tmpTemplateSlot, ToggleLabel: tmpIsTemplate ? 'use columns' : 'ƒ template' } ];
593
+ }
594
+
595
+ const tmpParents = (tmpUI.Parents || []).map((pParent, pIndex) => (
596
+ {
597
+ WizardHash: this.options.WizardHash, Entity: pEntityName, Index: pIndex,
598
+ EntityOptions: [ { Value: '', Label: '(entity)', Selected: !pParent.Entity } ].concat(tmpCatalogEntities.map((pName) => ({ Value: pName, Label: pName, Selected: pName === pParent.Entity }))),
599
+ KeyOptions: tmpColumnOptions(pParent.KeyColumn),
600
+ ModeOptions: tmpModeOptions(pParent.Mode || 'prefixed'),
601
+ }));
602
+
603
+ const tmpUsed = {};
604
+ (tmpUI.Parents || []).forEach((pParent) => { if (pParent.Entity) { tmpUsed[pParent.Entity] = true; } });
605
+ const tmpAddOptions = [ { Value: '', Label: '+ Related entity', Selected: true } ].concat(tmpCatalogEntities.filter((pName) => !tmpUsed[pName] && (pName !== pEntityName)).map((pName) => ({ Value: pName, Label: pName, Selected: false })));
606
+ const tmpAddParentSlot = (tmpAddOptions.length > 1) ? [ { WizardHash: this.options.WizardHash, Entity: pEntityName, Options: tmpAddOptions } ] : [];
607
+
608
+ let tmpPreviewSlot = [];
609
+ try
610
+ {
611
+ const tmpPreview = libStrategyApply.previewEntityGUID(tmpSession.GUIDStrategyUI || {}, this.options.GUIDStrategyPrefix || 'UI', tmpCatalog, this._schema, pEntityName, (tmpSession.SampleRows || [])[0] || {});
612
+ if (tmpPreview) { tmpPreviewSlot = [ { Preview: tmpPreview } ]; }
613
+ }
614
+ catch (pError) { /* preview is best-effort */ }
615
+
616
+ const tmpWarnings = (this._strategyWarnings || []).filter((pWarning) => pWarning.indexOf(`"${pEntityName}"`) >= 0).map((pWarning) => ({ Message: pWarning }));
617
+
618
+ return {
619
+ WizardHash: this.options.WizardHash, Entity: pEntityName,
620
+ ModeOptions: tmpModeOptions(tmpUI.Mode || 'prefixed'),
621
+ OwnKeySlot: tmpOwnKeySlot, Parents: tmpParents, AddParentSlot: tmpAddParentSlot,
622
+ PreviewSlot: tmpPreviewSlot, Warnings: tmpWarnings,
623
+ };
624
+ }
625
+
424
626
  /** Auto-build a starting mapping from the schema: same-named source columns + a GUID template guess. */
425
627
  _autoBuildMapping(pSchema)
426
628
  {
@@ -492,7 +694,11 @@ class PictViewDataImportWizard extends libPictView
492
694
  const tmpRequired = tmpSchemaEntity ? !!(tmpSchemaEntity.Fields.find((pField) => pField.Name === pFieldName) || {}).Required : false;
493
695
  return { WizardHash: this.options.WizardHash, Entity: pEntityName, Name: pFieldName, Required: tmpRequired, Options: tmpOptions };
494
696
  });
495
- return { Entity: pEntityName, GUIDTemplate: tmpEntityMapping.GUIDTemplate || '', Fields: tmpFields };
697
+ return {
698
+ Entity: pEntityName, GUIDTemplate: tmpEntityMapping.GUIDTemplate || '', Fields: tmpFields,
699
+ LegacyGUIDSlot: this._strategyActive() ? [] : [ { GUIDTemplate: tmpEntityMapping.GUIDTemplate || '' } ],
700
+ StrategySlot: this._strategyActive() ? [ this._strategyEntityState(pEntityName, tmpSourceNames) ] : [],
701
+ };
496
702
  });
497
703
 
498
704
  return { WizardHash: this.options.WizardHash, Entities: tmpEntities, RawJSON: JSON.stringify(tmpMapping, null, 2) };
@@ -591,6 +797,11 @@ class PictViewDataImportWizard extends libPictView
591
797
  URLPrefix: this.options.URLPrefix,
592
798
  ComprehensionPushURL: this.options.ComprehensionPushURL,
593
799
  AllowGUIDTruncation: this.options.AllowGUIDTruncation,
800
+ // When the GUID strategy owns length, the composer has already sized every GUID to its real
801
+ // column width. Hand the push target those widths so a marshaling adapter doesn't re-truncate a
802
+ // composed (already-fitting) GUID down to a smaller default — which would clip the entity's own
803
+ // distinguishing segment and collide. Only sent for the strategy path (no behavior change otherwise).
804
+ GUIDColumnSizes: (this.options.GUIDStrategy && this._schema) ? libStrategyApply.schemaSizes(this._schema) : undefined,
594
805
  onProgress: (pPushed, pTotal) => { tmpSession.Push.Progress = { Pushed: pPushed, Total: pTotal }; this._renderStepBody('push'); },
595
806
  };
596
807
  Promise.resolve(tmpTarget.push(tmpSession.Comprehension, tmpContext)).then((pResult) =>