meadow-integration 1.0.19 → 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.
- package/example-applications/mapping-demo/.quackage.json +10 -0
- package/example-applications/mapping-demo/README.md +99 -0
- package/example-applications/mapping-demo/data/books-sample.csv +21 -0
- package/example-applications/mapping-demo/generate-build-config.js +44 -0
- package/example-applications/mapping-demo/mappings/books-to-book.json +14 -0
- package/example-applications/mapping-demo/package.json +14 -0
- package/example-applications/mapping-demo/server.js +814 -0
- package/example-applications/mapping-demo/source/MappingDemoApp.js +52 -0
- package/example-applications/mapping-demo/source/views/MappingDemoEditorView.js +186 -0
- package/example-applications/mapping-demo/web/index.html +892 -0
- package/example-applications/mapping-demo/web/mapping-demo-editor.js +3195 -0
- package/example-applications/mapping-demo/web/mapping-demo-editor.js.map +1 -0
- package/example-applications/mapping-demo/web/mapping-demo-editor.min.js +2 -0
- package/example-applications/mapping-demo/web/mapping-demo-editor.min.js.map +1 -0
- package/example-applications/mapping-demo/web/pict.min.js +12 -0
- package/package.json +11 -5
- package/source/Meadow-Integration-Browser.js +31 -0
- package/source/Meadow-Integration.js +30 -1
- package/source/services/certainty/Service-CertaintyAccumulator.js +402 -0
- package/source/services/clone/Meadow-Service-Sync-Entity-Initial.js +16 -3
- package/source/services/clone/Meadow-Service-Sync-Entity-Ongoing.js +15 -2
- package/source/services/clone/Meadow-Service-Sync.js +21 -0
- package/source/services/parser/Service-FileParser-CSV.js +263 -0
- package/source/services/parser/Service-FileParser-FixedWidth.js +158 -0
- package/source/services/parser/Service-FileParser-JSON.js +255 -0
- package/source/services/parser/Service-FileParser-XLSX.js +194 -0
- package/source/services/parser/Service-FileParser-XML.js +190 -0
- package/source/services/parser/Service-FileParser.js +142 -0
- package/source/views/MappingEditor-SchemaUtils.js +71 -0
- package/source/views/PictView-MeadowMappingEditor.js +1299 -0
- package/source/views/flow-cards/FlowCard-MappingSource.js +50 -0
- package/source/views/flow-cards/FlowCard-MappingTarget.js +49 -0
- package/source/views/flow-cards/FlowCard-SolverExpression.js +78 -0
- package/source/views/flow-cards/FlowCard-TemplateExpression.js +77 -0
- 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()">← 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;
|