meadow-integration 1.0.20 → 1.0.21

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.
Files changed (29) hide show
  1. package/example-applications/mapping-demo/.quackage.json +10 -0
  2. package/example-applications/mapping-demo/README.md +99 -0
  3. package/example-applications/mapping-demo/data/books-sample.csv +21 -0
  4. package/example-applications/mapping-demo/generate-build-config.js +44 -0
  5. package/example-applications/mapping-demo/mappings/books-to-book.json +14 -0
  6. package/example-applications/mapping-demo/package.json +14 -0
  7. package/example-applications/mapping-demo/server.js +814 -0
  8. package/example-applications/mapping-demo/source/MappingDemoApp.js +52 -0
  9. package/example-applications/mapping-demo/source/views/MappingDemoEditorView.js +186 -0
  10. package/example-applications/mapping-demo/web/index.html +892 -0
  11. package/example-applications/mapping-demo/web/mapping-demo-editor.js +3195 -0
  12. package/example-applications/mapping-demo/web/mapping-demo-editor.js.map +1 -0
  13. package/example-applications/mapping-demo/web/mapping-demo-editor.min.js +2 -0
  14. package/example-applications/mapping-demo/web/mapping-demo-editor.min.js.map +1 -0
  15. package/example-applications/mapping-demo/web/pict.min.js +12 -0
  16. package/package.json +8 -4
  17. package/source/Meadow-Integration-Browser.js +31 -0
  18. package/source/Meadow-Integration.js +16 -1
  19. package/source/services/certainty/Service-CertaintyAccumulator.js +402 -0
  20. package/source/services/clone/Meadow-Service-Sync-Entity-Initial.js +16 -3
  21. package/source/services/clone/Meadow-Service-Sync-Entity-Ongoing.js +15 -2
  22. package/source/services/clone/Meadow-Service-Sync.js +21 -0
  23. package/source/views/MappingEditor-SchemaUtils.js +71 -0
  24. package/source/views/PictView-MeadowMappingEditor.js +1299 -0
  25. package/source/views/flow-cards/FlowCard-MappingSource.js +50 -0
  26. package/source/views/flow-cards/FlowCard-MappingTarget.js +49 -0
  27. package/source/views/flow-cards/FlowCard-SolverExpression.js +78 -0
  28. package/source/views/flow-cards/FlowCard-TemplateExpression.js +77 -0
  29. package/test/Meadow-Integration-CloneDeleteSync_test.js +809 -0
