json-object-editor 0.10.633 → 0.10.638
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/CHANGELOG.md +10 -0
- package/_www/matrix.html +126 -0
- package/_www/mcp-nav.js +6 -0
- package/_www/mcp-prompt.html +116 -116
- package/_www/mcp-schemas.html +32 -2
- package/css/joe-styles.css +6 -2
- package/css/joe.css +6 -3
- package/css/joe.min.css +1 -1
- package/docs/JOE_AI_Overview.md +330 -0
- package/favicon-dev.png +0 -0
- package/js/JsonObjectEditor.jquery.craydent.js +8 -2
- package/js/favicon-env.js +50 -0
- package/js/joe-ai.js +9 -4
- package/js/joe.js +9 -3
- package/js/joe.min.js +1 -1
- package/package.json +1 -1
- package/pages/joe.html +6 -4
- package/pages/template.html +5 -4
- package/pages/template_ie.html +5 -4
- package/readme.md +8 -4
- package/server/app-config.js +2 -0
- package/server/modules/Apps.js +3 -0
- package/server/plugins/chatgpt.js +36 -11
- package/server/schemas/ai_widget_conversation.js +229 -229
- package/server/schemas/form.js +51 -7
- package/server/schemas/location.js +24 -0
- package/server/schemas/member.js +35 -0
- package/server/schemas/question.js +55 -2
- package/server/schemas/submission.js +30 -0
- package/web-components/joe-matrix.css +480 -0
- package/web-components/joe-matrix.js +1065 -0
|
@@ -0,0 +1,1065 @@
|
|
|
1
|
+
class JoeMatrix extends HTMLElement {
|
|
2
|
+
constructor() {
|
|
3
|
+
super();
|
|
4
|
+
this.schemas = {};
|
|
5
|
+
this.nodes = [];
|
|
6
|
+
this.links = [];
|
|
7
|
+
this.simulation = null;
|
|
8
|
+
this.svg = null;
|
|
9
|
+
this.g = null;
|
|
10
|
+
this.zoom = null;
|
|
11
|
+
this.width = 0;
|
|
12
|
+
this.height = 0;
|
|
13
|
+
this.selectedNode = null;
|
|
14
|
+
this.mode = 'schema-to-schema';
|
|
15
|
+
this.transform = { x: 0, y: 0, k: 1 };
|
|
16
|
+
this.appMap = {};
|
|
17
|
+
this.selectedApp = '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static get observedAttributes() {
|
|
21
|
+
return ['mode'];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
connectedCallback() {
|
|
25
|
+
// CSS is loaded externally via link tag in the page
|
|
26
|
+
// Ensure it's loaded if not already
|
|
27
|
+
if (!document.querySelector('link[href*="joe-matrix.css"]')) {
|
|
28
|
+
var link = document.createElement('link');
|
|
29
|
+
link.rel = 'stylesheet';
|
|
30
|
+
link.href = '/JsonObjectEditor/web-components/joe-matrix.css';
|
|
31
|
+
document.head.appendChild(link);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.mode = this.getAttribute('mode') || 'schema-to-schema';
|
|
35
|
+
this.init();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
attributeChangedCallback(attr, oldValue, newValue) {
|
|
39
|
+
if (attr === 'mode' && newValue !== oldValue) {
|
|
40
|
+
this.mode = newValue;
|
|
41
|
+
if (this.schemas && Object.keys(this.schemas).length > 0) {
|
|
42
|
+
this.updateVisualization();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async init() {
|
|
48
|
+
// Wait for D3 to be available
|
|
49
|
+
if (typeof d3 === 'undefined') {
|
|
50
|
+
setTimeout(function() { this.init(); }.bind(this), 100);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.setupCanvas();
|
|
55
|
+
await this.loadSchemas();
|
|
56
|
+
this.updateVisualization();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setupCanvas() {
|
|
60
|
+
// Get container dimensions
|
|
61
|
+
var container = this.parentElement || document.body;
|
|
62
|
+
this.width = container.clientWidth || window.innerWidth;
|
|
63
|
+
this.height = container.clientHeight || window.innerHeight;
|
|
64
|
+
|
|
65
|
+
// Create SVG
|
|
66
|
+
this.svg = d3.select(this)
|
|
67
|
+
.append('svg')
|
|
68
|
+
.attr('class', 'matrix-canvas')
|
|
69
|
+
.attr('width', this.width)
|
|
70
|
+
.attr('height', this.height);
|
|
71
|
+
|
|
72
|
+
// Create main group for zoom/pan
|
|
73
|
+
this.g = this.svg.append('g');
|
|
74
|
+
|
|
75
|
+
// Create tooltip
|
|
76
|
+
this.tooltip = d3.select('body')
|
|
77
|
+
.append('div')
|
|
78
|
+
.attr('class', 'matrix-tooltip');
|
|
79
|
+
|
|
80
|
+
// Setup zoom behavior
|
|
81
|
+
var self = this;
|
|
82
|
+
this.zoom = d3.behavior.zoom()
|
|
83
|
+
.scaleExtent([0.1, 4])
|
|
84
|
+
.on('zoom', function() {
|
|
85
|
+
self.transform = d3.event.translate;
|
|
86
|
+
self.g.attr('transform', 'translate(' + d3.event.translate + ') scale(' + d3.event.scale + ')');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
this.svg.call(this.zoom);
|
|
90
|
+
|
|
91
|
+
// Add zoom controls
|
|
92
|
+
this.addZoomControls();
|
|
93
|
+
|
|
94
|
+
// Add legend
|
|
95
|
+
this.addLegend();
|
|
96
|
+
|
|
97
|
+
// Handle window resize
|
|
98
|
+
var self = this;
|
|
99
|
+
window.addEventListener('resize', function() {
|
|
100
|
+
self.width = container.clientWidth || window.innerWidth;
|
|
101
|
+
self.height = container.clientHeight || window.innerHeight;
|
|
102
|
+
self.svg.attr('width', self.width).attr('height', self.height);
|
|
103
|
+
if (self.simulation) {
|
|
104
|
+
self.simulation.size([self.width, self.height]);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
addZoomControls() {
|
|
110
|
+
var controls = d3.select(this)
|
|
111
|
+
.append('div')
|
|
112
|
+
.attr('class', 'matrix-zoom-controls');
|
|
113
|
+
|
|
114
|
+
var self = this;
|
|
115
|
+
controls.append('button')
|
|
116
|
+
.text('+')
|
|
117
|
+
.on('click', function() {
|
|
118
|
+
self.zoom.scale(self.zoom.scale() * 1.5);
|
|
119
|
+
self.svg.transition().call(self.zoom.event);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
controls.append('button')
|
|
123
|
+
.text('−')
|
|
124
|
+
.on('click', function() {
|
|
125
|
+
self.zoom.scale(self.zoom.scale() / 1.5);
|
|
126
|
+
self.svg.transition().call(self.zoom.event);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
controls.append('button')
|
|
130
|
+
.text('⌂')
|
|
131
|
+
.attr('title', 'Reset view')
|
|
132
|
+
.on('click', function() {
|
|
133
|
+
self.resetView();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
addLegend() {
|
|
138
|
+
var legend = d3.select(this)
|
|
139
|
+
.append('div')
|
|
140
|
+
.attr('class', 'matrix-legend');
|
|
141
|
+
|
|
142
|
+
legend.append('h4').text('Legend');
|
|
143
|
+
|
|
144
|
+
var items = [
|
|
145
|
+
{ label: 'Schema Node', color: '#4a90e2', border: '#2c5aa0' },
|
|
146
|
+
{ label: 'Selected Node', color: '#ff8804', border: '#cc6d03' },
|
|
147
|
+
{ label: 'One-to-One', style: 'solid' },
|
|
148
|
+
{ label: 'One-to-Many', style: 'dashed' }
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
items.forEach(item => {
|
|
152
|
+
var itemDiv = legend.append('div').attr('class', 'matrix-legend-item');
|
|
153
|
+
if (item.color) {
|
|
154
|
+
itemDiv.append('div')
|
|
155
|
+
.attr('class', 'legend-color')
|
|
156
|
+
.style('background-color', item.color)
|
|
157
|
+
.style('border-color', item.border);
|
|
158
|
+
} else {
|
|
159
|
+
itemDiv.append('svg')
|
|
160
|
+
.attr('width', 20)
|
|
161
|
+
.attr('height', 20)
|
|
162
|
+
.append('line')
|
|
163
|
+
.attr('x1', 0)
|
|
164
|
+
.attr('y1', 10)
|
|
165
|
+
.attr('x2', 20)
|
|
166
|
+
.attr('y2', 10)
|
|
167
|
+
.style('stroke', '#999')
|
|
168
|
+
.style('stroke-width', '2px')
|
|
169
|
+
.style('stroke-dasharray', item.style === 'dashed' ? '5,5' : 'none');
|
|
170
|
+
}
|
|
171
|
+
itemDiv.append('span').text(item.label);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async loadSchemas() {
|
|
176
|
+
var self = this;
|
|
177
|
+
try {
|
|
178
|
+
// Get list of schemas using fetch (no jQuery dependency)
|
|
179
|
+
var baseUrl = location.origin;
|
|
180
|
+
console.log('[Matrix] Loading schemas from:', baseUrl + '/API/list/schemas');
|
|
181
|
+
var schemaNames = await fetch(baseUrl + '/API/list/schemas')
|
|
182
|
+
.then(function(res) { return res.ok ? res.json() : []; })
|
|
183
|
+
.catch(function() { return []; });
|
|
184
|
+
|
|
185
|
+
console.log('[Matrix] Found', schemaNames.length, 'schemas:', schemaNames);
|
|
186
|
+
|
|
187
|
+
// Load all schemas with summaries, then get menuicon from full schema
|
|
188
|
+
var schemaPromises = schemaNames.map(function(name) {
|
|
189
|
+
return fetch(baseUrl + '/API/schema/' + encodeURIComponent(name) + '?summaryOnly=true')
|
|
190
|
+
.then(function(res) {
|
|
191
|
+
return res.ok ? res.json() : null;
|
|
192
|
+
})
|
|
193
|
+
.then(function(data) {
|
|
194
|
+
if (data && !data.error) {
|
|
195
|
+
// API returns {schemas: {schemaName: summary}, ...}
|
|
196
|
+
// Extract the actual summary from the response
|
|
197
|
+
var summary = (data.schemas && data.schemas[name]) || data[name] || data;
|
|
198
|
+
|
|
199
|
+
if (summary) {
|
|
200
|
+
self.schemas[name] = summary;
|
|
201
|
+
// Debug: log relationship info for first few schemas
|
|
202
|
+
if (schemaNames.indexOf(name) < 3) {
|
|
203
|
+
console.log('[Matrix] Schema:', name);
|
|
204
|
+
console.log(' - Raw API response keys:', Object.keys(data));
|
|
205
|
+
console.log(' - Has summary object:', !!summary);
|
|
206
|
+
console.log(' - Summary keys:', summary ? Object.keys(summary) : 'none');
|
|
207
|
+
console.log(' - Has relationships:', !!(summary && summary.relationships));
|
|
208
|
+
if (summary && summary.relationships) {
|
|
209
|
+
console.log(' - Has outbound:', !!summary.relationships.outbound);
|
|
210
|
+
if (summary.relationships.outbound) {
|
|
211
|
+
console.log(' - Outbound count:', summary.relationships.outbound.length);
|
|
212
|
+
console.log(' - Outbound:', JSON.stringify(summary.relationships.outbound, null, 2));
|
|
213
|
+
} else {
|
|
214
|
+
console.log(' - Outbound is:', summary.relationships.outbound);
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
console.log(' - Relationships object:', summary ? summary.relationships : 'missing');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Load full schema to get menuicon and default_schema (if not in summary)
|
|
222
|
+
if (!summary.menuicon || summary.default_schema === undefined) {
|
|
223
|
+
return fetch(baseUrl + '/API/schema/' + encodeURIComponent(name))
|
|
224
|
+
.then(function(res) {
|
|
225
|
+
return res.ok ? res.json() : null;
|
|
226
|
+
})
|
|
227
|
+
.then(function(fullData) {
|
|
228
|
+
if (fullData && !fullData.error) {
|
|
229
|
+
var fullSchema = (fullData.schemas && fullData.schemas[name]) || fullData[name] || fullData;
|
|
230
|
+
if (fullSchema) {
|
|
231
|
+
if (fullSchema.menuicon && !self.schemas[name].menuicon) {
|
|
232
|
+
self.schemas[name].menuicon = fullSchema.menuicon;
|
|
233
|
+
}
|
|
234
|
+
if (fullSchema.default_schema !== undefined) {
|
|
235
|
+
self.schemas[name].default_schema = fullSchema.default_schema;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
.catch(function() {
|
|
241
|
+
// Ignore errors loading full schema
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
console.warn('[Matrix] No summary found for schema:', name, 'Response:', data);
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
console.warn('[Matrix] Failed to load schema:', name, data);
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
.catch(function(err) {
|
|
252
|
+
console.warn('[Matrix] Error loading schema', name + ':', err);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await Promise.all(schemaPromises);
|
|
257
|
+
|
|
258
|
+
// Load apps via MCP
|
|
259
|
+
try {
|
|
260
|
+
var baseUrl = location.origin;
|
|
261
|
+
var mcpResponse = await fetch(baseUrl + '/mcp', {
|
|
262
|
+
method: 'POST',
|
|
263
|
+
headers: { 'Content-Type': 'application/json' },
|
|
264
|
+
body: JSON.stringify({
|
|
265
|
+
jsonrpc: '2.0',
|
|
266
|
+
id: String(Date.now()),
|
|
267
|
+
method: 'listApps',
|
|
268
|
+
params: {}
|
|
269
|
+
})
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (mcpResponse.ok) {
|
|
273
|
+
var mcpData = await mcpResponse.json();
|
|
274
|
+
var appData = (mcpData && (mcpData.result || mcpData)) || {};
|
|
275
|
+
self.appMap = (appData && appData.apps) || {};
|
|
276
|
+
|
|
277
|
+
// Determine which apps use each schema
|
|
278
|
+
Object.keys(self.schemas).forEach(function(schemaName) {
|
|
279
|
+
var usedBy = [];
|
|
280
|
+
for (var appName in self.appMap) {
|
|
281
|
+
var app = self.appMap[appName] || {};
|
|
282
|
+
var cols = Array.isArray(app.collections) ? app.collections : [];
|
|
283
|
+
if (cols.indexOf(schemaName) !== -1) {
|
|
284
|
+
usedBy.push(appName);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// If schema is a default core schema, show only the JOE app
|
|
288
|
+
var schema = self.schemas[schemaName];
|
|
289
|
+
if (schema && schema.default_schema) {
|
|
290
|
+
usedBy = ['joe'];
|
|
291
|
+
}
|
|
292
|
+
self.schemas[schemaName].apps = usedBy.sort();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Populate app filter dropdown
|
|
296
|
+
self.populateAppFilter();
|
|
297
|
+
}
|
|
298
|
+
} catch (err) {
|
|
299
|
+
console.warn('[Matrix] Error loading apps:', err);
|
|
300
|
+
self.appMap = {};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
console.log('[Matrix] Loaded', Object.keys(self.schemas).length, 'schemas total');
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error('[Matrix] Error loading schemas:', error);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
updateVisualization() {
|
|
310
|
+
if (this.mode === 'schema-to-schema') {
|
|
311
|
+
this.buildSchemaToSchemaGraph();
|
|
312
|
+
} else if (this.mode === 'object-relationships') {
|
|
313
|
+
// TODO: Implement 3b
|
|
314
|
+
console.log('Object relationships mode not yet implemented');
|
|
315
|
+
} else if (this.mode === 'aggregate-relationships') {
|
|
316
|
+
// TODO: Implement 3c
|
|
317
|
+
console.log('Aggregate relationships mode not yet implemented');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
buildSchemaToSchemaGraph() {
|
|
322
|
+
var self = this;
|
|
323
|
+
|
|
324
|
+
// Clear existing visualization
|
|
325
|
+
this.g.selectAll('*').remove();
|
|
326
|
+
this.nodes = [];
|
|
327
|
+
this.links = [];
|
|
328
|
+
|
|
329
|
+
// Build nodes from schemas (filtered by app if selected)
|
|
330
|
+
var schemaNames = Object.keys(this.schemas).filter(function(name) {
|
|
331
|
+
return self.shouldShowSchema(name);
|
|
332
|
+
});
|
|
333
|
+
var nodeMap = {};
|
|
334
|
+
var self = this;
|
|
335
|
+
schemaNames.forEach(function(name, i) {
|
|
336
|
+
var schema = self.schemas[name];
|
|
337
|
+
var node = {
|
|
338
|
+
id: name,
|
|
339
|
+
name: name,
|
|
340
|
+
schema: schema, // Store reference to schema (includes menuicon if loaded)
|
|
341
|
+
type: 'schema',
|
|
342
|
+
x: (i % 10) * 150 + 100,
|
|
343
|
+
y: Math.floor(i / 10) * 150 + 100
|
|
344
|
+
};
|
|
345
|
+
self.nodes.push(node);
|
|
346
|
+
nodeMap[name] = node;
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Build links from relationships
|
|
350
|
+
console.log('[Matrix] ===== Building Links from Relationships =====');
|
|
351
|
+
console.log('[Matrix] Total schemas loaded:', schemaNames.length);
|
|
352
|
+
console.log('[Matrix] Available schema names:', schemaNames.slice(0, 20).join(', '), schemaNames.length > 20 ? '...' : '');
|
|
353
|
+
|
|
354
|
+
var totalRelationshipsFound = 0;
|
|
355
|
+
var totalLinksCreated = 0;
|
|
356
|
+
var missingTargets = [];
|
|
357
|
+
|
|
358
|
+
schemaNames.forEach(function(name) {
|
|
359
|
+
var schema = self.schemas[name];
|
|
360
|
+
|
|
361
|
+
// Debug specific schemas like 'page' and 'task'
|
|
362
|
+
var isDebugSchema = (name === 'page' || name === 'task');
|
|
363
|
+
|
|
364
|
+
if (isDebugSchema) {
|
|
365
|
+
console.log('[Matrix] === Debugging schema: ' + name + ' ===');
|
|
366
|
+
console.log(' - Schema object exists:', !!schema);
|
|
367
|
+
console.log(' - Schema keys:', schema ? Object.keys(schema) : 'none');
|
|
368
|
+
// When summaryOnly=true, the API returns the summary directly, not wrapped
|
|
369
|
+
// So schema IS the summary, not schema.summary
|
|
370
|
+
console.log(' - Has relationships:', !!(schema && schema.relationships));
|
|
371
|
+
if (schema && schema.relationships) {
|
|
372
|
+
console.log(' - Relationships keys:', Object.keys(schema.relationships));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// When summaryOnly=true, the API returns the summary object directly
|
|
377
|
+
// So schema.relationships exists, not schema.summary.relationships
|
|
378
|
+
if (schema && schema.relationships && schema.relationships.outbound) {
|
|
379
|
+
var outbound = schema.relationships.outbound;
|
|
380
|
+
totalRelationshipsFound += outbound.length;
|
|
381
|
+
|
|
382
|
+
if (isDebugSchema) {
|
|
383
|
+
console.log(' - Has outbound relationships:', outbound.length);
|
|
384
|
+
console.log(' - Outbound data:', JSON.stringify(outbound, null, 4));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
outbound.forEach(function(rel) {
|
|
388
|
+
if (isDebugSchema) {
|
|
389
|
+
console.log(' - Processing relationship:', rel.field, '→', rel.targetSchema, '(' + rel.cardinality + ')');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Handle targetSchema that might be a string like "user|group"
|
|
393
|
+
var targetSchemas = (rel.targetSchema || '').split('|');
|
|
394
|
+
|
|
395
|
+
if (isDebugSchema) {
|
|
396
|
+
console.log(' Split into target schemas:', targetSchemas);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
targetSchemas.forEach(function(targetSchema) {
|
|
400
|
+
targetSchema = targetSchema.trim();
|
|
401
|
+
var targetExists = !!(targetSchema && nodeMap[targetSchema]);
|
|
402
|
+
|
|
403
|
+
if (isDebugSchema) {
|
|
404
|
+
console.log(' Checking target "' + targetSchema + '":', targetExists ? '✓ EXISTS' : '✗ NOT FOUND');
|
|
405
|
+
if (!targetExists && targetSchema) {
|
|
406
|
+
var similar = schemaNames.filter(function(n) {
|
|
407
|
+
return n.toLowerCase().indexOf(targetSchema.toLowerCase()) !== -1 ||
|
|
408
|
+
targetSchema.toLowerCase().indexOf(n.toLowerCase()) !== -1;
|
|
409
|
+
});
|
|
410
|
+
if (similar.length > 0) {
|
|
411
|
+
console.log(' Similar schema names found:', similar.join(', '));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (targetSchema && nodeMap[targetSchema]) {
|
|
417
|
+
var link = {
|
|
418
|
+
source: nodeMap[name],
|
|
419
|
+
target: nodeMap[targetSchema],
|
|
420
|
+
field: rel.field,
|
|
421
|
+
cardinality: rel.cardinality || 'one',
|
|
422
|
+
type: 'schema-relationship'
|
|
423
|
+
};
|
|
424
|
+
self.links.push(link);
|
|
425
|
+
totalLinksCreated++;
|
|
426
|
+
|
|
427
|
+
if (isDebugSchema) {
|
|
428
|
+
console.log(' ✓ Created link:', name, '→', targetSchema, 'via field "' + rel.field + '"');
|
|
429
|
+
}
|
|
430
|
+
} else if (targetSchema) {
|
|
431
|
+
missingTargets.push({ from: name, to: targetSchema, field: rel.field });
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
} else {
|
|
436
|
+
if (isDebugSchema) {
|
|
437
|
+
console.log(' - ⚠ No outbound relationships found');
|
|
438
|
+
if (schema) {
|
|
439
|
+
console.log(' Schema keys:', Object.keys(schema));
|
|
440
|
+
if (schema.relationships) {
|
|
441
|
+
console.log(' Relationships keys:', Object.keys(schema.relationships));
|
|
442
|
+
console.log(' Relationships object:', schema.relationships);
|
|
443
|
+
} else {
|
|
444
|
+
console.log(' No relationships property found');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
console.log('[Matrix] ===== Link Building Summary =====');
|
|
452
|
+
console.log('[Matrix] Total relationships found:', totalRelationshipsFound);
|
|
453
|
+
console.log('[Matrix] Total links created:', totalLinksCreated);
|
|
454
|
+
console.log('[Matrix] Missing target schemas:', missingTargets.length);
|
|
455
|
+
if (missingTargets.length > 0) {
|
|
456
|
+
console.log('[Matrix] Missing targets:', missingTargets.slice(0, 10));
|
|
457
|
+
}
|
|
458
|
+
console.log('[Matrix] Final link count:', self.links.length);
|
|
459
|
+
|
|
460
|
+
if (self.links.length === 0) {
|
|
461
|
+
console.warn('[Matrix] ⚠ WARNING: No links were created!');
|
|
462
|
+
console.warn('[Matrix] Possible reasons:');
|
|
463
|
+
console.warn('[Matrix] 1. Schemas don\'t have summary.relationships.outbound');
|
|
464
|
+
console.warn('[Matrix] 2. Target schemas don\'t exist in loaded schemas');
|
|
465
|
+
console.warn('[Matrix] 3. Data structure is different than expected');
|
|
466
|
+
console.warn('[Matrix] Check the API response: /API/schema/page?summaryOnly=true');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Create force simulation (D3 v3 API)
|
|
470
|
+
this.simulation = d3.layout.force()
|
|
471
|
+
.nodes(this.nodes)
|
|
472
|
+
.links(this.links)
|
|
473
|
+
.size([this.width, this.height])
|
|
474
|
+
.linkDistance(150)
|
|
475
|
+
.charge(-500)
|
|
476
|
+
.gravity(0.1)
|
|
477
|
+
.on('tick', function() {
|
|
478
|
+
self.tick();
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Draw links FIRST (so they appear behind nodes) - make sure they're visible
|
|
482
|
+
var link = this.g.selectAll('.matrix-link')
|
|
483
|
+
.data(this.links)
|
|
484
|
+
.enter()
|
|
485
|
+
.append('line')
|
|
486
|
+
.attr('class', function(d) {
|
|
487
|
+
return 'matrix-link ' + (d.cardinality === 'many' ? 'many' : 'one');
|
|
488
|
+
})
|
|
489
|
+
.attr('x1', function(d) { return d.source.x || 0; })
|
|
490
|
+
.attr('y1', function(d) { return d.source.y || 0; })
|
|
491
|
+
.attr('x2', function(d) { return d.target.x || 0; })
|
|
492
|
+
.attr('y2', function(d) { return d.target.y || 0; })
|
|
493
|
+
.style('stroke', '#999')
|
|
494
|
+
.style('stroke-width', function(d) {
|
|
495
|
+
return d.cardinality === 'many' ? '1.5px' : '2px';
|
|
496
|
+
})
|
|
497
|
+
.style('stroke-opacity', '0.6')
|
|
498
|
+
.style('stroke-dasharray', function(d) {
|
|
499
|
+
return d.cardinality === 'many' ? '5,5' : 'none';
|
|
500
|
+
})
|
|
501
|
+
.style('fill', 'none')
|
|
502
|
+
.style('pointer-events', 'none');
|
|
503
|
+
|
|
504
|
+
// Draw link labels - hidden by default, shown when node is selected
|
|
505
|
+
var linkLabelGroup = this.g.selectAll('.matrix-link-label-group')
|
|
506
|
+
.data(this.links)
|
|
507
|
+
.enter()
|
|
508
|
+
.append('g')
|
|
509
|
+
.attr('class', 'matrix-link-label-group')
|
|
510
|
+
.style('opacity', 0)
|
|
511
|
+
.style('display', 'none');
|
|
512
|
+
|
|
513
|
+
// Add background rectangle for label
|
|
514
|
+
linkLabelGroup.append('rect')
|
|
515
|
+
.attr('class', 'matrix-link-label-bg')
|
|
516
|
+
.attr('x', function(d) { return ((d.source.x || 0) + (d.target.x || 0)) / 2 - 20; })
|
|
517
|
+
.attr('y', function(d) { return ((d.source.y || 0) + (d.target.y || 0)) / 2 - 8; })
|
|
518
|
+
.attr('width', 40)
|
|
519
|
+
.attr('height', 16)
|
|
520
|
+
.attr('rx', 4)
|
|
521
|
+
.style('fill', 'rgba(255, 255, 255, 0.9)')
|
|
522
|
+
.style('stroke', '#ddd')
|
|
523
|
+
.style('stroke-width', '1px');
|
|
524
|
+
|
|
525
|
+
// Add text label
|
|
526
|
+
linkLabelGroup.append('text')
|
|
527
|
+
.attr('class', 'matrix-link-label')
|
|
528
|
+
.attr('x', function(d) { return ((d.source.x || 0) + (d.target.x || 0)) / 2; })
|
|
529
|
+
.attr('y', function(d) { return ((d.source.y || 0) + (d.target.y || 0)) / 2 + 4; })
|
|
530
|
+
.text(function(d) {
|
|
531
|
+
return d.field.split('.').pop();
|
|
532
|
+
})
|
|
533
|
+
.style('pointer-events', 'none');
|
|
534
|
+
|
|
535
|
+
// Draw nodes AFTER links (so they appear on top)
|
|
536
|
+
var node = this.g.selectAll('.matrix-node')
|
|
537
|
+
.data(this.nodes)
|
|
538
|
+
.enter()
|
|
539
|
+
.append('g')
|
|
540
|
+
.attr('class', 'matrix-node')
|
|
541
|
+
.attr('transform', function(d) { return 'translate(' + (d.x || 0) + ',' + (d.y || 0) + ')'; })
|
|
542
|
+
.call(this.simulation.drag)
|
|
543
|
+
.on('click', function(d) {
|
|
544
|
+
self.selectNode(d);
|
|
545
|
+
})
|
|
546
|
+
.on('mouseover', function(d) {
|
|
547
|
+
self.showTooltip(d);
|
|
548
|
+
})
|
|
549
|
+
.on('mouseout', function() {
|
|
550
|
+
self.hideTooltip();
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// Add icon if available, otherwise use circle
|
|
554
|
+
node.each(function(d) {
|
|
555
|
+
var nodeGroup = d3.select(this);
|
|
556
|
+
var schema = d.schema;
|
|
557
|
+
var menuicon = null;
|
|
558
|
+
|
|
559
|
+
// Get menuicon from schema (loaded separately if not in summary)
|
|
560
|
+
if (schema && schema.menuicon) {
|
|
561
|
+
menuicon = schema.menuicon;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Always add a background circle for clickability (larger hitbox)
|
|
565
|
+
// Note: Base styles are in CSS, only pointer-events and cursor set here
|
|
566
|
+
var bgCircle = nodeGroup.append('circle')
|
|
567
|
+
.attr('class', 'matrix-node-bg')
|
|
568
|
+
.attr('r', 16) // Larger radius for better clickability
|
|
569
|
+
.attr('cx', 0)
|
|
570
|
+
.attr('cy', 0)
|
|
571
|
+
.style('pointer-events', 'all')
|
|
572
|
+
.style('cursor', 'pointer');
|
|
573
|
+
// Fill, stroke, and stroke-width are handled by CSS
|
|
574
|
+
|
|
575
|
+
if (menuicon && typeof menuicon === 'string' && menuicon.indexOf('<svg') !== -1) {
|
|
576
|
+
// Use foreignObject to embed HTML SVG
|
|
577
|
+
var iconGroup = nodeGroup.append('g')
|
|
578
|
+
.attr('class', 'matrix-node-icon')
|
|
579
|
+
.attr('transform', 'translate(-12,-12)')
|
|
580
|
+
.style('pointer-events', 'none'); // Let clicks pass through to the circle
|
|
581
|
+
|
|
582
|
+
var foreignObject = iconGroup.append('foreignObject')
|
|
583
|
+
.attr('width', 24)
|
|
584
|
+
.attr('height', 24)
|
|
585
|
+
.attr('x', 0)
|
|
586
|
+
.attr('y', 0)
|
|
587
|
+
.style('pointer-events', 'none');
|
|
588
|
+
|
|
589
|
+
// Create a div to hold the SVG HTML
|
|
590
|
+
var iconDiv = foreignObject.append('xhtml:div')
|
|
591
|
+
.style('width', '24px')
|
|
592
|
+
.style('height', '24px')
|
|
593
|
+
.style('overflow', 'hidden')
|
|
594
|
+
.style('line-height', '0')
|
|
595
|
+
.style('pointer-events', 'none');
|
|
596
|
+
|
|
597
|
+
// Parse and modify the SVG to fit
|
|
598
|
+
try {
|
|
599
|
+
var tempDiv = document.createElement('div');
|
|
600
|
+
tempDiv.innerHTML = menuicon;
|
|
601
|
+
var svgElement = tempDiv.querySelector('svg');
|
|
602
|
+
|
|
603
|
+
if (svgElement) {
|
|
604
|
+
// Get viewBox or set default
|
|
605
|
+
var viewBox = svgElement.getAttribute('viewBox') || '0 0 24 24';
|
|
606
|
+
svgElement.setAttribute('width', '24');
|
|
607
|
+
svgElement.setAttribute('height', '24');
|
|
608
|
+
svgElement.setAttribute('viewBox', viewBox);
|
|
609
|
+
svgElement.style.width = '24px';
|
|
610
|
+
svgElement.style.height = '24px';
|
|
611
|
+
svgElement.style.display = 'block';
|
|
612
|
+
|
|
613
|
+
// Validate and fix SVG paths before setting
|
|
614
|
+
var paths = svgElement.querySelectorAll('path');
|
|
615
|
+
var validElements = false;
|
|
616
|
+
var nodeName = d.name || 'unknown';
|
|
617
|
+
paths.forEach(function(path) {
|
|
618
|
+
var pathData = path.getAttribute('d');
|
|
619
|
+
if (pathData) {
|
|
620
|
+
var trimmed = pathData.trim();
|
|
621
|
+
// Check if path starts with moveto command (M or m)
|
|
622
|
+
if (!/^[Mm]/.test(trimmed)) {
|
|
623
|
+
// Invalid path - try to fix by prepending M 0,0
|
|
624
|
+
console.warn('[Matrix] Invalid SVG path found for schema', nodeName + ', fixing:', trimmed.substring(0, 20));
|
|
625
|
+
// Try to fix by prepending a moveto
|
|
626
|
+
path.setAttribute('d', 'M 0,0 ' + trimmed);
|
|
627
|
+
validElements = true;
|
|
628
|
+
} else {
|
|
629
|
+
validElements = true;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// Check for other valid elements
|
|
635
|
+
if (!validElements) {
|
|
636
|
+
validElements = !!(svgElement.querySelector('circle') || svgElement.querySelector('rect') || svgElement.querySelector('polygon') || svgElement.querySelector('polyline'));
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Only set if we have valid content
|
|
640
|
+
if (validElements) {
|
|
641
|
+
iconDiv.node().innerHTML = svgElement.outerHTML;
|
|
642
|
+
} else {
|
|
643
|
+
// No valid paths, just show the background circle
|
|
644
|
+
console.warn('[Matrix] No valid SVG elements found for', d.name || 'unknown');
|
|
645
|
+
iconGroup.remove();
|
|
646
|
+
}
|
|
647
|
+
} else {
|
|
648
|
+
// No SVG element found, remove icon group
|
|
649
|
+
iconGroup.remove();
|
|
650
|
+
}
|
|
651
|
+
} catch (err) {
|
|
652
|
+
console.warn('[Matrix] Error parsing SVG icon for', d.name || 'unknown' + ':', err);
|
|
653
|
+
// Remove icon group on error, just show background circle
|
|
654
|
+
iconGroup.remove();
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// If no icon, we already have the background circle, so we're done
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
node.append('text')
|
|
661
|
+
.attr('dy', 20)
|
|
662
|
+
.text(function(d) {
|
|
663
|
+
return d.name;
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// Start simulation
|
|
667
|
+
this.simulation.start();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
tick() {
|
|
671
|
+
var self = this;
|
|
672
|
+
this.g.selectAll('.matrix-link')
|
|
673
|
+
.attr('x1', function(d) { return d.source.x; })
|
|
674
|
+
.attr('y1', function(d) { return d.source.y; })
|
|
675
|
+
.attr('x2', function(d) { return d.target.x; })
|
|
676
|
+
.attr('y2', function(d) { return d.target.y; });
|
|
677
|
+
|
|
678
|
+
// Update link label positions
|
|
679
|
+
this.g.selectAll('.matrix-link-label-group')
|
|
680
|
+
.each(function(d) {
|
|
681
|
+
var group = d3.select(this);
|
|
682
|
+
var x = (d.source.x + d.target.x) / 2;
|
|
683
|
+
var y = (d.source.y + d.target.y) / 2;
|
|
684
|
+
group.select('.matrix-link-label-bg')
|
|
685
|
+
.attr('x', x - 20)
|
|
686
|
+
.attr('y', y - 8);
|
|
687
|
+
group.select('.matrix-link-label')
|
|
688
|
+
.attr('x', x)
|
|
689
|
+
.attr('y', y + 4);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
this.g.selectAll('.matrix-node')
|
|
693
|
+
.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; });
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
selectNode(node) {
|
|
697
|
+
// Remove previous selection
|
|
698
|
+
this.g.selectAll('.matrix-node').classed('selected', false);
|
|
699
|
+
|
|
700
|
+
// Select new node
|
|
701
|
+
var self = this;
|
|
702
|
+
var nodeElement = this.g.selectAll('.matrix-node')
|
|
703
|
+
.filter(function(d) { return d.id === node.id; })
|
|
704
|
+
.classed('selected', true);
|
|
705
|
+
|
|
706
|
+
this.selectedNode = node;
|
|
707
|
+
|
|
708
|
+
// Show schema properties panel
|
|
709
|
+
this.showSchemaProperties(node);
|
|
710
|
+
|
|
711
|
+
// Highlight connected nodes and links
|
|
712
|
+
this.highlightConnections(node);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
highlightConnections(node) {
|
|
716
|
+
var self = this;
|
|
717
|
+
|
|
718
|
+
if (!node) {
|
|
719
|
+
// Reset all to normal
|
|
720
|
+
this.g.selectAll('.matrix-link').style('stroke-opacity', 0.6);
|
|
721
|
+
this.g.selectAll('.matrix-node').style('opacity', 1);
|
|
722
|
+
// Hide all link labels
|
|
723
|
+
this.g.selectAll('.matrix-link-label-group')
|
|
724
|
+
.style('opacity', 0)
|
|
725
|
+
.style('display', 'none');
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Reset all links
|
|
730
|
+
this.g.selectAll('.matrix-link').style('stroke-opacity', 0.1);
|
|
731
|
+
this.g.selectAll('.matrix-node').style('opacity', 0.3);
|
|
732
|
+
// Hide all link labels initially
|
|
733
|
+
this.g.selectAll('.matrix-link-label-group')
|
|
734
|
+
.style('opacity', 0)
|
|
735
|
+
.style('display', 'none');
|
|
736
|
+
|
|
737
|
+
// Highlight selected node
|
|
738
|
+
this.g.selectAll('.matrix-node')
|
|
739
|
+
.filter(function(d) {
|
|
740
|
+
return d.id === node.id;
|
|
741
|
+
})
|
|
742
|
+
.style('opacity', 1);
|
|
743
|
+
|
|
744
|
+
// Highlight connected nodes and links, and show their labels
|
|
745
|
+
var connectedNodeIds = {};
|
|
746
|
+
var connectedLinks = [];
|
|
747
|
+
connectedNodeIds[node.id] = true;
|
|
748
|
+
|
|
749
|
+
this.links.forEach(function(link) {
|
|
750
|
+
var sourceId = (typeof link.source === 'object' ? link.source.id : link.source);
|
|
751
|
+
var targetId = (typeof link.target === 'object' ? link.target.id : link.target);
|
|
752
|
+
|
|
753
|
+
if (sourceId === node.id || targetId === node.id) {
|
|
754
|
+
connectedNodeIds[sourceId] = true;
|
|
755
|
+
connectedNodeIds[targetId] = true;
|
|
756
|
+
connectedLinks.push(link);
|
|
757
|
+
|
|
758
|
+
self.g.selectAll('.matrix-link')
|
|
759
|
+
.filter(function(d) {
|
|
760
|
+
var dSourceId = (typeof d.source === 'object' ? d.source.id : d.source);
|
|
761
|
+
var dTargetId = (typeof d.target === 'object' ? d.target.id : d.target);
|
|
762
|
+
return (dSourceId === sourceId && dTargetId === targetId) ||
|
|
763
|
+
(dSourceId === targetId && dTargetId === sourceId);
|
|
764
|
+
})
|
|
765
|
+
.style('stroke-opacity', 1)
|
|
766
|
+
.style('stroke-width', '2.5px');
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// Show labels for connected links
|
|
771
|
+
connectedLinks.forEach(function(link) {
|
|
772
|
+
self.g.selectAll('.matrix-link-label-group')
|
|
773
|
+
.filter(function(d) {
|
|
774
|
+
var dSourceId = (typeof d.source === 'object' ? d.source.id : d.source);
|
|
775
|
+
var dTargetId = (typeof d.target === 'object' ? d.target.id : d.target);
|
|
776
|
+
var linkSourceId = (typeof link.source === 'object' ? link.source.id : link.source);
|
|
777
|
+
var linkTargetId = (typeof link.target === 'object' ? link.target.id : link.target);
|
|
778
|
+
return (dSourceId === linkSourceId && dTargetId === linkTargetId) ||
|
|
779
|
+
(dSourceId === linkTargetId && dTargetId === linkSourceId);
|
|
780
|
+
})
|
|
781
|
+
.style('opacity', 1)
|
|
782
|
+
.style('display', 'block');
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
Object.keys(connectedNodeIds).forEach(function(id) {
|
|
786
|
+
self.g.selectAll('.matrix-node')
|
|
787
|
+
.filter(function(d) {
|
|
788
|
+
return d.id === id;
|
|
789
|
+
})
|
|
790
|
+
.style('opacity', 1);
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
showTooltip(node) {
|
|
795
|
+
var self = this;
|
|
796
|
+
var schema = node.schema;
|
|
797
|
+
var tooltipText = '<div style="max-width:300px;">';
|
|
798
|
+
tooltipText += '<strong style="font-size:14px;color:#fff;">' + node.name + '</strong><br/>';
|
|
799
|
+
|
|
800
|
+
// When summaryOnly=true, schema IS the summary object
|
|
801
|
+
if (schema) {
|
|
802
|
+
if (schema.description) {
|
|
803
|
+
tooltipText += '<div style="margin-top:6px;font-size:12px;color:#ddd;">' + schema.description + '</div>';
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (schema.relationships && schema.relationships.outbound && schema.relationships.outbound.length > 0) {
|
|
807
|
+
tooltipText += '<div style="margin-top:8px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.3);">';
|
|
808
|
+
tooltipText += '<strong style="font-size:12px;color:#fff;">Relationships (' + schema.relationships.outbound.length + '):</strong><br/>';
|
|
809
|
+
tooltipText += '<div style="margin-top:4px;font-size:11px;">';
|
|
810
|
+
|
|
811
|
+
schema.relationships.outbound.forEach(function(rel) {
|
|
812
|
+
var targetSchemas = (rel.targetSchema || '').split('|');
|
|
813
|
+
var cardinalityIcon = rel.cardinality === 'many' ? '⟷' : '→';
|
|
814
|
+
|
|
815
|
+
targetSchemas.forEach(function(targetSchema) {
|
|
816
|
+
targetSchema = targetSchema.trim();
|
|
817
|
+
var linkExists = self.links.some(function(link) {
|
|
818
|
+
var sourceId = (typeof link.source === 'object' ? link.source.id : link.source);
|
|
819
|
+
var targetId = (typeof link.target === 'object' ? link.target.id : link.target);
|
|
820
|
+
return (sourceId === node.name && targetId === targetSchema);
|
|
821
|
+
});
|
|
822
|
+
var existsMarker = linkExists ? '✓' : '⚠';
|
|
823
|
+
|
|
824
|
+
tooltipText += '<div style="margin:2px 0;">';
|
|
825
|
+
tooltipText += '<code style="background:rgba(255,255,255,0.2);padding:1px 4px;border-radius:2px;font-size:10px;">' + rel.field + '</code> ';
|
|
826
|
+
tooltipText += '<span>' + cardinalityIcon + '</span> ';
|
|
827
|
+
tooltipText += '<strong>' + targetSchema + '</strong> ';
|
|
828
|
+
tooltipText += '<span style="color:#bbb;">(' + rel.cardinality + ')</span> ';
|
|
829
|
+
tooltipText += '<span style="color:' + (linkExists ? '#4caf50' : '#ff9800') + ';">' + existsMarker + '</span>';
|
|
830
|
+
tooltipText += '</div>';
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
tooltipText += '</div></div>';
|
|
835
|
+
} else {
|
|
836
|
+
tooltipText += '<div style="margin-top:8px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.3);color:#bbb;font-size:11px;">No relationships defined</div>';
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
tooltipText += '</div>';
|
|
841
|
+
|
|
842
|
+
this.tooltip
|
|
843
|
+
.html(tooltipText)
|
|
844
|
+
.classed('visible', true)
|
|
845
|
+
.style('left', (d3.event.pageX + 10) + 'px')
|
|
846
|
+
.style('top', (d3.event.pageY - 10) + 'px');
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
hideTooltip() {
|
|
850
|
+
this.tooltip.classed('visible', false);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
resetView() {
|
|
854
|
+
this.zoom.scale(1);
|
|
855
|
+
this.zoom.translate([0, 0]);
|
|
856
|
+
this.svg.transition()
|
|
857
|
+
.duration(750)
|
|
858
|
+
.call(this.zoom.event);
|
|
859
|
+
|
|
860
|
+
// Reset node highlighting
|
|
861
|
+
this.g.selectAll('.matrix-link').style('stroke-opacity', 0.6).style('opacity', 1);
|
|
862
|
+
this.g.selectAll('.matrix-node').style('opacity', 1).classed('selected', false);
|
|
863
|
+
this.selectedNode = null;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
fitToScreen() {
|
|
867
|
+
if (this.nodes.length === 0) return;
|
|
868
|
+
|
|
869
|
+
var bounds = this.nodes.reduce(function(acc, node) {
|
|
870
|
+
return {
|
|
871
|
+
minX: Math.min(acc.minX, node.x),
|
|
872
|
+
maxX: Math.max(acc.maxX, node.x),
|
|
873
|
+
minY: Math.min(acc.minY, node.y),
|
|
874
|
+
maxY: Math.max(acc.maxY, node.y)
|
|
875
|
+
};
|
|
876
|
+
}, { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity });
|
|
877
|
+
|
|
878
|
+
var graphWidth = bounds.maxX - bounds.minX;
|
|
879
|
+
var graphHeight = bounds.maxY - bounds.minY;
|
|
880
|
+
var scale = Math.min(this.width / graphWidth, this.height / graphHeight) * 0.8;
|
|
881
|
+
var translateX = (this.width - (bounds.minX + bounds.maxX) * scale) / 2;
|
|
882
|
+
var translateY = (this.height - (bounds.minY + bounds.maxY) * scale) / 2;
|
|
883
|
+
|
|
884
|
+
this.zoom.scale(scale);
|
|
885
|
+
this.zoom.translate([translateX, translateY]);
|
|
886
|
+
this.svg.transition()
|
|
887
|
+
.duration(750)
|
|
888
|
+
.call(this.zoom.event);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
showSchemaProperties(node) {
|
|
892
|
+
var self = this;
|
|
893
|
+
var schema = node.schema;
|
|
894
|
+
|
|
895
|
+
// Remove existing properties panel
|
|
896
|
+
var existing = document.getElementById('matrix-properties-panel');
|
|
897
|
+
if (existing) {
|
|
898
|
+
existing.remove();
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Create properties panel
|
|
902
|
+
var panel = document.createElement('div');
|
|
903
|
+
panel.id = 'matrix-properties-panel';
|
|
904
|
+
|
|
905
|
+
var html = '<div class="matrix-props-header">';
|
|
906
|
+
// Add icon if available
|
|
907
|
+
var iconHtml = '';
|
|
908
|
+
if (schema && schema.menuicon && typeof schema.menuicon === 'string' && schema.menuicon.indexOf('<svg') !== -1) {
|
|
909
|
+
iconHtml = '<span class="matrix-props-title-icon">' + schema.menuicon + '</span>';
|
|
910
|
+
}
|
|
911
|
+
html += '<h2 class="matrix-props-title">' + iconHtml + '<span class="matrix-props-title-text">' + node.name + '</span></h2>';
|
|
912
|
+
html += '<button id="close-properties">×</button>';
|
|
913
|
+
html += '</div>';
|
|
914
|
+
|
|
915
|
+
// When summaryOnly=true, schema IS the summary object
|
|
916
|
+
if (schema) {
|
|
917
|
+
if (schema.description) {
|
|
918
|
+
html += '<div class="matrix-props-section"><strong>Description:</strong><br/><span class="matrix-props-text">' + (schema.description || 'N/A') + '</span></div>';
|
|
919
|
+
}
|
|
920
|
+
if (schema.purpose) {
|
|
921
|
+
html += '<div class="matrix-props-section"><strong>Purpose:</strong><br/><span class="matrix-props-text">' + (schema.purpose || 'N/A') + '</span></div>';
|
|
922
|
+
}
|
|
923
|
+
if (schema.labelField) {
|
|
924
|
+
html += '<div class="matrix-props-section"><strong>Label Field:</strong> <code>' + schema.labelField + '</code></div>';
|
|
925
|
+
}
|
|
926
|
+
if (schema.source) {
|
|
927
|
+
html += '<div class="matrix-props-section"><strong>Source:</strong> <span class="matrix-props-text">' + schema.source + '</span></div>';
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Relationships - Show prominently at the top
|
|
932
|
+
// When summaryOnly=true, schema.relationships exists directly
|
|
933
|
+
if (schema && schema.relationships && schema.relationships.outbound && schema.relationships.outbound.length > 0) {
|
|
934
|
+
html += '<div class="matrix-props-relationships">';
|
|
935
|
+
html += '<strong class="matrix-props-relationships-title">Outbound Relationships (' + schema.relationships.outbound.length + '):</strong>';
|
|
936
|
+
html += '<div class="matrix-props-relationships-list">';
|
|
937
|
+
schema.relationships.outbound.forEach(function(rel) {
|
|
938
|
+
var targetSchemas = (rel.targetSchema || '').split('|');
|
|
939
|
+
var cardinalityIcon = rel.cardinality === 'many' ? '⟷' : '→';
|
|
940
|
+
var cardinalityClass = rel.cardinality === 'many' ? 'many' : 'one';
|
|
941
|
+
|
|
942
|
+
targetSchemas.forEach(function(targetSchema) {
|
|
943
|
+
targetSchema = targetSchema.trim();
|
|
944
|
+
var linkExists = self.links.some(function(link) {
|
|
945
|
+
var sourceId = (typeof link.source === 'object' ? link.source.id : link.source);
|
|
946
|
+
var targetId = (typeof link.target === 'object' ? link.target.id : link.target);
|
|
947
|
+
return (sourceId === node.name && targetId === targetSchema) ||
|
|
948
|
+
(sourceId === targetSchema && targetId === node.name);
|
|
949
|
+
});
|
|
950
|
+
var missingClass = linkExists ? '' : ' missing';
|
|
951
|
+
|
|
952
|
+
html += '<div class="matrix-props-relationship-item ' + cardinalityClass + missingClass + '">';
|
|
953
|
+
html += '<code class="matrix-props-relationship-field">' + rel.field + '</code>';
|
|
954
|
+
html += '<span class="matrix-props-relationship-arrow">' + cardinalityIcon + '</span>';
|
|
955
|
+
html += '<strong>' + targetSchema + '</strong>';
|
|
956
|
+
html += ' <span class="matrix-props-relationship-cardinality">(' + rel.cardinality + ')</span>';
|
|
957
|
+
if (!linkExists) {
|
|
958
|
+
html += ' <span class="matrix-props-relationship-warning">[target not loaded]</span>';
|
|
959
|
+
}
|
|
960
|
+
html += '</div>';
|
|
961
|
+
});
|
|
962
|
+
});
|
|
963
|
+
html += '</div></div>';
|
|
964
|
+
} else {
|
|
965
|
+
html += '<div class="matrix-props-no-relationships">';
|
|
966
|
+
html += '<strong>No outbound relationships found</strong><br/>';
|
|
967
|
+
html += '<span class="matrix-props-no-relationships-text">This schema may not have relationships defined in its summary.</span>';
|
|
968
|
+
html += '</div>';
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Fields
|
|
972
|
+
if (schema && schema.fields && schema.fields.length > 0) {
|
|
973
|
+
html += '<div class="matrix-props-section"><strong>Fields (' + schema.fields.length + '):</strong>';
|
|
974
|
+
html += '<div class="matrix-props-fields-container">';
|
|
975
|
+
schema.fields.forEach(function(field) {
|
|
976
|
+
var fieldInfo = '<div class="matrix-props-field-item">';
|
|
977
|
+
fieldInfo += '<code>' + field.name + '</code>';
|
|
978
|
+
fieldInfo += ' <span class="matrix-props-field-type">(' + field.type;
|
|
979
|
+
if (field.isArray) fieldInfo += '[]';
|
|
980
|
+
if (field.isReference) fieldInfo += ', ref';
|
|
981
|
+
if (field.targetSchema) fieldInfo += ' → ' + field.targetSchema;
|
|
982
|
+
if (field.required) fieldInfo += ', required';
|
|
983
|
+
fieldInfo += ')</span>';
|
|
984
|
+
if (field.comment || field.tooltip) {
|
|
985
|
+
fieldInfo += '<br/><span class="matrix-props-field-comment">' + (field.comment || field.tooltip || '') + '</span>';
|
|
986
|
+
}
|
|
987
|
+
fieldInfo += '</div>';
|
|
988
|
+
html += fieldInfo;
|
|
989
|
+
});
|
|
990
|
+
html += '</div></div>';
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Searchable fields
|
|
994
|
+
if (schema && schema.searchableFields && schema.searchableFields.length > 0) {
|
|
995
|
+
html += '<div class="matrix-props-section"><strong>Searchable Fields:</strong><br/>';
|
|
996
|
+
html += '<div class="matrix-props-searchable-list">';
|
|
997
|
+
schema.searchableFields.forEach(function(field) {
|
|
998
|
+
html += '<span class="matrix-props-searchable-tag">' + field + '</span>';
|
|
999
|
+
});
|
|
1000
|
+
html += '</div></div>';
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
panel.innerHTML = html;
|
|
1004
|
+
this.appendChild(panel);
|
|
1005
|
+
|
|
1006
|
+
// Close button handler
|
|
1007
|
+
document.getElementById('close-properties').addEventListener('click', function() {
|
|
1008
|
+
panel.remove();
|
|
1009
|
+
self.g.selectAll('.matrix-node').classed('selected', false);
|
|
1010
|
+
self.selectedNode = null;
|
|
1011
|
+
self.highlightConnections(null);
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
disconnectedCallback() {
|
|
1016
|
+
if (this.simulation) {
|
|
1017
|
+
this.simulation.stop();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
populateAppFilter() {
|
|
1022
|
+
var appFilter = document.getElementById('app-filter');
|
|
1023
|
+
if (!appFilter) return;
|
|
1024
|
+
|
|
1025
|
+
// Clear existing options except "All"
|
|
1026
|
+
appFilter.innerHTML = '<option value="">All</option>';
|
|
1027
|
+
|
|
1028
|
+
// Add app options
|
|
1029
|
+
var appNames = Object.keys(this.appMap || {}).sort();
|
|
1030
|
+
var self = this;
|
|
1031
|
+
appNames.forEach(function(appName) {
|
|
1032
|
+
var opt = document.createElement('option');
|
|
1033
|
+
opt.value = appName;
|
|
1034
|
+
opt.textContent = appName;
|
|
1035
|
+
appFilter.appendChild(opt);
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
setAppFilter(appName) {
|
|
1040
|
+
this.selectedApp = appName || '';
|
|
1041
|
+
// Rebuild the graph with filtered schemas
|
|
1042
|
+
if (this.mode === 'schema-to-schema') {
|
|
1043
|
+
this.buildSchemaToSchemaGraph();
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
shouldShowSchema(schemaName) {
|
|
1048
|
+
// Always show core schemas: user, tag, and status
|
|
1049
|
+
var coreSchemas = ['user', 'tag', 'status'];
|
|
1050
|
+
if (coreSchemas.indexOf(schemaName) !== -1) {
|
|
1051
|
+
return true;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (!this.selectedApp) {
|
|
1055
|
+
return true; // Show all if no filter
|
|
1056
|
+
}
|
|
1057
|
+
var schema = this.schemas[schemaName];
|
|
1058
|
+
if (!schema || !schema.apps) {
|
|
1059
|
+
return false; // Hide if no app info
|
|
1060
|
+
}
|
|
1061
|
+
return schema.apps.indexOf(this.selectedApp) !== -1;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
window.customElements.define('joe-matrix', JoeMatrix);
|