pict-section-dataimport 1.1.0 → 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.1.0",
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": [
@@ -79,6 +79,10 @@ module.exports = /*css*/`
79
79
  .psd-guid-parent { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; padding-left: 0.5rem; }
80
80
  .psd-guid-rm { padding: 0 0.45rem; line-height: 1.4; }
81
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); }
82
86
  .psd-guid-preview { font-size: var(--theme-typography-size-sm, 0.82rem); color: var(--theme-color-text-secondary, #6b7686); }
83
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); }
84
88
  `;
@@ -96,6 +96,10 @@ function buildStrategyConfig(pUIEntities, pPrefix)
96
96
  tmpEntities[pEntityName] = {
97
97
  Mode: tmpUI.Mode || 'prefixed',
98
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,
99
103
  OwnGUIDColumn: tmpUI.OwnGUIDColumn,
100
104
  ContextEntities: tmpContextEntities,
101
105
  ContextKeyColumns: tmpContextKeyColumns,
@@ -128,16 +132,14 @@ function previewGUID(pComposeSpec, pSampleRow)
128
132
  const tmpRow = pSampleRow || {};
129
133
  const tmpSegments = pComposeSpec.segments.map((pSegment) =>
130
134
  {
131
- const tmpMatch = /^\{~D:Record\.(.+)~\}$/.exec(pSegment.valueTemplate || '');
132
- let tmpValue = '';
133
- if (tmpMatch)
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) =>
134
139
  {
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
- }
140
+ const tmpCell = tmpRow[pColumn];
141
+ return (tmpCell !== undefined && tmpCell !== null) ? String(tmpCell) : '';
142
+ });
141
143
  return { abbrev: pSegment.abbrev, value: tmpValue };
142
144
  });
143
145
  return libEngine.composeGUID({ prefix: pComposeSpec.prefix, separator: pComposeSpec.separator, maxLength: pComposeSpec.maxLength, hashLength: pComposeSpec.hashLength, segments: tmpSegments });
@@ -104,14 +104,18 @@ const _DEFAULT_CONFIGURATION =
104
104
  Hash: 'DI-GUID-Panel',
105
105
  Template: /*html*/`
106
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>
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
108
  {~TS:DI-GUID-Parent:Record.Parents~}
109
109
  {~TS:DI-GUID-AddParent:Record.AddParentSlot~}
110
110
  {~TS:DI-GUID-Preview:Record.PreviewSlot~}
111
111
  {~TS:DI-GUID-Warn:Record.Warnings~}
112
112
  </div>`
113
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>` },
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)" />` },
115
119
  {
116
120
  Hash: 'DI-GUID-Parent',
117
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>`
@@ -465,7 +469,7 @@ class PictViewDataImportWizard extends libPictView
465
469
  {
466
470
  tmpSession.GUIDStrategyUI[pEntityName] = tmpDefaults[pEntityName]
467
471
  ? JSON.parse(JSON.stringify(tmpDefaults[pEntityName]))
468
- : { Mode: 'prefixed', OwnKeyColumn: tmpFirstColumn, Parents: [] };
472
+ : { Mode: 'prefixed', OwnKeyColumn: tmpFirstColumn, OwnKeyColumns: tmpFirstColumn ? [ tmpFirstColumn ] : [], OwnKeyMode: 'columns', OwnKeyTemplate: '', Parents: [] };
469
473
  }
470
474
  });
471
475
  }
@@ -489,7 +493,7 @@ class PictViewDataImportWizard extends libPictView
489
493
  {
490
494
  const tmpSession = this._session();
491
495
  tmpSession.GUIDStrategyUI = tmpSession.GUIDStrategyUI || {};
492
- if (!tmpSession.GUIDStrategyUI[pEntity]) { tmpSession.GUIDStrategyUI[pEntity] = { Mode: 'prefixed', OwnKeyColumn: '', Parents: [] }; }
496
+ if (!tmpSession.GUIDStrategyUI[pEntity]) { tmpSession.GUIDStrategyUI[pEntity] = { Mode: 'prefixed', OwnKeyColumn: '', OwnKeyColumns: [], OwnKeyMode: 'columns', OwnKeyTemplate: '', Parents: [] }; }
493
497
  return tmpSession.GUIDStrategyUI[pEntity];
494
498
  }
495
499
 
@@ -502,7 +506,49 @@ class PictViewDataImportWizard extends libPictView
502
506
  }
503
507
 
504
508
  setStrategyMode(pEntity, pMode) { this._strategyUIEntity(pEntity).Mode = pMode; this._afterStrategyEdit(); }
505
- setStrategyOwnKey(pEntity, pColumn) { this._strategyUIEntity(pEntity).OwnKeyColumn = pColumn; 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
+ }
506
552
  addStrategyParent(pEntity, pParentEntity)
507
553
  {
508
554
  const tmpEntity = this._strategyUIEntity(pEntity);
@@ -525,9 +571,26 @@ class PictViewDataImportWizard extends libPictView
525
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' } ];
526
572
  const tmpColumnOptions = (pSelected) => [ { Value: '', Label: '(column)', Selected: !pSelected } ].concat(pSourceNames.map((pName) => ({ Value: pName, Label: pName, Selected: pName === pSelected })));
527
573
 
528
- const tmpOwnKeySlot = ((tmpUI.Mode || 'prefixed') === 'prefixed')
529
- ? [ { WizardHash: this.options.WizardHash, Entity: pEntityName, Options: tmpColumnOptions(tmpUI.OwnKeyColumn) } ]
530
- : [];
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
+ }
531
594
 
532
595
  const tmpParents = (tmpUI.Parents || []).map((pParent, pIndex) => (
533
596
  {