@@ -0,0 +1,1299 @@
1
+ const libPictView = require('pict-view');
2
+
3
+ const libSchemaUtils = require('./MappingEditor-SchemaUtils.js');
4
+
5
+ const _ViewConfiguration =
6
+ {
7
+ ViewIdentifier: "MeadowMappingEditor",
8
+
9
+ DefaultRenderable: "MeadowMappingEditor-Content",
10
+ DefaultDestinationAddress: "#MeadowMap-Editor-Container",
11
+
12
+ AutoRender: false,
13
+
14
+ CSS: /*css*/`
15
+ /* Meadow Mapping Editor */
16
+ .meadow-mapping-editor {
17
+ display: none;
18
+ }
19
+ .meadow-mapping-editor.active {
20
+ display: block;
21
+ }
22
+ .meadow-mapping-header {
23
+ display: flex;
24
+ align-items: center;
25
+ gap: 1em;
26
+ margin-bottom: 1em;
27
+ }
28
+ .meadow-mapping-header h3 {
29
+ margin: 0;
30
+ flex: 1;
31
+ }
32
+ .meadow-mapping-list-table {
33
+ width: 100%;
34
+ border-collapse: collapse;
35
+ margin-bottom: 1em;
36
+ }
37
+ .meadow-mapping-list-table th {
38
+ text-align: left;
39
+ font-size: 0.72em;
40
+ font-weight: 600;
41
+ text-transform: uppercase;
42
+ letter-spacing: 0.5px;
43
+ color: var(--facto-text-tertiary, #a09070);
44
+ padding: 0.5em 0.4em;
45
+ border-bottom: 1px solid var(--facto-border, #d6c8ae);
46
+ }
47
+ .meadow-mapping-list-table td {
48
+ padding: 0.35em 0.4em;
49
+ border-bottom: 1px solid var(--facto-border-subtle, #e8ddc8);
50
+ vertical-align: middle;
51
+ }
52
+ .meadow-flow-container {
53
+ width: 100%;
54
+ height: 500px;
55
+ border: 1px solid var(--facto-border, #d6c8ae);
56
+ border-radius: 6px;
57
+ background: var(--facto-bg-surface, #fcf8f0);
58
+ margin-bottom: 0.75em;
59
+ }
60
+ .meadow-mapping-json-editor {
61
+ width: 100%;
62
+ min-height: 300px;
63
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
64
+ font-size: 0.85em;
65
+ padding: 0.75em;
66
+ border: 1px solid var(--facto-border, #d6c8ae);
67
+ border-radius: 6px;
68
+ background: var(--facto-bg-input, #fcf8f0);
69
+ color: var(--facto-text, #3a3020);
70
+ resize: vertical;
71
+ tab-size: 4;
72
+ }
73
+ .meadow-mapping-store-checklist {
74
+ display: flex;
75
+ flex-wrap: wrap;
76
+ gap: 0.5em;
77
+ margin-top: 0.25em;
78
+ }
79
+ .meadow-mapping-store-checklist label {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 0.35em;
83
+ font-size: 0.82em;
84
+ cursor: pointer;
85
+ padding: 0.3em 0.5em;
86
+ border: 1px solid var(--facto-border-subtle, #e8ddc8);
87
+ border-radius: 4px;
88
+ background: var(--facto-bg-input, #fcf8f0);
89
+ }
90
+ .meadow-mapping-store-checklist label:has(input:checked) {
91
+ border-color: var(--facto-brand, #18a5a0);
92
+ background: var(--facto-brand-a12, rgba(24,165,160,0.12));
93
+ }
94
+ .meadow-mapping-btn {
95
+ display: inline-flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ padding: 0.35em 0.9em;
99
+ font-size: 0.82em;
100
+ font-weight: 500;
101
+ border-radius: 4px;
102
+ border: 1px solid transparent;
103
+ cursor: pointer;
104
+ text-decoration: none;
105
+ line-height: 1.4;
106
+ }
107
+ .meadow-mapping-btn-primary {
108
+ background: var(--facto-brand, #18a5a0);
109
+ color: #fff;
110
+ border-color: var(--facto-brand, #18a5a0);
111
+ }
112
+ .meadow-mapping-btn-primary:hover {
113
+ opacity: 0.88;
114
+ }
115
+ .meadow-mapping-btn-secondary {
116
+ background: var(--facto-bg-input, #fcf8f0);
117
+ color: var(--facto-text, #3a3020);
118
+ border-color: var(--facto-border, #d6c8ae);
119
+ }
120
+ .meadow-mapping-btn-secondary:hover {
121
+ background: var(--facto-border-subtle, #e8ddc8);
122
+ }
123
+ .meadow-mapping-btn-danger {
124
+ background: #e74c3c;
125
+ color: #fff;
126
+ border-color: #e74c3c;
127
+ }
128
+ .meadow-mapping-btn-danger:hover {
129
+ opacity: 0.88;
130
+ }
131
+ .meadow-mapping-btn-small {
132
+ padding: 0.2em 0.6em;
133
+ font-size: 0.78em;
134
+ }
135
+ .meadow-schema-mode-tabs {
136
+ display: flex;
137
+ gap: 0.25em;
138
+ }
139
+ .meadow-schema-mode-tab {
140
+ padding: 0.25em 0.75em;
141
+ font-size: 0.8em;
142
+ border: 1px solid var(--facto-border, #d6c8ae);
143
+ border-radius: 4px;
144
+ cursor: pointer;
145
+ background: var(--facto-bg-input, #fcf8f0);
146
+ color: var(--facto-text, #3a3020);
147
+ }
148
+ .meadow-schema-mode-tab.active {
149
+ background: var(--facto-brand, #18a5a0);
150
+ color: #fff;
151
+ border-color: var(--facto-brand, #18a5a0);
152
+ }
153
+ .meadow-section-title {
154
+ font-size: 0.72em;
155
+ font-weight: 600;
156
+ text-transform: uppercase;
157
+ letter-spacing: 0.5px;
158
+ color: var(--facto-text-tertiary, #a09070);
159
+ }
160
+ `,
161
+
162
+ Templates:
163
+ [
164
+ {
165
+ Hash: "MeadowMappingEditor-Template",
166
+ Template: /*html*/`
167
+ <div>
168
+ <div id="MeadowMap-Editor" class="meadow-mapping-editor">
169
+ <div class="meadow-mapping-header">
170
+ <button class="meadow-mapping-btn meadow-mapping-btn-secondary meadow-mapping-btn-small" onclick="{~P~}.views['MeadowMappingEditor'].closeMappingEditor()">&larr; Back</button>
171
+ <h3 id="MeadowMap-Title">Mapping Editor</h3>
172
+ <div class="meadow-schema-mode-tabs">
173
+ <button class="meadow-schema-mode-tab active" id="MeadowMap-Mode-Flow" onclick="{~P~}.views['MeadowMappingEditor'].switchMapMode('flow')">Visual Mapper</button>
174
+ <button class="meadow-schema-mode-tab" id="MeadowMap-Mode-JSON" onclick="{~P~}.views['MeadowMappingEditor'].switchMapMode('json')">JSON Config</button>
175
+ </div>
176
+ </div>
177
+
178
+ <div id="MeadowMap-List-Wrap">
179
+ <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:0.75em;">
180
+ <div class="meadow-section-title" style="margin:0;">Existing Mappings</div>
181
+ <button class="meadow-mapping-btn meadow-mapping-btn-primary meadow-mapping-btn-small" onclick="{~P~}.views['MeadowMappingEditor'].newMapping()">+ New Mapping</button>
182
+ </div>
183
+ <div id="MeadowMap-List"></div>
184
+ </div>
185
+
186
+ <div id="MeadowMap-Detail" style="display:none;">
187
+ <div style="display:flex; gap:0.5em; align-items:center; margin-bottom:0.75em;">
188
+ <label style="font-size:0.78em; font-weight:600;">Mapping Name</label>
189
+ <input type="text" id="MeadowMap-Name" placeholder="Mapping name" style="flex:1; padding:0.3em 0.5em; font-size:0.85em; border:1px solid var(--facto-border); border-radius:4px; background:var(--facto-bg-input); color:var(--facto-text);">
190
+ </div>
191
+
192
+ <div style="display:flex; gap:0.5em; align-items:center; margin-bottom:0.75em;">
193
+ <label style="font-size:0.78em; font-weight:600;">Source</label>
194
+ <select id="MeadowMap-Source" style="flex:1; padding:0.3em 0.5em; font-size:0.85em; border:1px solid var(--facto-border); border-radius:4px;"></select>
195
+ <button class="meadow-mapping-btn meadow-mapping-btn-secondary meadow-mapping-btn-small" onclick="{~P~}.views['MeadowMappingEditor'].discoverSourceFields()">Discover Fields</button>
196
+ </div>
197
+
198
+ <div id="MeadowMap-Flow-Wrap">
199
+ <div id="MeadowMap-Flow-Container" class="meadow-flow-container"></div>
200
+ </div>
201
+
202
+ <div id="MeadowMap-JSON-Wrap" style="display:none;">
203
+ <textarea class="meadow-mapping-json-editor" id="MeadowMap-JSON" placeholder='{"Entity":"MyTable","GUIDTemplate":"{~D:Record.IDRecord~}","Mappings":{},"Solvers":[],"ManyfestAddresses":false}'></textarea>
204
+ </div>
205
+
206
+ <div style="margin-top:0.75em;">
207
+ <div style="font-size:0.72em; font-weight:600; text-transform:uppercase; letter-spacing:0.5px; color:var(--facto-text-tertiary); margin-bottom:0.35em;">Target Stores</div>
208
+ <div id="MeadowMap-Stores" class="meadow-mapping-store-checklist"></div>
209
+ </div>
210
+
211
+ <div style="margin-top:0.75em; display:flex; gap:0.5em; flex-wrap:wrap; align-items:center;">
212
+ <button class="meadow-mapping-btn meadow-mapping-btn-primary" onclick="{~P~}.views['MeadowMappingEditor'].saveMapping()">Save Mapping</button>
213
+ </div>
214
+ </div>
215
+ </div>
216
+ </div>
217
+ `
218
+ }
219
+ ],
220
+
221
+ Renderables:
222
+ [
223
+ {
224
+ RenderableHash: "MeadowMappingEditor-Content",
225
+ TemplateHash: "MeadowMappingEditor-Template",
226
+ DestinationAddress: "#MeadowMap-Editor-Container",
227
+ RenderMethod: "replace"
228
+ }
229
+ ]
230
+ };
231
+
232
+ class MeadowMappingEditorView extends libPictView
233
+ {
234
+ constructor(pFable, pOptions, pServiceHash)
235
+ {
236
+ super(pFable, pOptions, pServiceHash);
237
+
238
+ this._EditingContextID = 0;
239
+ this._EditingName = '';
240
+ this._CurrentMappings = [];
241
+ this._SelectedMappingID = 0;
242
+ this._DiscoveredFields = {};
243
+ this._FlowView = null;
244
+ this._MapEditorMode = 'flow';
245
+ this._MappingSources = [];
246
+ this._MappingStores = [];
247
+ this._CurrentTargetSchema = null;
248
+ }
249
+
250
+ // ── Overridable data methods ─────────────────────────────────────────────
251
+ // Embedding apps override these to wire up their own persistence layer.
252
+
253
+ /** Load all mappings for a context (e.g. dataset). Must return a Promise
254
+ * that resolves to { Mappings: [...] }. */
255
+ _doLoadMappings(pContextID)
256
+ {
257
+ return Promise.resolve({ Mappings: [] });
258
+ }
259
+
260
+ /** Load all available sources. Must return a Promise that resolves to an
261
+ * array of source objects with at least { IDSource, Name }. */
262
+ _doLoadSources()
263
+ {
264
+ return Promise.resolve([]);
265
+ }
266
+
267
+ /** Load all available target stores for a context. Must return a Promise
268
+ * that resolves to { Stores: [...] }. */
269
+ _doLoadStores(pContextID)
270
+ {
271
+ return Promise.resolve({ Stores: [] });
272
+ }
273
+
274
+ /** Load the target schema for a context. Must return a Promise that
275
+ * resolves to { SchemaDefinition: "<micro-DDL string>" }. */
276
+ _doLoadTargetSchema(pContextID)
277
+ {
278
+ return Promise.resolve({ SchemaDefinition: '' });
279
+ }
280
+
281
+ /** Load a single mapping by ID. Must return a Promise that resolves to
282
+ * { Mapping: { Name, IDSource, IDProjectionStore, MappingConfiguration,
283
+ * FlowDiagramState, Active, ... } }. */
284
+ _doLoadMapping(pMappingID)
285
+ {
286
+ return Promise.resolve({ Mapping: null });
287
+ }
288
+
289
+ /** Delete a mapping by ID. Must return a Promise. */
290
+ _doDeleteMapping(pMappingID)
291
+ {
292
+ return Promise.resolve({});
293
+ }
294
+
295
+ /** Discover fields from a source dataset. Must return a Promise that
296
+ * resolves to { Headers: [...], SampleSize: N }. */
297
+ _doDiscoverSourceFields(pContextID, pSourceID, pRecordLimit)
298
+ {
299
+ return Promise.resolve({ Headers: [], SampleSize: 0 });
300
+ }
301
+
302
+ /** Create a new mapping. Must return a Promise that resolves to
303
+ * { Mapping: { IDProjectionMapping, ... } }. */
304
+ _doCreateMapping(pContextID, pData)
305
+ {
306
+ return Promise.resolve({ Mapping: {} });
307
+ }
308
+
309
+ /** Update an existing mapping. Must return a Promise that resolves to
310
+ * { Mapping: { ... } }. */
311
+ _doUpdateMapping(pMappingID, pData)
312
+ {
313
+ return Promise.resolve({ Mapping: {} });
314
+ }
315
+
316
+ /** Called when the editor is closed. Override to notify the parent view. */
317
+ _onClose()
318
+ {
319
+ // Default: no-op. Override in embedding app.
320
+ }
321
+
322
+ /** Show a toast notification. */
323
+ _doToast(pMessage, pOptions)
324
+ {
325
+ let tmpModal = this.pict.views && this.pict.views['Pict-Section-Modal'];
326
+ if (tmpModal && typeof tmpModal.toast === 'function')
327
+ {
328
+ tmpModal.toast(pMessage, pOptions);
329
+ }
330
+ else
331
+ {
332
+ this.log.info('[MeadowMappingEditor] ' + pMessage);
333
+ }
334
+ }
335
+
336
+ /** Show a confirmation dialog. Returns a Promise<boolean>. */
337
+ _doConfirm(pMessage, pOptions)
338
+ {
339
+ let tmpModal = this.pict.views && this.pict.views['Pict-Section-Modal'];
340
+ if (tmpModal && typeof tmpModal.confirm === 'function')
341
+ {
342
+ return tmpModal.confirm(pMessage, pOptions);
343
+ }
344
+ // Fallback to native confirm
345
+ return Promise.resolve(typeof window !== 'undefined' ? window.confirm(pMessage) : false);
346
+ }
347
+
348
+ // ── Public API ───────────────────────────────────────────────────────────
349
+
350
+ editMappings(pContextID, pName)
351
+ {
352
+ this._EditingContextID = pContextID;
353
+ this._EditingName = pName || '';
354
+
355
+ // Render the sub-view so its DOM exists
356
+ this.render();
357
+
358
+ let tmpEditor = document.getElementById('MeadowMap-Editor');
359
+ let tmpTitle = document.getElementById('MeadowMap-Title');
360
+
361
+ if (tmpEditor)
362
+ {
363
+ tmpEditor.classList.add('active');
364
+ tmpEditor.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
365
+ }
366
+ if (tmpTitle) tmpTitle.textContent = 'Mappings: ' + (pName || 'Untitled');
367
+
368
+ // Show the mapping list, hide detail
369
+ let tmpMappingListWrap = document.getElementById('MeadowMap-List-Wrap');
370
+ let tmpMappingDetail = document.getElementById('MeadowMap-Detail');
371
+ if (tmpMappingListWrap) tmpMappingListWrap.style.display = '';
372
+ if (tmpMappingDetail) tmpMappingDetail.style.display = 'none';
373
+
374
+ // Load mappings, sources, stores, and fresh schema in parallel
375
+ Promise.all(
376
+ [
377
+ this._doLoadMappings(pContextID),
378
+ this._doLoadSources(),
379
+ this._doLoadStores(pContextID),
380
+ this._doLoadTargetSchema(pContextID)
381
+ ]).then(
382
+ (pResults) =>
383
+ {
384
+ this._CurrentMappings = (pResults[0] && pResults[0].Mappings) ? pResults[0].Mappings : [];
385
+ this._MappingSources = Array.isArray(pResults[1]) ? pResults[1] : [];
386
+ this._MappingStores = (pResults[2] && pResults[2].Stores) ? pResults[2].Stores : [];
387
+
388
+ // Pre-populate _DiscoveredFields from source Columns (config-driven).
389
+ // Sources that include a Columns array provide field names without
390
+ // requiring a separate "Discover Fields" API call.
391
+ for (let i = 0; i < this._MappingSources.length; i++)
392
+ {
393
+ let tmpSrc = this._MappingSources[i];
394
+ if (Array.isArray(tmpSrc.Columns) && tmpSrc.Columns.length > 0)
395
+ {
396
+ this._DiscoveredFields[tmpSrc.IDSource] = tmpSrc.Columns;
397
+ }
398
+ }
399
+
400
+ // Store the fresh schema locally for use by flow nodes
401
+ let tmpSchema = pResults[3];
402
+ if (tmpSchema && tmpSchema.SchemaDefinition)
403
+ {
404
+ this._CurrentTargetSchema = tmpSchema.SchemaDefinition;
405
+ }
406
+
407
+ this.refreshMappingList();
408
+ });
409
+ }
410
+
411
+ closeMappingEditor()
412
+ {
413
+ // Clean up flow view
414
+ if (this._FlowView)
415
+ {
416
+ this._FlowView = null;
417
+ }
418
+
419
+ this._SelectedMappingID = 0;
420
+
421
+ this._onClose();
422
+ }
423
+
424
+ refreshMappingList()
425
+ {
426
+ let tmpContainer = document.getElementById('MeadowMap-List');
427
+ if (!tmpContainer) return;
428
+
429
+ if (this._CurrentMappings.length === 0)
430
+ {
431
+ tmpContainer.innerHTML = '<div style="text-align:center; padding:1.5em; color:var(--facto-text-tertiary, #a09070);">No mappings yet. Create one to map source fields to target columns.</div>';
432
+ return;
433
+ }
434
+
435
+ let tmpViewID = this.options.ViewIdentifier;
436
+
437
+ let tmpHtml = '<table class="meadow-mapping-list-table"><thead><tr>';
438
+ tmpHtml += '<th>ID</th><th>Name</th><th>Source</th><th>Active</th><th>Actions</th>';
439
+ tmpHtml += '</tr></thead><tbody>';
440
+
441
+ for (let i = 0; i < this._CurrentMappings.length; i++)
442
+ {
443
+ let tmpMap = this._CurrentMappings[i];
444
+ let tmpSourceName = '\u2014';
445
+ for (let j = 0; j < this._MappingSources.length; j++)
446
+ {
447
+ if (this._MappingSources[j].IDSource === tmpMap.IDSource)
448
+ {
449
+ tmpSourceName = this._MappingSources[j].Name || 'Source ' + tmpMap.IDSource;
450
+ break;
451
+ }
452
+ }
453
+
454
+ tmpHtml += '<tr>';
455
+ tmpHtml += '<td>' + tmpMap.IDProjectionMapping + '</td>';
456
+ tmpHtml += '<td><strong>' + (tmpMap.Name || '\u2014') + '</strong></td>';
457
+ tmpHtml += '<td>' + tmpSourceName + '</td>';
458
+ tmpHtml += '<td>' + (tmpMap.Active ? '\u2713' : '\u2717') + '</td>';
459
+ tmpHtml += '<td>';
460
+ tmpHtml += '<button class="meadow-mapping-btn meadow-mapping-btn-primary meadow-mapping-btn-small" onclick="window._Pict.views[\'' + tmpViewID + '\'].openMappingDetail(' + tmpMap.IDProjectionMapping + ')">Edit</button> ';
461
+ tmpHtml += '<button class="meadow-mapping-btn meadow-mapping-btn-danger meadow-mapping-btn-small" onclick="window._Pict.views[\'' + tmpViewID + '\'].deleteMapping(' + tmpMap.IDProjectionMapping + ')">Delete</button>';
462
+ tmpHtml += '</td>';
463
+ tmpHtml += '</tr>';
464
+ }
465
+
466
+ tmpHtml += '</tbody></table>';
467
+ tmpContainer.innerHTML = tmpHtml;
468
+ }
469
+
470
+ newMapping()
471
+ {
472
+ this._SelectedMappingID = 0;
473
+
474
+ let tmpMappingListWrap = document.getElementById('MeadowMap-List-Wrap');
475
+ let tmpMappingDetail = document.getElementById('MeadowMap-Detail');
476
+ if (tmpMappingListWrap) tmpMappingListWrap.style.display = 'none';
477
+ if (tmpMappingDetail) tmpMappingDetail.style.display = '';
478
+
479
+ // Reset fields
480
+ let tmpNameInput = document.getElementById('MeadowMap-Name');
481
+ if (tmpNameInput) tmpNameInput.value = '';
482
+
483
+ // Populate source dropdown -- auto-select the first source if one exists.
484
+ // _DiscoveredFields for that source is already populated from the
485
+ // source Columns loaded in editMappings(), so _rebuildFlowNodes
486
+ // will immediately show the source fields on the SRC node.
487
+ let tmpAutoSourceID = (this._MappingSources.length > 0) ? this._MappingSources[0].IDSource : undefined;
488
+ this._populateSourceDropdown(tmpAutoSourceID);
489
+ this._populateStoreChecklist();
490
+
491
+ // Clear JSON editor
492
+ let tmpJSONTextarea = document.getElementById('MeadowMap-JSON');
493
+ if (tmpJSONTextarea)
494
+ {
495
+ let tmpNewEntityName = (this._EditingName || 'Record').replace(/[^a-zA-Z0-9_]/g, '');
496
+ let tmpNewGUIDCol = 'GUID' + tmpNewEntityName;
497
+ let tmpNewIDCol = 'ID' + tmpNewEntityName;
498
+ let tmpNewMappings = {};
499
+ tmpNewMappings[tmpNewGUIDCol] = '{~D:Record.IDRecord~}';
500
+ tmpNewMappings[tmpNewIDCol] = '{~D:Record.IDRecord~}';
501
+
502
+ tmpJSONTextarea.value = JSON.stringify(
503
+ {
504
+ Entity: tmpNewEntityName,
505
+ GUIDTemplate: '{~D:Record.IDRecord~}',
506
+ GUIDName: tmpNewGUIDCol,
507
+ Mappings: tmpNewMappings,
508
+ Solvers: [],
509
+ ManyfestAddresses: false
510
+ }, null, '\t');
511
+ }
512
+
513
+ // Clear flow container
514
+ let tmpFlowContainer = document.getElementById('MeadowMap-Flow-Container');
515
+ if (tmpFlowContainer) tmpFlowContainer.innerHTML = '';
516
+ this._FlowView = null;
517
+
518
+ // Switch to flow mode and initialize the flow editor
519
+ this.switchMapMode('flow');
520
+ this.initFlowView();
521
+
522
+ // Fetch fresh schema then build TGT node ports from schema columns
523
+ this._doLoadTargetSchema(this._EditingContextID).then(
524
+ (pSchema) =>
525
+ {
526
+ if (pSchema && pSchema.SchemaDefinition)
527
+ {
528
+ this._CurrentTargetSchema = pSchema.SchemaDefinition;
529
+ }
530
+ this._rebuildFlowNodes();
531
+ });
532
+ }
533
+
534
+ openMappingDetail(pMappingID)
535
+ {
536
+ this._SelectedMappingID = pMappingID;
537
+
538
+ this._doLoadMapping(pMappingID).then(
539
+ (pResponse) =>
540
+ {
541
+ if (!pResponse || !pResponse.Mapping)
542
+ {
543
+ this._doToast('Mapping not found.', 'error');
544
+ return;
545
+ }
546
+
547
+ let tmpMapping = pResponse.Mapping;
548
+
549
+ let tmpMappingListWrap = document.getElementById('MeadowMap-List-Wrap');
550
+ let tmpMappingDetail = document.getElementById('MeadowMap-Detail');
551
+ if (tmpMappingListWrap) tmpMappingListWrap.style.display = 'none';
552
+ if (tmpMappingDetail) tmpMappingDetail.style.display = '';
553
+
554
+ // Set name
555
+ let tmpNameInput = document.getElementById('MeadowMap-Name');
556
+ if (tmpNameInput) tmpNameInput.value = tmpMapping.Name || '';
557
+
558
+ // Populate dropdowns
559
+ this._populateSourceDropdown(tmpMapping.IDSource);
560
+
561
+ // Parse TargetStores from config, fall back to legacy IDProjectionStore
562
+ let tmpTargetStores = null;
563
+ try
564
+ {
565
+ let tmpParsedConfig = JSON.parse(tmpMapping.MappingConfiguration || '{}');
566
+ if (Array.isArray(tmpParsedConfig.TargetStores) && tmpParsedConfig.TargetStores.length > 0)
567
+ {
568
+ tmpTargetStores = tmpParsedConfig.TargetStores;
569
+ }
570
+ }
571
+ catch (e) { /* ignore */ }
572
+ if (!tmpTargetStores && tmpMapping.IDProjectionStore)
573
+ {
574
+ tmpTargetStores = [tmpMapping.IDProjectionStore];
575
+ }
576
+ this._populateStoreChecklist(tmpTargetStores);
577
+
578
+ // Parse mapping config
579
+ let tmpConfig = {};
580
+ try { tmpConfig = JSON.parse(tmpMapping.MappingConfiguration || '{}'); }
581
+ catch (e) { /* ignore */ }
582
+
583
+ // Restore discovered source fields from config (config-driven approach).
584
+ // sourceColumns is written by saveMapping() so the SRC node shows
585
+ // all fields immediately without an extra API call.
586
+ if (Array.isArray(tmpConfig.sourceColumns) && tmpConfig.sourceColumns.length > 0)
587
+ {
588
+ this._DiscoveredFields[tmpMapping.IDSource] = tmpConfig.sourceColumns;
589
+ }
590
+
591
+ // Set JSON editor
592
+ let tmpJSONTextarea = document.getElementById('MeadowMap-JSON');
593
+ if (tmpJSONTextarea)
594
+ {
595
+ tmpJSONTextarea.value = JSON.stringify(tmpConfig, null, '\t');
596
+ }
597
+
598
+ // Clear flow container and re-initialize
599
+ let tmpFlowContainer = document.getElementById('MeadowMap-Flow-Container');
600
+ if (tmpFlowContainer) tmpFlowContainer.innerHTML = '';
601
+ this._FlowView = null;
602
+
603
+ // Switch to flow mode and initialize the flow editor
604
+ this.switchMapMode('flow');
605
+ this.initFlowView();
606
+
607
+ // Fetch fresh schema then build TGT node ports from schema columns
608
+ this._doLoadTargetSchema(this._EditingContextID).then(
609
+ (pSchema) =>
610
+ {
611
+ if (pSchema && pSchema.SchemaDefinition)
612
+ {
613
+ this._CurrentTargetSchema = pSchema.SchemaDefinition;
614
+ }
615
+ // Restore saved flow diagram state if available,
616
+ // then rebuild ports from current schema (schema is
617
+ // the source of truth for ports, not saved state).
618
+ if (this._FlowView)
619
+ {
620
+ let tmpFlowState = null;
621
+ try { tmpFlowState = JSON.parse(tmpMapping.FlowDiagramState || 'null'); }
622
+ catch (pParseError) { /* ignore invalid JSON */ }
623
+
624
+ if (tmpFlowState && tmpFlowState.Nodes && tmpFlowState.Nodes.length > 0)
625
+ {
626
+ if (typeof this._FlowView.setFlowData === 'function')
627
+ {
628
+ this._FlowView.setFlowData(tmpFlowState);
629
+ }
630
+ }
631
+ }
632
+ // Always rebuild SRC/TGT ports from current schema
633
+ // after restoring positions and connections
634
+ this._rebuildFlowNodes();
635
+ });
636
+ });
637
+ }
638
+
639
+ async deleteMapping(pMappingID)
640
+ {
641
+ let tmpConfirmed = await this._doConfirm('Delete this mapping?', { title: 'Delete Mapping', confirmLabel: 'Delete', dangerous: true });
642
+ if (!tmpConfirmed) return;
643
+
644
+ this._doDeleteMapping(pMappingID).then(
645
+ () =>
646
+ {
647
+ this._doLoadMappings(this._EditingContextID).then(
648
+ (pResult) =>
649
+ {
650
+ this._CurrentMappings = (pResult && pResult.Mappings) ? pResult.Mappings : [];
651
+ this.refreshMappingList();
652
+ });
653
+ });
654
+ }
655
+
656
+ switchMapMode(pMode)
657
+ {
658
+ this._MapEditorMode = pMode;
659
+
660
+ let tmpFlowWrap = document.getElementById('MeadowMap-Flow-Wrap');
661
+ let tmpJSONWrap = document.getElementById('MeadowMap-JSON-Wrap');
662
+ let tmpFlowTab = document.getElementById('MeadowMap-Mode-Flow');
663
+ let tmpJSONTab = document.getElementById('MeadowMap-Mode-JSON');
664
+
665
+ if (pMode === 'flow')
666
+ {
667
+ if (tmpFlowWrap) tmpFlowWrap.style.display = '';
668
+ if (tmpJSONWrap) tmpJSONWrap.style.display = 'none';
669
+ if (tmpFlowTab) tmpFlowTab.classList.add('active');
670
+ if (tmpJSONTab) tmpJSONTab.classList.remove('active');
671
+ }
672
+ else
673
+ {
674
+ if (tmpFlowWrap) tmpFlowWrap.style.display = 'none';
675
+ if (tmpJSONWrap) tmpJSONWrap.style.display = '';
676
+ if (tmpFlowTab) tmpFlowTab.classList.remove('active');
677
+ if (tmpJSONTab) tmpJSONTab.classList.add('active');
678
+
679
+ // If there's a flow view, serialize flow -> JSON
680
+ if (this._FlowView && typeof this._FlowView.getFlowData === 'function')
681
+ {
682
+ let tmpConfig = this.flowToMappingConfig();
683
+ let tmpJSONTextarea = document.getElementById('MeadowMap-JSON');
684
+ if (tmpJSONTextarea)
685
+ {
686
+ tmpJSONTextarea.value = JSON.stringify(tmpConfig, null, '\t');
687
+ }
688
+ }
689
+ }
690
+ }
691
+
692
+ _populateSourceDropdown(pSelectedIDSource)
693
+ {
694
+ let tmpSelect = document.getElementById('MeadowMap-Source');
695
+ if (!tmpSelect) return;
696
+
697
+ let tmpHtml = '<option value="0">Select a source...</option>';
698
+ for (let i = 0; i < this._MappingSources.length; i++)
699
+ {
700
+ let tmpSrc = this._MappingSources[i];
701
+ let tmpSelected = (tmpSrc.IDSource === pSelectedIDSource) ? ' selected' : '';
702
+ tmpHtml += '<option value="' + tmpSrc.IDSource + '"' + tmpSelected + '>' + (tmpSrc.Name || 'Source ' + tmpSrc.IDSource) + '</option>';
703
+ }
704
+ tmpSelect.innerHTML = tmpHtml;
705
+ }
706
+
707
+ _populateStoreChecklist(pSelectedStoreIDs)
708
+ {
709
+ let tmpContainer = document.getElementById('MeadowMap-Stores');
710
+ if (!tmpContainer) return;
711
+
712
+ let tmpSelectedSet = {};
713
+ if (Array.isArray(pSelectedStoreIDs))
714
+ {
715
+ for (let i = 0; i < pSelectedStoreIDs.length; i++)
716
+ {
717
+ tmpSelectedSet[pSelectedStoreIDs[i]] = true;
718
+ }
719
+ }
720
+ else if (pSelectedStoreIDs)
721
+ {
722
+ // Backwards compat: single IDProjectionStore value
723
+ tmpSelectedSet[pSelectedStoreIDs] = true;
724
+ }
725
+
726
+ if (this._MappingStores.length === 0)
727
+ {
728
+ tmpContainer.innerHTML = '<div style="font-size:0.82em; color:var(--facto-text-tertiary, #a09070);">No stores configured yet.</div>';
729
+ return;
730
+ }
731
+
732
+ let tmpHtml = '';
733
+ for (let i = 0; i < this._MappingStores.length; i++)
734
+ {
735
+ let tmpStore = this._MappingStores[i];
736
+ let tmpChecked = tmpSelectedSet[tmpStore.IDProjectionStore] ? ' checked' : '';
737
+ let tmpLabel = (tmpStore.TargetTableName || 'Store ' + tmpStore.IDProjectionStore) + ' (' + (tmpStore.Status || 'Unknown') + ')';
738
+ tmpHtml += '<label>';
739
+ tmpHtml += '<input type="checkbox" value="' + tmpStore.IDProjectionStore + '"' + tmpChecked + '>';
740
+ tmpHtml += ' ' + tmpLabel;
741
+ tmpHtml += '</label>';
742
+ }
743
+ tmpContainer.innerHTML = tmpHtml;
744
+ }
745
+
746
+ _getCheckedStoreIDs()
747
+ {
748
+ let tmpContainer = document.getElementById('MeadowMap-Stores');
749
+ if (!tmpContainer) return [];
750
+
751
+ let tmpChecked = tmpContainer.querySelectorAll('input[type="checkbox"]:checked');
752
+ let tmpIDs = [];
753
+ for (let i = 0; i < tmpChecked.length; i++)
754
+ {
755
+ tmpIDs.push(parseInt(tmpChecked[i].value, 10));
756
+ }
757
+ return tmpIDs;
758
+ }
759
+
760
+ discoverSourceFields()
761
+ {
762
+ let tmpSourceSelect = document.getElementById('MeadowMap-Source');
763
+ let tmpIDSource = tmpSourceSelect ? parseInt(tmpSourceSelect.value, 10) : 0;
764
+
765
+ if (!tmpIDSource)
766
+ {
767
+ this._doToast('Select a source first.', {type: 'warning'});
768
+ return;
769
+ }
770
+
771
+ this._doDiscoverSourceFields(this._EditingContextID, tmpIDSource, 50).then(
772
+ (pResponse) =>
773
+ {
774
+ if (pResponse && pResponse.Error)
775
+ {
776
+ this._doToast('Error: ' + pResponse.Error, {type: 'error'});
777
+ return;
778
+ }
779
+
780
+ let tmpHeaders = (pResponse && pResponse.Headers) ? pResponse.Headers : [];
781
+ this._DiscoveredFields[tmpIDSource] = tmpHeaders;
782
+
783
+ this._doToast('Discovered ' + tmpHeaders.length + ' fields from ' + (pResponse.SampleSize || 0) + ' records: ' + tmpHeaders.join(', '), {type: 'success', duration: 6000});
784
+
785
+ // Rebuild the flow if it exists
786
+ this._rebuildFlowNodes();
787
+ });
788
+ }
789
+
790
+ _rebuildFlowNodes()
791
+ {
792
+ // Get current source and schema columns
793
+ let tmpSourceSelect = document.getElementById('MeadowMap-Source');
794
+ let tmpIDSource = tmpSourceSelect ? parseInt(tmpSourceSelect.value, 10) : 0;
795
+ let tmpFields = this._DiscoveredFields[tmpIDSource] || [];
796
+
797
+ // Get schema columns from the target
798
+ let tmpSchemaColumns = this._getSchemaColumns();
799
+
800
+ // Initialize the flow view if needed
801
+ this.initFlowView();
802
+
803
+ if (!this._FlowView) return;
804
+
805
+ let tmpSourceTitle = 'Source: ' + (tmpSourceSelect && tmpSourceSelect.selectedIndex >= 0 ? tmpSourceSelect.options[tmpSourceSelect.selectedIndex].text : 'Source');
806
+ let tmpTargetTitle = 'Target: ' + (this._EditingName || 'Target');
807
+
808
+ // Build deterministic source ports (Whole Record + discovered fields)
809
+ let tmpSourcePorts =
810
+ [
811
+ { Hash: 'src-whole-record', Direction: 'output', Side: 'right', Label: 'Whole Record' }
812
+ ];
813
+ for (let i = 0; i < tmpFields.length; i++)
814
+ {
815
+ tmpSourcePorts.push(
816
+ {
817
+ Hash: 'src-field-' + tmpFields[i].replace(/[^a-zA-Z0-9_-]/g, '_'),
818
+ Direction: 'output',
819
+ Side: 'right',
820
+ Label: tmpFields[i]
821
+ });
822
+ }
823
+
824
+ // Build deterministic target ports -- entity-specific GUID and ID are always present
825
+ let tmpEntityName = (this._EditingName || 'Record').replace(/[^a-zA-Z0-9_]/g, '');
826
+ let tmpGUIDColumnName = 'GUID' + tmpEntityName;
827
+ let tmpIDColumnName = 'ID' + tmpEntityName;
828
+
829
+ let tmpTargetPorts =
830
+ [
831
+ { Hash: 'tgt-col-' + tmpGUIDColumnName, Direction: 'input', Side: 'left', Label: tmpGUIDColumnName },
832
+ { Hash: 'tgt-col-' + tmpIDColumnName, Direction: 'input', Side: 'left', Label: tmpIDColumnName }
833
+ ];
834
+ for (let i = 0; i < tmpSchemaColumns.length; i++)
835
+ {
836
+ // Skip entity GUID/ID if they appear in schema columns (already added above)
837
+ if (tmpSchemaColumns[i] === tmpGUIDColumnName || tmpSchemaColumns[i] === tmpIDColumnName) continue;
838
+
839
+ tmpTargetPorts.push(
840
+ {
841
+ Hash: 'tgt-col-' + tmpSchemaColumns[i].replace(/[^a-zA-Z0-9_-]/g, '_'),
842
+ Direction: 'input',
843
+ Side: 'left',
844
+ Label: tmpSchemaColumns[i]
845
+ });
846
+ }
847
+
848
+ // Find existing SRC and TGT nodes (preserve user-added TPL/SOL nodes)
849
+ let tmpFlowData = this._FlowView.getFlowData();
850
+ let tmpSrcNode = null;
851
+ let tmpTgtNode = null;
852
+
853
+ for (let i = 0; i < tmpFlowData.Nodes.length; i++)
854
+ {
855
+ if (tmpFlowData.Nodes[i].Type === 'SRC') tmpSrcNode = tmpFlowData.Nodes[i];
856
+ if (tmpFlowData.Nodes[i].Type === 'TGT') tmpTgtNode = tmpFlowData.Nodes[i];
857
+ }
858
+
859
+ if (tmpSrcNode)
860
+ {
861
+ // Merge source ports: start with newly built ports, then preserve
862
+ // any existing ports from the saved state (e.g. previously discovered
863
+ // fields) that aren't already in the new set.
864
+ let tmpMergedSrcPorts = tmpSourcePorts.slice();
865
+ let tmpSrcPortHashes = {};
866
+ for (let p = 0; p < tmpMergedSrcPorts.length; p++)
867
+ {
868
+ tmpSrcPortHashes[tmpMergedSrcPorts[p].Hash] = true;
869
+ }
870
+ let tmpExistingPorts = tmpSrcNode.Ports || [];
871
+ for (let p = 0; p < tmpExistingPorts.length; p++)
872
+ {
873
+ if (!tmpSrcPortHashes[tmpExistingPorts[p].Hash])
874
+ {
875
+ tmpMergedSrcPorts.push(tmpExistingPorts[p]);
876
+ }
877
+ }
878
+
879
+ // Update existing source node in-place
880
+ let tmpInternalNodes = this._FlowView._FlowData.Nodes;
881
+ for (let i = 0; i < tmpInternalNodes.length; i++)
882
+ {
883
+ if (tmpInternalNodes[i].Hash === tmpSrcNode.Hash)
884
+ {
885
+ tmpInternalNodes[i].Ports = tmpMergedSrcPorts;
886
+ tmpInternalNodes[i].Title = tmpSourceTitle;
887
+ break;
888
+ }
889
+ }
890
+ }
891
+ else
892
+ {
893
+ // Push directly into _FlowData.Nodes to avoid addNode() rendering with empty ports
894
+ this._FlowView._FlowData.Nodes.push(
895
+ {
896
+ Hash: 'node-src-' + this.fable.getUUID(),
897
+ Type: 'SRC',
898
+ X: 50,
899
+ Y: 50,
900
+ Width: 200,
901
+ Height: 100,
902
+ Title: tmpSourceTitle,
903
+ Ports: tmpSourcePorts,
904
+ Data: {}
905
+ });
906
+ }
907
+
908
+ if (tmpTgtNode)
909
+ {
910
+ // Target ports: schema is the source of truth. Start with schema-
911
+ // derived ports, then preserve any extra existing ports (e.g. user-
912
+ // added custom columns) that aren't already in the new set.
913
+ let tmpMergedTgtPorts = tmpTargetPorts.slice();
914
+ let tmpTgtPortHashes = {};
915
+ for (let p = 0; p < tmpMergedTgtPorts.length; p++)
916
+ {
917
+ tmpTgtPortHashes[tmpMergedTgtPorts[p].Hash] = true;
918
+ }
919
+ let tmpExistingTgtPorts = tmpTgtNode.Ports || [];
920
+ for (let p = 0; p < tmpExistingTgtPorts.length; p++)
921
+ {
922
+ if (!tmpTgtPortHashes[tmpExistingTgtPorts[p].Hash])
923
+ {
924
+ tmpMergedTgtPorts.push(tmpExistingTgtPorts[p]);
925
+ }
926
+ }
927
+
928
+ // Update existing target node in-place
929
+ let tmpInternalNodes = this._FlowView._FlowData.Nodes;
930
+ for (let i = 0; i < tmpInternalNodes.length; i++)
931
+ {
932
+ if (tmpInternalNodes[i].Hash === tmpTgtNode.Hash)
933
+ {
934
+ tmpInternalNodes[i].Ports = tmpMergedTgtPorts;
935
+ tmpInternalNodes[i].Title = tmpTargetTitle;
936
+ break;
937
+ }
938
+ }
939
+ }
940
+ else
941
+ {
942
+ // Push directly into _FlowData.Nodes to avoid addNode() rendering with empty ports
943
+ this._FlowView._FlowData.Nodes.push(
944
+ {
945
+ Hash: 'node-tgt-' + this.fable.getUUID(),
946
+ Type: 'TGT',
947
+ X: 550,
948
+ Y: 50,
949
+ Width: 200,
950
+ Height: 100,
951
+ Title: tmpTargetTitle,
952
+ Ports: tmpTargetPorts,
953
+ Data: {}
954
+ });
955
+ }
956
+
957
+ // Render the flow once with all ports correctly set
958
+ if (typeof this._FlowView.renderFlow === 'function')
959
+ {
960
+ this._FlowView.renderFlow();
961
+ }
962
+ else if (typeof this._FlowView.render === 'function')
963
+ {
964
+ this._FlowView.render();
965
+ }
966
+ }
967
+
968
+ _getSchemaColumns()
969
+ {
970
+ // Use the locally cached schema definition
971
+ let tmpColumns = [];
972
+ let tmpDDL = this._CurrentTargetSchema || '';
973
+ if (tmpDDL)
974
+ {
975
+ let tmpParsedColumns = libSchemaUtils.microDDLToColumns(tmpDDL);
976
+ for (let j = 0; j < tmpParsedColumns.length; j++)
977
+ {
978
+ tmpColumns.push(tmpParsedColumns[j].Name);
979
+ }
980
+ }
981
+ return tmpColumns;
982
+ }
983
+
984
+ initFlowView()
985
+ {
986
+ if (this._FlowView) return;
987
+
988
+ let tmpFlowContainer = document.getElementById('MeadowMap-Flow-Container');
989
+ if (!tmpFlowContainer) return;
990
+
991
+ try
992
+ {
993
+ let libPictSectionFlow = require('pict-section-flow');
994
+
995
+ this._FlowView = this.pict.addView('MeadowMapping-Flow',
996
+ {
997
+ ViewIdentifier: 'MeadowMapping-Flow',
998
+ DefaultDestinationAddress: '#MeadowMap-Flow-Container',
999
+ EnableToolbar: true,
1000
+ EnablePanning: true,
1001
+ EnableZooming: true,
1002
+ EnableNodeDragging: true,
1003
+ EnableConnectionCreation: true
1004
+ }, libPictSectionFlow);
1005
+
1006
+ // Register card types
1007
+ let libFlowCardSource = require('./flow-cards/FlowCard-MappingSource.js');
1008
+ let libFlowCardTarget = require('./flow-cards/FlowCard-MappingTarget.js');
1009
+ let libFlowCardTemplate = require('./flow-cards/FlowCard-TemplateExpression.js');
1010
+ let libFlowCardSolver = require('./flow-cards/FlowCard-SolverExpression.js');
1011
+
1012
+ this.pict.addServiceType('FlowCardMappingSource', libFlowCardSource);
1013
+ this.pict.addServiceType('FlowCardMappingTarget', libFlowCardTarget);
1014
+ this.pict.addServiceType('FlowCardTemplateExpression', libFlowCardTemplate);
1015
+ this.pict.addServiceType('FlowCardSolverExpression', libFlowCardSolver);
1016
+
1017
+ // Render the flow view first so _NodeTypeProvider is initialized
1018
+ if (typeof this._FlowView.render === 'function')
1019
+ {
1020
+ this._FlowView.render();
1021
+ }
1022
+
1023
+ // Register card types with the flow view (must happen after render
1024
+ // so _NodeTypeProvider exists)
1025
+ let tmpSourceCard = this.pict.instantiateServiceProviderWithoutRegistration('FlowCardMappingSource', {});
1026
+ let tmpTargetCard = this.pict.instantiateServiceProviderWithoutRegistration('FlowCardMappingTarget', {});
1027
+ let tmpTemplateCard = this.pict.instantiateServiceProviderWithoutRegistration('FlowCardTemplateExpression', {});
1028
+ let tmpSolverCard = this.pict.instantiateServiceProviderWithoutRegistration('FlowCardSolverExpression', {});
1029
+
1030
+ tmpSourceCard.registerWithFlowView(this._FlowView);
1031
+ tmpTargetCard.registerWithFlowView(this._FlowView);
1032
+ tmpTemplateCard.registerWithFlowView(this._FlowView);
1033
+ tmpSolverCard.registerWithFlowView(this._FlowView);
1034
+ }
1035
+ catch (pFlowError)
1036
+ {
1037
+ this.log.error('Failed to initialize flow view: ' + pFlowError.message);
1038
+ tmpFlowContainer.innerHTML = '<div style="padding:2em; text-align:center; color:var(--facto-text-tertiary, #a09070);">Flow editor could not be loaded. Use JSON Config mode instead.</div>';
1039
+ }
1040
+ }
1041
+
1042
+ flowToMappingConfig()
1043
+ {
1044
+ let tmpEntityName = (this._EditingName || 'Record').replace(/[^a-zA-Z0-9_]/g, '');
1045
+ let tmpGUIDColumnName = 'GUID' + tmpEntityName;
1046
+ let tmpIDColumnName = 'ID' + tmpEntityName;
1047
+
1048
+ let tmpConfig =
1049
+ {
1050
+ Entity: tmpEntityName,
1051
+ GUIDTemplate: '{~D:Record.IDRecord~}',
1052
+ GUIDName: tmpGUIDColumnName,
1053
+ Mappings: {},
1054
+ Solvers: [],
1055
+ ManyfestAddresses: false
1056
+ };
1057
+
1058
+ if (!this._FlowView || typeof this._FlowView.getFlowData !== 'function')
1059
+ {
1060
+ return tmpConfig;
1061
+ }
1062
+
1063
+ let tmpFlowData = this._FlowView.getFlowData();
1064
+ if (!tmpFlowData || !tmpFlowData.Connections) return tmpConfig;
1065
+
1066
+ // Build node hash->node map and port hash->{Label, NodeHash, NodeType} map
1067
+ let tmpNodeMap = {};
1068
+ let tmpPortMap = {};
1069
+
1070
+ if (tmpFlowData.Nodes)
1071
+ {
1072
+ for (let i = 0; i < tmpFlowData.Nodes.length; i++)
1073
+ {
1074
+ let tmpNode = tmpFlowData.Nodes[i];
1075
+ tmpNodeMap[tmpNode.Hash] = tmpNode;
1076
+
1077
+ if (tmpNode.Ports)
1078
+ {
1079
+ for (let j = 0; j < tmpNode.Ports.length; j++)
1080
+ {
1081
+ tmpPortMap[tmpNode.Ports[j].Hash] =
1082
+ {
1083
+ Label: tmpNode.Ports[j].Label,
1084
+ NodeHash: tmpNode.Hash,
1085
+ NodeType: tmpNode.Type
1086
+ };
1087
+ }
1088
+ }
1089
+ }
1090
+ }
1091
+
1092
+ // Track solver nodes that connect to multiple target columns
1093
+ let tmpSolverEntries = {};
1094
+
1095
+ // Process each connection where the target is a TGT node
1096
+ for (let i = 0; i < tmpFlowData.Connections.length; i++)
1097
+ {
1098
+ let tmpConn = tmpFlowData.Connections[i];
1099
+ let tmpSourcePort = tmpPortMap[tmpConn.SourcePortHash];
1100
+ let tmpTargetPort = tmpPortMap[tmpConn.TargetPortHash];
1101
+
1102
+ if (!tmpSourcePort || !tmpTargetPort) continue;
1103
+
1104
+ // Only process connections that end at a TGT node
1105
+ if (tmpTargetPort.NodeType !== 'TGT') continue;
1106
+
1107
+ let tmpTargetColumn = tmpTargetPort.Label;
1108
+ if (!tmpTargetColumn) continue;
1109
+
1110
+ let tmpSourceNode = tmpNodeMap[tmpSourcePort.NodeHash];
1111
+ if (!tmpSourceNode) continue;
1112
+
1113
+ if (tmpSourceNode.Type === 'SRC')
1114
+ {
1115
+ // Direct mapping: SRC field -> TGT column
1116
+ let tmpSourceField = tmpSourcePort.Label;
1117
+
1118
+ // Skip "Whole Record" direct connections to TGT (need intermediate node)
1119
+ if (tmpSourceField === 'Whole Record') continue;
1120
+
1121
+ let tmpTemplate = (tmpConn.Data && tmpConn.Data.Template)
1122
+ ? tmpConn.Data.Template
1123
+ : '{~D:Record.' + tmpSourceField + '~}';
1124
+
1125
+ // Connection to the entity GUID port sets the GUIDTemplate for upsert uniqueness
1126
+ if (tmpTargetColumn === tmpGUIDColumnName)
1127
+ {
1128
+ tmpConfig.GUIDTemplate = tmpTemplate;
1129
+ }
1130
+
1131
+ tmpConfig.Mappings[tmpTargetColumn] = tmpTemplate;
1132
+ }
1133
+ else if (tmpSourceNode.Type === 'TPL')
1134
+ {
1135
+ // Template expression: TPL result -> TGT column
1136
+ let tmpExpression = (tmpSourceNode.Data && tmpSourceNode.Data.TemplateExpression)
1137
+ ? tmpSourceNode.Data.TemplateExpression
1138
+ : '';
1139
+
1140
+ if (tmpExpression)
1141
+ {
1142
+ // TPL connected to entity GUID sets the GUIDTemplate
1143
+ if (tmpTargetColumn === tmpGUIDColumnName)
1144
+ {
1145
+ tmpConfig.GUIDTemplate = tmpExpression;
1146
+ }
1147
+
1148
+ tmpConfig.Mappings[tmpTargetColumn] = tmpExpression;
1149
+ }
1150
+ }
1151
+ else if (tmpSourceNode.Type === 'SOL')
1152
+ {
1153
+ // Solver expression: SOL result -> TGT column
1154
+ let tmpExpression = (tmpSourceNode.Data && tmpSourceNode.Data.SolverExpression)
1155
+ ? tmpSourceNode.Data.SolverExpression
1156
+ : '';
1157
+
1158
+ if (tmpExpression)
1159
+ {
1160
+ // Group outputs for the same solver node
1161
+ if (!tmpSolverEntries[tmpSourceNode.Hash])
1162
+ {
1163
+ tmpSolverEntries[tmpSourceNode.Hash] =
1164
+ {
1165
+ expression: tmpExpression,
1166
+ outputs: {}
1167
+ };
1168
+ }
1169
+ tmpSolverEntries[tmpSourceNode.Hash].outputs[tmpTargetColumn] = true;
1170
+ }
1171
+ }
1172
+ }
1173
+
1174
+ // Ensure entity-specific GUID and ID are always present in Mappings
1175
+ if (!tmpConfig.Mappings.hasOwnProperty(tmpGUIDColumnName))
1176
+ {
1177
+ tmpConfig.Mappings[tmpGUIDColumnName] = tmpConfig.GUIDTemplate;
1178
+ }
1179
+ if (!tmpConfig.Mappings.hasOwnProperty(tmpIDColumnName))
1180
+ {
1181
+ tmpConfig.Mappings[tmpIDColumnName] = '{~D:Record.IDRecord~}';
1182
+ }
1183
+
1184
+ // Add grouped solver entries
1185
+ let tmpSolverKeys = Object.keys(tmpSolverEntries);
1186
+ for (let i = 0; i < tmpSolverKeys.length; i++)
1187
+ {
1188
+ tmpConfig.Solvers.push(tmpSolverEntries[tmpSolverKeys[i]]);
1189
+ }
1190
+
1191
+ return tmpConfig;
1192
+ }
1193
+
1194
+ saveMapping()
1195
+ {
1196
+ let tmpNameInput = document.getElementById('MeadowMap-Name');
1197
+ let tmpSourceSelect = document.getElementById('MeadowMap-Source');
1198
+
1199
+ let tmpName = tmpNameInput ? tmpNameInput.value.trim() : '';
1200
+ let tmpIDSource = tmpSourceSelect ? parseInt(tmpSourceSelect.value, 10) : 0;
1201
+ let tmpCheckedStoreIDs = this._getCheckedStoreIDs();
1202
+ let tmpIDProjectionStore = tmpCheckedStoreIDs.length > 0 ? tmpCheckedStoreIDs[0] : 0;
1203
+
1204
+ if (!tmpName)
1205
+ {
1206
+ this._doToast('Enter a mapping name.', {type: 'warning'});
1207
+ return;
1208
+ }
1209
+
1210
+ // Get mapping config
1211
+ let tmpMappingConfig;
1212
+ if (this._MapEditorMode === 'json')
1213
+ {
1214
+ let tmpJSONTextarea = document.getElementById('MeadowMap-JSON');
1215
+ let tmpJSON = tmpJSONTextarea ? tmpJSONTextarea.value : '{}';
1216
+ try
1217
+ {
1218
+ tmpMappingConfig = JSON.parse(tmpJSON);
1219
+ }
1220
+ catch (e)
1221
+ {
1222
+ this._doToast('Invalid JSON: ' + e.message, {type: 'error'});
1223
+ return;
1224
+ }
1225
+ }
1226
+ else
1227
+ {
1228
+ tmpMappingConfig = this.flowToMappingConfig();
1229
+ }
1230
+
1231
+ // Store target stores in the mapping config
1232
+ tmpMappingConfig.TargetStores = tmpCheckedStoreIDs;
1233
+
1234
+ // Persist discovered source columns so the SRC node loads correctly
1235
+ // on next open without requiring a separate API call.
1236
+ let tmpSavedColumns = this._DiscoveredFields[tmpIDSource];
1237
+ if (Array.isArray(tmpSavedColumns) && tmpSavedColumns.length > 0)
1238
+ {
1239
+ tmpMappingConfig.sourceColumns = tmpSavedColumns;
1240
+ }
1241
+
1242
+ // Get flow diagram state
1243
+ let tmpFlowState = {};
1244
+ if (this._FlowView && typeof this._FlowView.getFlowData === 'function')
1245
+ {
1246
+ tmpFlowState = this._FlowView.getFlowData();
1247
+ }
1248
+
1249
+ let tmpData =
1250
+ {
1251
+ Name: tmpName,
1252
+ IDSource: tmpIDSource,
1253
+ IDProjectionStore: tmpIDProjectionStore,
1254
+ MappingConfiguration: JSON.stringify(tmpMappingConfig),
1255
+ FlowDiagramState: JSON.stringify(tmpFlowState),
1256
+ Active: 1
1257
+ };
1258
+
1259
+ let tmpPromise;
1260
+ if (this._SelectedMappingID)
1261
+ {
1262
+ tmpPromise = this._doUpdateMapping(this._SelectedMappingID, tmpData);
1263
+ }
1264
+ else
1265
+ {
1266
+ tmpPromise = this._doCreateMapping(this._EditingContextID, tmpData);
1267
+ }
1268
+
1269
+ tmpPromise.then(
1270
+ (pResponse) =>
1271
+ {
1272
+ if (pResponse && pResponse.Error)
1273
+ {
1274
+ this._doToast('Error: ' + pResponse.Error, {type: 'error'});
1275
+ return;
1276
+ }
1277
+
1278
+ // Update the selected mapping ID if it was a create
1279
+ if (pResponse && pResponse.Mapping && pResponse.Mapping.IDProjectionMapping)
1280
+ {
1281
+ this._SelectedMappingID = pResponse.Mapping.IDProjectionMapping;
1282
+ }
1283
+
1284
+ this._doToast('Mapping saved.', {type: 'success'});
1285
+
1286
+ // Refresh mapping list
1287
+ this._doLoadMappings(this._EditingContextID).then(
1288
+ (pResult) =>
1289
+ {
1290
+ this._CurrentMappings = (pResult && pResult.Mappings) ? pResult.Mappings : [];
1291
+ });
1292
+ });
1293
+ }
1294
+
1295
+ }
1296
+
1297
+ module.exports = MeadowMappingEditorView;
1298
+
1299
+ module.exports.default_configuration = _ViewConfiguration;