@praxisui/page-builder 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +102 -0
- package/fesm2022/praxisui-page-builder-connection-graph.component-C6x--6--.mjs +1129 -0
- package/fesm2022/praxisui-page-builder-connection-graph.component-C6x--6--.mjs.map +1 -0
- package/fesm2022/praxisui-page-builder.mjs +620 -0
- package/fesm2022/praxisui-page-builder.mjs.map +1 -0
- package/index.d.ts +134 -0
- package/package.json +41 -0
|
@@ -0,0 +1,1129 @@
|
|
|
1
|
+
import * as i3 from '@angular/common';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import * as i0 from '@angular/core';
|
|
4
|
+
import { EventEmitter, HostListener, Output, Input, Directive, Injectable, signal, Optional, Inject, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
5
|
+
import * as i4 from '@angular/forms';
|
|
6
|
+
import { FormsModule } from '@angular/forms';
|
|
7
|
+
import * as i2$2 from '@angular/material/button';
|
|
8
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
9
|
+
import * as i3$1 from '@angular/material/icon';
|
|
10
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
11
|
+
import * as i3$2 from '@angular/material/tooltip';
|
|
12
|
+
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
13
|
+
import { BehaviorSubject } from 'rxjs';
|
|
14
|
+
import * as i2$1 from '@praxisui/settings-panel';
|
|
15
|
+
import { SETTINGS_PANEL_DATA } from '@praxisui/settings-panel';
|
|
16
|
+
import { ConnectionBuilderComponent } from './praxisui-page-builder.mjs';
|
|
17
|
+
import * as i2 from '@praxisui/core';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Simple elbow router (L-shape): horizontal then vertical.
|
|
21
|
+
* Produces an SVG path string.
|
|
22
|
+
*/
|
|
23
|
+
function routeElbow(from, to) {
|
|
24
|
+
const midX = (from.x + to.x) / 2;
|
|
25
|
+
const p = [
|
|
26
|
+
`M ${from.x} ${from.y}`,
|
|
27
|
+
`L ${midX} ${from.y}`,
|
|
28
|
+
`L ${midX} ${to.y}`,
|
|
29
|
+
`L ${to.x} ${to.y}`,
|
|
30
|
+
];
|
|
31
|
+
return p.join(' ');
|
|
32
|
+
}
|
|
33
|
+
/** Straight line path */
|
|
34
|
+
function routeStraight(from, to) {
|
|
35
|
+
return `M ${from.x} ${from.y} L ${to.x} ${to.y}`;
|
|
36
|
+
}
|
|
37
|
+
/** Simple cubic bezier: horizontal tangents from endpoints */
|
|
38
|
+
function routeBezier(from, to) {
|
|
39
|
+
const dx = (to.x - from.x) * 0.5;
|
|
40
|
+
const c1 = { x: from.x + dx, y: from.y };
|
|
41
|
+
const c2 = { x: to.x - dx, y: to.y };
|
|
42
|
+
return `M ${from.x} ${from.y} C ${c1.x} ${c1.y}, ${c2.x} ${c2.y}, ${to.x} ${to.y}`;
|
|
43
|
+
}
|
|
44
|
+
/** Basic geometry helpers for auto-routing */
|
|
45
|
+
function rectContains(r, p) {
|
|
46
|
+
return p.x >= r.x && p.x <= r.x + r.width && p.y >= r.y && p.y <= r.y + r.height;
|
|
47
|
+
}
|
|
48
|
+
function segmentsIntersect(p1, p2, p3, p4) {
|
|
49
|
+
// cross product approach
|
|
50
|
+
const d = (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y);
|
|
51
|
+
if (d === 0)
|
|
52
|
+
return false;
|
|
53
|
+
const ua = ((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) / d;
|
|
54
|
+
const ub = ((p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x)) / d;
|
|
55
|
+
return ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1;
|
|
56
|
+
}
|
|
57
|
+
function lineIntersectsRect(p1, p2, r) {
|
|
58
|
+
if (rectContains(r, p1) || rectContains(r, p2))
|
|
59
|
+
return true;
|
|
60
|
+
const tl = { x: r.x, y: r.y };
|
|
61
|
+
const tr = { x: r.x + r.width, y: r.y };
|
|
62
|
+
const bl = { x: r.x, y: r.y + r.height };
|
|
63
|
+
const br = { x: r.x + r.width, y: r.y + r.height };
|
|
64
|
+
return (segmentsIntersect(p1, p2, tl, tr) ||
|
|
65
|
+
segmentsIntersect(p1, p2, tr, br) ||
|
|
66
|
+
segmentsIntersect(p1, p2, br, bl) ||
|
|
67
|
+
segmentsIntersect(p1, p2, bl, tl));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Auto router: choose straight/bezier if clear, else elbow to avoid boxes.
|
|
71
|
+
* Obstacles are rectangles in world space. Optional padding inflates obstacles.
|
|
72
|
+
*/
|
|
73
|
+
function routeAuto(from, to, obstacles = [], padding = 6) {
|
|
74
|
+
const inflated = obstacles.map(r => ({ x: r.x - padding, y: r.y - padding, width: r.width + padding * 2, height: r.height + padding * 2 }));
|
|
75
|
+
const intersects = inflated.some(r => lineIntersectsRect(from, to, r));
|
|
76
|
+
if (intersects)
|
|
77
|
+
return routeElbow(from, to);
|
|
78
|
+
// prefer bezier for aesthetics when unobstructed
|
|
79
|
+
return routeBezier(from, to);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class DragConnectDirective {
|
|
83
|
+
el;
|
|
84
|
+
from;
|
|
85
|
+
connectDragStart = new EventEmitter();
|
|
86
|
+
connectDragMove = new EventEmitter();
|
|
87
|
+
connectDragEnd = new EventEmitter();
|
|
88
|
+
active = false;
|
|
89
|
+
onMove = (e) => this.handleMove(e);
|
|
90
|
+
onUp = (e) => this.handleUp(e);
|
|
91
|
+
constructor(el) {
|
|
92
|
+
this.el = el;
|
|
93
|
+
}
|
|
94
|
+
onMouseDown(ev) {
|
|
95
|
+
if (!this.from)
|
|
96
|
+
return;
|
|
97
|
+
ev.preventDefault();
|
|
98
|
+
ev.stopPropagation();
|
|
99
|
+
this.active = true;
|
|
100
|
+
const rect = this.el.nativeElement.getBoundingClientRect();
|
|
101
|
+
const x = rect.left + rect.width / 2;
|
|
102
|
+
const y = rect.top + rect.height / 2;
|
|
103
|
+
try {
|
|
104
|
+
console.debug('[DragConnect] mousedown start', { from: this.from, x, y, target: ev.target?.tagName });
|
|
105
|
+
}
|
|
106
|
+
catch { }
|
|
107
|
+
this.connectDragStart.emit({ from: this.from, x, y });
|
|
108
|
+
document.addEventListener('mousemove', this.onMove);
|
|
109
|
+
document.addEventListener('mouseup', this.onUp, { once: true });
|
|
110
|
+
}
|
|
111
|
+
handleMove(e) {
|
|
112
|
+
if (!this.active)
|
|
113
|
+
return;
|
|
114
|
+
try {
|
|
115
|
+
console.debug('[DragConnect] mousemove', { x: e.clientX, y: e.clientY });
|
|
116
|
+
}
|
|
117
|
+
catch { }
|
|
118
|
+
this.connectDragMove.emit({ x: e.clientX, y: e.clientY });
|
|
119
|
+
}
|
|
120
|
+
handleUp(e) {
|
|
121
|
+
if (!this.active)
|
|
122
|
+
return;
|
|
123
|
+
this.active = false;
|
|
124
|
+
document.removeEventListener('mousemove', this.onMove);
|
|
125
|
+
try {
|
|
126
|
+
console.debug('[DragConnect] mouseup end', { x: e.clientX, y: e.clientY, target: e.target?.tagName, classes: e.target?.className });
|
|
127
|
+
}
|
|
128
|
+
catch { }
|
|
129
|
+
this.connectDragEnd.emit({ x: e.clientX, y: e.clientY, endEvent: e });
|
|
130
|
+
}
|
|
131
|
+
ngOnDestroy() {
|
|
132
|
+
document.removeEventListener('mousemove', this.onMove);
|
|
133
|
+
}
|
|
134
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: DragConnectDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
|
|
135
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.1.4", type: DragConnectDirective, isStandalone: true, selector: "[dragConnect]", inputs: { from: ["dragConnect", "from"] }, outputs: { connectDragStart: "connectDragStart", connectDragMove: "connectDragMove", connectDragEnd: "connectDragEnd" }, host: { listeners: { "mousedown": "onMouseDown($event)" } }, ngImport: i0 });
|
|
136
|
+
}
|
|
137
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: DragConnectDirective, decorators: [{
|
|
138
|
+
type: Directive,
|
|
139
|
+
args: [{
|
|
140
|
+
selector: '[dragConnect]'
|
|
141
|
+
}]
|
|
142
|
+
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { from: [{
|
|
143
|
+
type: Input,
|
|
144
|
+
args: ['dragConnect']
|
|
145
|
+
}], connectDragStart: [{
|
|
146
|
+
type: Output
|
|
147
|
+
}], connectDragMove: [{
|
|
148
|
+
type: Output
|
|
149
|
+
}], connectDragEnd: [{
|
|
150
|
+
type: Output
|
|
151
|
+
}], onMouseDown: [{
|
|
152
|
+
type: HostListener,
|
|
153
|
+
args: ['mousedown', ['$event']]
|
|
154
|
+
}] } });
|
|
155
|
+
|
|
156
|
+
class GraphMapperService {
|
|
157
|
+
registry;
|
|
158
|
+
constructor(registry) {
|
|
159
|
+
this.registry = registry;
|
|
160
|
+
}
|
|
161
|
+
/** Build nodes and edges for a given page definition and widgets. */
|
|
162
|
+
mapToGraph(page, widgets) {
|
|
163
|
+
const nodes = [];
|
|
164
|
+
const nodeById = new Map();
|
|
165
|
+
const tabsCfgByKey = new Map();
|
|
166
|
+
const edges = [];
|
|
167
|
+
// Layout: simple columns — sources left, targets right
|
|
168
|
+
const colWidth = 240;
|
|
169
|
+
const rowHeight = 96;
|
|
170
|
+
const gap = 28;
|
|
171
|
+
let leftY = 0;
|
|
172
|
+
let rightY = 0;
|
|
173
|
+
// First pass: create a node per widget
|
|
174
|
+
for (const w of widgets) {
|
|
175
|
+
const meta = this.registry.get(w.definition.id);
|
|
176
|
+
const metaOutputs = new Map();
|
|
177
|
+
for (const o of meta?.outputs || [])
|
|
178
|
+
metaOutputs.set(o.name, { label: o.label, description: o.description });
|
|
179
|
+
const outputs = new Set([(meta?.outputs || []).map((o) => o.name)].flat());
|
|
180
|
+
if (w.definition.id === 'praxis-tabs')
|
|
181
|
+
outputs.add('widgetEvent');
|
|
182
|
+
if (w.definition.id === 'praxis-stepper')
|
|
183
|
+
outputs.add('widgetEvent');
|
|
184
|
+
const metaInputs = new Map();
|
|
185
|
+
for (const i of meta?.inputs || [])
|
|
186
|
+
metaInputs.set(i.name, { label: i.label, description: i.description });
|
|
187
|
+
// Show both instance-defined inputs and metadata-declared inputs (e.g., resourceId)
|
|
188
|
+
const inputNames = Array.from(new Set([
|
|
189
|
+
...Object.keys(w.definition.inputs || {}),
|
|
190
|
+
...(meta?.inputs || []).map((m) => m.name),
|
|
191
|
+
]));
|
|
192
|
+
const inputPorts = inputNames.map((name, i) => ({
|
|
193
|
+
id: `in:${name}`,
|
|
194
|
+
label: metaInputs.get(name)?.label || name,
|
|
195
|
+
description: metaInputs.get(name)?.description,
|
|
196
|
+
kind: 'input',
|
|
197
|
+
anchor: { x: colWidth, y: 24 + i * 22 },
|
|
198
|
+
}));
|
|
199
|
+
// Generic port for new inputs
|
|
200
|
+
inputPorts.push({ id: 'in:*', label: 'novo input…', kind: 'input', anchor: { x: colWidth, y: 24 + inputNames.length * 22 } });
|
|
201
|
+
const outputPorts = Array.from(outputs.values()).map((name, i) => ({
|
|
202
|
+
id: `out:${name}`,
|
|
203
|
+
label: metaOutputs.get(name)?.label || name,
|
|
204
|
+
description: metaOutputs.get(name)?.description,
|
|
205
|
+
kind: 'output',
|
|
206
|
+
anchor: { x: 0, y: 24 + i * 22 },
|
|
207
|
+
}));
|
|
208
|
+
const left = outputPorts.length > 0;
|
|
209
|
+
const pos = left ? { x: gap, y: gap + leftY } : { x: colWidth + gap * 3, y: gap + rightY };
|
|
210
|
+
const node = {
|
|
211
|
+
id: w.key,
|
|
212
|
+
label: meta?.friendlyName || w.definition.id || w.key,
|
|
213
|
+
icon: meta?.icon,
|
|
214
|
+
type: w.definition.id,
|
|
215
|
+
parentId: null,
|
|
216
|
+
collapsed: true,
|
|
217
|
+
bounds: { x: pos.x, y: pos.y, width: colWidth, height: rowHeight },
|
|
218
|
+
ports: [...outputPorts.map((p) => ({ ...p, anchor: { x: pos.x, y: pos.y + p.anchor.y } })), ...inputPorts.map((p) => ({ ...p, anchor: { x: pos.x + colWidth, y: pos.y + p.anchor.y } }))],
|
|
219
|
+
};
|
|
220
|
+
nodes.push(node);
|
|
221
|
+
nodeById.set(node.id, node);
|
|
222
|
+
if (left)
|
|
223
|
+
leftY += rowHeight + gap;
|
|
224
|
+
else
|
|
225
|
+
rightY += rowHeight + gap;
|
|
226
|
+
// Expand Tabs internals as child nodes (collapsed by default)
|
|
227
|
+
if (w.definition.id === 'praxis-tabs') {
|
|
228
|
+
const cfg = w.definition?.inputs?.config;
|
|
229
|
+
tabsCfgByKey.set(w.key, cfg);
|
|
230
|
+
// Group tabs
|
|
231
|
+
const tabs = (cfg?.tabs || []);
|
|
232
|
+
for (let ti = 0; ti < tabs.length; ti++) {
|
|
233
|
+
const inner = tabs[ti]?.widgets || [];
|
|
234
|
+
for (let wi = 0; wi < inner.length; wi++) {
|
|
235
|
+
const innerDef = inner[wi];
|
|
236
|
+
const innerMeta = this.registry.get(innerDef?.id);
|
|
237
|
+
const innerInputs = Object.keys(innerDef?.inputs || {});
|
|
238
|
+
const innerMetaInputs = new Map();
|
|
239
|
+
for (const i of innerMeta?.inputs || [])
|
|
240
|
+
innerMetaInputs.set(i.name, { label: i.label, description: i.description });
|
|
241
|
+
const sectionLabel = cfg?.tabs?.[ti]?.textLabel;
|
|
242
|
+
const containerTitle = sectionLabel ? `Aba: ${sectionLabel}` : `Aba ${ti + 1}`;
|
|
243
|
+
const widgetLabel = innerMeta?.friendlyName || innerDef?.id || `widget ${wi}`;
|
|
244
|
+
const inPorts = innerInputs.map((name, ii) => {
|
|
245
|
+
const path = `inputs.config.tabs[${ti}].widgets[${wi}].inputs.${name}`;
|
|
246
|
+
const base = innerMetaInputs.get(name);
|
|
247
|
+
const label = base?.label || name;
|
|
248
|
+
const description = (base?.description ? base.description + '\n' : '') + `${containerTitle} • ${widgetLabel} • ${name}\n${path}`;
|
|
249
|
+
return { id: `in:${name}`, label, description, kind: 'input', path, anchor: { x: node.bounds.x + colWidth, y: node.bounds.y + rowHeight + (ti * 72) + ii * 18 } };
|
|
250
|
+
});
|
|
251
|
+
if (!inPorts.length)
|
|
252
|
+
inPorts.push({ id: 'in:*', label: 'novo input…', kind: 'input', path: `inputs.config.tabs[${ti}].widgets[${wi}].inputs.`, anchor: { x: node.bounds.x + colWidth, y: node.bounds.y + rowHeight + (ti * 72) } });
|
|
253
|
+
const child = {
|
|
254
|
+
id: `${w.key}#group[${ti}]/${wi}`,
|
|
255
|
+
label: innerMeta?.friendlyName || innerDef?.id || `widget ${wi}`,
|
|
256
|
+
icon: innerMeta?.icon,
|
|
257
|
+
type: innerDef?.id || 'inner',
|
|
258
|
+
parentId: w.key,
|
|
259
|
+
collapsed: false,
|
|
260
|
+
bounds: { x: node.bounds.x + 24, y: node.bounds.y + rowHeight + (ti * 72) + wi * 32, width: colWidth - 48, height: 48 },
|
|
261
|
+
ports: inPorts,
|
|
262
|
+
};
|
|
263
|
+
nodes.push(child);
|
|
264
|
+
nodeById.set(child.id, child);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Nav links
|
|
268
|
+
const links = (cfg?.nav?.links || []);
|
|
269
|
+
for (let li = 0; li < links.length; li++) {
|
|
270
|
+
const inner = links[li]?.widgets || [];
|
|
271
|
+
for (let wi = 0; wi < inner.length; wi++) {
|
|
272
|
+
const innerDef = inner[wi];
|
|
273
|
+
const innerMeta = this.registry.get(innerDef?.id);
|
|
274
|
+
const innerInputs = Object.keys(innerDef?.inputs || {});
|
|
275
|
+
const innerMetaInputs = new Map();
|
|
276
|
+
for (const i of innerMeta?.inputs || [])
|
|
277
|
+
innerMetaInputs.set(i.name, { label: i.label, description: i.description });
|
|
278
|
+
const linkLabel = cfg?.nav?.links?.[li]?.label;
|
|
279
|
+
const containerTitle = linkLabel ? `Link: ${linkLabel}` : `Link ${li + 1}`;
|
|
280
|
+
const widgetLabel = innerMeta?.friendlyName || innerDef?.id || `widget ${wi}`;
|
|
281
|
+
const inPorts = innerInputs.map((name, ii) => {
|
|
282
|
+
const path = `inputs.config.nav.links[${li}].widgets[${wi}].inputs.${name}`;
|
|
283
|
+
const base = innerMetaInputs.get(name);
|
|
284
|
+
const label = base?.label || name;
|
|
285
|
+
const description = (base?.description ? base.description + '\n' : '') + `${containerTitle} • ${widgetLabel} • ${name}\n${path}`;
|
|
286
|
+
return { id: `in:${name}`, label, description, kind: 'input', path, anchor: { x: node.bounds.x + colWidth, y: node.bounds.y + rowHeight + (li * 72) + ii * 18 } };
|
|
287
|
+
});
|
|
288
|
+
if (!inPorts.length)
|
|
289
|
+
inPorts.push({ id: 'in:*', label: 'novo input…', kind: 'input', path: `inputs.config.nav.links[${li}].widgets[${wi}].inputs.`, anchor: { x: node.bounds.x + colWidth, y: node.bounds.y + rowHeight + (li * 72) } });
|
|
290
|
+
const child = {
|
|
291
|
+
id: `${w.key}#nav[${li}]/${wi}`,
|
|
292
|
+
label: innerMeta?.friendlyName || innerDef?.id || `link ${wi}`,
|
|
293
|
+
icon: innerMeta?.icon,
|
|
294
|
+
type: innerDef?.id || 'inner',
|
|
295
|
+
parentId: w.key,
|
|
296
|
+
collapsed: false,
|
|
297
|
+
bounds: { x: node.bounds.x + 24, y: node.bounds.y + rowHeight + (li * 72) + wi * 32, width: colWidth - 48, height: 48 },
|
|
298
|
+
ports: inPorts,
|
|
299
|
+
};
|
|
300
|
+
nodes.push(child);
|
|
301
|
+
nodeById.set(child.id, child);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Second pass: map edges from page.connections
|
|
307
|
+
const conns = (page?.connections || []);
|
|
308
|
+
for (let i = 0; i < conns.length; i++) {
|
|
309
|
+
const c = conns[i];
|
|
310
|
+
const fromNodeId = c.from.widget;
|
|
311
|
+
const fromPortId = `out:${c.from.output}`;
|
|
312
|
+
let toNodeId = c.to.widget;
|
|
313
|
+
let toPortId = `in:${c.to.input}`;
|
|
314
|
+
const deep = this.parseTabsPath(c.to.input);
|
|
315
|
+
let friendlyPath;
|
|
316
|
+
if (deep) {
|
|
317
|
+
const childId = `${c.to.widget}#${deep.kind}[${deep.index}]/${deep.widgetIndex}`;
|
|
318
|
+
// If such child exists, route edge to it and its input name
|
|
319
|
+
if (nodes.some((n) => n.id === childId)) {
|
|
320
|
+
toNodeId = childId;
|
|
321
|
+
toPortId = `in:${deep.input}`;
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
// Keep to widget but still expose label/path
|
|
325
|
+
toPortId = `in:${deep.input}`;
|
|
326
|
+
}
|
|
327
|
+
const cfg = tabsCfgByKey.get(c.to.widget);
|
|
328
|
+
const labelFromCfg = deep.kind === 'group' ? cfg?.tabs?.[deep.index]?.textLabel : cfg?.nav?.links?.[deep.index]?.label;
|
|
329
|
+
const containerLabel = labelFromCfg ? (deep.kind === 'group' ? `Aba: ${labelFromCfg}` : `Link: ${labelFromCfg}`) : (deep.kind === 'group' ? `Aba ${deep.index + 1}` : `Link ${deep.index + 1}`);
|
|
330
|
+
const child = nodes.find(n => n.id === childId);
|
|
331
|
+
friendlyPath = `${containerLabel} • ${(child?.label || 'Widget')} • ${deep.input}`;
|
|
332
|
+
}
|
|
333
|
+
// Ensure ports exist for visualization (create synthetic when missing)
|
|
334
|
+
this.ensurePort(nodeById, fromNodeId, fromPortId, 'output', colWidth);
|
|
335
|
+
this.ensurePort(nodeById, toNodeId, toPortId, 'input', colWidth);
|
|
336
|
+
const edge = {
|
|
337
|
+
id: `e${i}`,
|
|
338
|
+
from: { nodeId: fromNodeId, portId: fromPortId },
|
|
339
|
+
to: { nodeId: toNodeId, portId: toPortId },
|
|
340
|
+
label: c.map || undefined,
|
|
341
|
+
meta: { map: c.map, bindingOrder: c.to.bindingOrder, ...(friendlyPath ? { friendlyPath } : {}) },
|
|
342
|
+
};
|
|
343
|
+
edges.push(edge);
|
|
344
|
+
}
|
|
345
|
+
return { nodes, edges };
|
|
346
|
+
}
|
|
347
|
+
ensurePort(nodeById, nodeId, portId, kind, colWidth) {
|
|
348
|
+
const n = nodeById.get(nodeId);
|
|
349
|
+
if (!n)
|
|
350
|
+
return;
|
|
351
|
+
const exists = (n.ports || []).some(p => p.id === portId);
|
|
352
|
+
if (exists)
|
|
353
|
+
return;
|
|
354
|
+
const label = portId.replace(/^(in:|out:)/, '');
|
|
355
|
+
const portsOfKind = (n.ports || []).filter(p => p.kind === kind);
|
|
356
|
+
const idx = portsOfKind.length;
|
|
357
|
+
const y = n.bounds.y + 24 + idx * 22;
|
|
358
|
+
const x = kind === 'output' ? n.bounds.x : n.bounds.x + n.bounds.width;
|
|
359
|
+
const p = { id: portId, label, kind, anchor: { x, y } };
|
|
360
|
+
n.ports = [...(n.ports || []), p];
|
|
361
|
+
// Optionally grow node height to accommodate more ports
|
|
362
|
+
const approxBottom = y + 28 - n.bounds.y;
|
|
363
|
+
if (approxBottom > n.bounds.height)
|
|
364
|
+
n.bounds = { ...n.bounds, height: approxBottom };
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Parse dot-path for Tabs internals. Supported:
|
|
368
|
+
* - inputs.config.tabs[<i>].widgets[<j>].inputs.<input>
|
|
369
|
+
* - inputs.config.nav.links[<i>].widgets[<j>].inputs.<input>
|
|
370
|
+
*/
|
|
371
|
+
parseTabsPath(path) {
|
|
372
|
+
if (!path)
|
|
373
|
+
return null;
|
|
374
|
+
const groupRe = /^inputs\.config\.tabs\[(\d+)\]\.widgets\[(\d+)\]\.inputs\.(.+)$/;
|
|
375
|
+
const navRe = /^inputs\.config\.nav\.links\[(\d+)\]\.widgets\[(\d+)\]\.inputs\.(.+)$/;
|
|
376
|
+
const g = path.match(groupRe);
|
|
377
|
+
if (g)
|
|
378
|
+
return { kind: 'group', index: Number(g[1]), widgetIndex: Number(g[2]), input: g[3] };
|
|
379
|
+
const n = path.match(navRe);
|
|
380
|
+
if (n)
|
|
381
|
+
return { kind: 'nav', index: Number(n[1]), widgetIndex: Number(n[2]), input: n[3] };
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: GraphMapperService, deps: [{ token: i2.ComponentMetadataRegistry }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
385
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: GraphMapperService, providedIn: 'root' });
|
|
386
|
+
}
|
|
387
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: GraphMapperService, decorators: [{
|
|
388
|
+
type: Injectable,
|
|
389
|
+
args: [{ providedIn: 'root' }]
|
|
390
|
+
}], ctorParameters: () => [{ type: i2.ComponentMetadataRegistry }] });
|
|
391
|
+
|
|
392
|
+
class ConnectionGraphComponent {
|
|
393
|
+
mapper;
|
|
394
|
+
settingsPanel;
|
|
395
|
+
injectedData;
|
|
396
|
+
page;
|
|
397
|
+
widgets;
|
|
398
|
+
pageChange = new EventEmitter();
|
|
399
|
+
nodes = signal([], ...(ngDevMode ? [{ debugName: "nodes" }] : []));
|
|
400
|
+
edges = signal([], ...(ngDevMode ? [{ debugName: "edges" }] : []));
|
|
401
|
+
connections = signal([], ...(ngDevMode ? [{ debugName: "connections" }] : []));
|
|
402
|
+
showPortLabels = true;
|
|
403
|
+
showEdgeLabels = false;
|
|
404
|
+
showUnconnectedPorts = true;
|
|
405
|
+
showDetails = true;
|
|
406
|
+
showFriendlyLabels = true;
|
|
407
|
+
selectedEdgeIndex = signal(-1, ...(ngDevMode ? [{ debugName: "selectedEdgeIndex" }] : []));
|
|
408
|
+
// Panning/zoom
|
|
409
|
+
pan = { x: 0, y: 0 };
|
|
410
|
+
zoom = 1;
|
|
411
|
+
transform = signal('', ...(ngDevMode ? [{ debugName: "transform" }] : []));
|
|
412
|
+
// Hover state
|
|
413
|
+
hoverEdgeId = '';
|
|
414
|
+
// Drag state
|
|
415
|
+
dragFrom;
|
|
416
|
+
dragPath = signal('', ...(ngDevMode ? [{ debugName: "dragPath" }] : []));
|
|
417
|
+
// Pending popover
|
|
418
|
+
pendingPopover = signal(null, ...(ngDevMode ? [{ debugName: "pendingPopover" }] : []));
|
|
419
|
+
// Settings Panel integration
|
|
420
|
+
isDirty$ = new BehaviorSubject(false);
|
|
421
|
+
isValid$ = new BehaviorSubject(true);
|
|
422
|
+
isBusy$ = new BehaviorSubject(false);
|
|
423
|
+
initialSnapshot = '[]';
|
|
424
|
+
placeholderSample = 'payload | payload.id | ${payload.id}';
|
|
425
|
+
connectedPorts = new Map();
|
|
426
|
+
isPortConnected(nodeId, portId) {
|
|
427
|
+
const set = this.connectedPorts.get(nodeId);
|
|
428
|
+
return !!set && set.has(portId);
|
|
429
|
+
}
|
|
430
|
+
outputPorts(n) {
|
|
431
|
+
const ports = (n.ports || []).filter((p) => p.kind === 'output');
|
|
432
|
+
return this.showUnconnectedPorts ? ports : ports.filter(p => this.isPortConnected(n.id, p.id));
|
|
433
|
+
}
|
|
434
|
+
inputPorts(n) {
|
|
435
|
+
const ports = (n.ports || []).filter((p) => p.kind === 'input');
|
|
436
|
+
return this.showUnconnectedPorts ? ports : ports.filter(p => this.isPortConnected(n.id, p.id));
|
|
437
|
+
}
|
|
438
|
+
constructor(mapper, settingsPanel, injectedData) {
|
|
439
|
+
this.mapper = mapper;
|
|
440
|
+
this.settingsPanel = settingsPanel;
|
|
441
|
+
this.injectedData = injectedData;
|
|
442
|
+
}
|
|
443
|
+
ngOnInit() {
|
|
444
|
+
const data = this.injectedData || {};
|
|
445
|
+
const p = this.parsePage(data.page);
|
|
446
|
+
const ws = data.widgets || this.widgets || p?.widgets || [];
|
|
447
|
+
this.widgets = ws;
|
|
448
|
+
this.page = data.page || this.page;
|
|
449
|
+
const conns = [...(p?.connections || [])];
|
|
450
|
+
this.connections.set(conns);
|
|
451
|
+
this.initialSnapshot = JSON.stringify(conns);
|
|
452
|
+
this.rebuildGraph();
|
|
453
|
+
}
|
|
454
|
+
ngOnChanges(changes) {
|
|
455
|
+
if (changes['page'] || changes['widgets']) {
|
|
456
|
+
const p = this.parsePage(this.page);
|
|
457
|
+
this.connections.set([...(p?.connections || [])]);
|
|
458
|
+
this.rebuildGraph();
|
|
459
|
+
this.initialSnapshot = JSON.stringify(this.connections());
|
|
460
|
+
this.isDirty$.next(false);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
parsePage(input) {
|
|
464
|
+
if (!input)
|
|
465
|
+
return undefined;
|
|
466
|
+
if (typeof input === 'string') {
|
|
467
|
+
try {
|
|
468
|
+
return JSON.parse(input);
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return input;
|
|
475
|
+
}
|
|
476
|
+
rebuildGraph() {
|
|
477
|
+
const p = this.parsePage(this.page) || { widgets: this.widgets || [], connections: this.connections() };
|
|
478
|
+
const { nodes, edges } = this.mapper.mapToGraph({ ...p, connections: this.connections() }, this.widgets || []);
|
|
479
|
+
this.nodes.set(nodes);
|
|
480
|
+
this.edges.set(edges);
|
|
481
|
+
this.recomputeConnectedPorts();
|
|
482
|
+
this.updateTransform();
|
|
483
|
+
}
|
|
484
|
+
recomputeConnectedPorts() {
|
|
485
|
+
const map = new Map();
|
|
486
|
+
const add = (nodeId, portId) => {
|
|
487
|
+
const s = map.get(nodeId) || new Set();
|
|
488
|
+
s.add(portId);
|
|
489
|
+
map.set(nodeId, s);
|
|
490
|
+
};
|
|
491
|
+
for (const e of this.edges()) {
|
|
492
|
+
add(e.from.nodeId, e.from.portId);
|
|
493
|
+
add(e.to.nodeId, e.to.portId);
|
|
494
|
+
}
|
|
495
|
+
this.connectedPorts = map;
|
|
496
|
+
}
|
|
497
|
+
// External focus by connection (from Builder or other callers)
|
|
498
|
+
focusConnection(conn) {
|
|
499
|
+
const fromNodeId = conn.from.widget;
|
|
500
|
+
const fromPortId = `out:${conn.from.output}`;
|
|
501
|
+
let toNodeId = conn.to.widget;
|
|
502
|
+
let toPortId = `in:${conn.to.input}`;
|
|
503
|
+
const deep = this.parseTabsPath(conn.to.input);
|
|
504
|
+
if (deep) {
|
|
505
|
+
const childId = `${conn.to.widget}#${deep.kind}[${deep.index}]/${deep.widgetIndex}`;
|
|
506
|
+
if (this.nodes().some(n => n.id === childId)) {
|
|
507
|
+
toNodeId = childId;
|
|
508
|
+
toPortId = `in:${deep.input}`;
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
toPortId = `in:${deep.input}`;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const idx = this.edges().findIndex(e => e.from.nodeId === fromNodeId && e.from.portId === fromPortId && e.to.nodeId === toNodeId && e.to.portId === toPortId);
|
|
515
|
+
if (idx >= 0)
|
|
516
|
+
this.selectEdge(idx);
|
|
517
|
+
}
|
|
518
|
+
parseTabsPath(path) {
|
|
519
|
+
if (!path)
|
|
520
|
+
return null;
|
|
521
|
+
const g = path.match(/^inputs\.config\.tabs\[(\d+)\]\.widgets\[(\d+)\]\.inputs\.(.+)$/);
|
|
522
|
+
if (g)
|
|
523
|
+
return { kind: 'group', index: Number(g[1]), widgetIndex: Number(g[2]), input: g[3] };
|
|
524
|
+
const n = path.match(/^inputs\.config\.nav\.links\[(\d+)\]\.widgets\[(\d+)\]\.inputs\.(.+)$/);
|
|
525
|
+
if (n)
|
|
526
|
+
return { kind: 'nav', index: Number(n[1]), widgetIndex: Number(n[2]), input: n[3] };
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
// Pan/Zoom handlers
|
|
530
|
+
onWheel(ev) {
|
|
531
|
+
ev.preventDefault();
|
|
532
|
+
const delta = Math.sign(ev.deltaY) * -0.05; // invert: wheel down zoom out
|
|
533
|
+
this.zoom = Math.min(2, Math.max(0.5, this.zoom + delta));
|
|
534
|
+
this.updateTransform();
|
|
535
|
+
}
|
|
536
|
+
panning = false;
|
|
537
|
+
panStart = { x: 0, y: 0 };
|
|
538
|
+
onPanStart(ev) {
|
|
539
|
+
if (ev.target.closest('.port-hit'))
|
|
540
|
+
return; // don't pan when starting on port
|
|
541
|
+
this.panning = true;
|
|
542
|
+
this.panStart = { x: ev.clientX - this.pan.x, y: ev.clientY - this.pan.y };
|
|
543
|
+
const onMove = (e) => this.onPanMove(e);
|
|
544
|
+
const onUp = () => { this.panning = false; document.removeEventListener('mousemove', onMove); };
|
|
545
|
+
document.addEventListener('mousemove', onMove);
|
|
546
|
+
document.addEventListener('mouseup', onUp, { once: true });
|
|
547
|
+
}
|
|
548
|
+
onPanMove(ev) {
|
|
549
|
+
if (!this.panning)
|
|
550
|
+
return;
|
|
551
|
+
this.pan = { x: ev.clientX - this.panStart.x, y: ev.clientY - this.panStart.y };
|
|
552
|
+
this.updateTransform();
|
|
553
|
+
}
|
|
554
|
+
updateTransform() {
|
|
555
|
+
this.transform.set(`translate(${this.pan.x},${this.pan.y}) scale(${this.zoom})`);
|
|
556
|
+
}
|
|
557
|
+
// Edge/path helpers
|
|
558
|
+
edgePath(e) {
|
|
559
|
+
const from = this.findPortAnchor(e.from.nodeId, e.from.portId);
|
|
560
|
+
const to = this.findPortAnchor(e.to.nodeId, e.to.portId);
|
|
561
|
+
return routeElbow(from, to);
|
|
562
|
+
}
|
|
563
|
+
edgeTooltip(e) {
|
|
564
|
+
const toNode = this.nodes().find(n => n.id === e.to.nodeId);
|
|
565
|
+
const fromNode = this.nodes().find(n => n.id === e.from.nodeId);
|
|
566
|
+
const parts = [
|
|
567
|
+
`De: ${fromNode?.label || e.from.nodeId}.${e.from.portId.replace('out:', '')}`,
|
|
568
|
+
`Para: ${toNode?.label || e.to.nodeId}.${e.to.portId.replace('in:', '')}`,
|
|
569
|
+
];
|
|
570
|
+
if (e.meta && e.meta.friendlyPath)
|
|
571
|
+
parts.push(`Destino: ${e.meta.friendlyPath}`);
|
|
572
|
+
if (e.meta?.map)
|
|
573
|
+
parts.push(`map: ${e.meta.map}`);
|
|
574
|
+
return parts.join('\n');
|
|
575
|
+
}
|
|
576
|
+
// Port label/tooltip helpers
|
|
577
|
+
technicalName(p) {
|
|
578
|
+
const fromId = p.id?.replace(/^in:|^out:/, '') || '';
|
|
579
|
+
if (fromId)
|
|
580
|
+
return fromId;
|
|
581
|
+
if (p.path) {
|
|
582
|
+
const m = p.path.match(/\.inputs\.([^\.\[]+)$/);
|
|
583
|
+
if (m)
|
|
584
|
+
return m[1];
|
|
585
|
+
}
|
|
586
|
+
return p.label || '';
|
|
587
|
+
}
|
|
588
|
+
portText(p) { return this.showFriendlyLabels ? (p.label || this.technicalName(p)) : this.technicalName(p); }
|
|
589
|
+
portTooltip(p) {
|
|
590
|
+
const tech = this.technicalName(p);
|
|
591
|
+
const friendly = p.label || tech;
|
|
592
|
+
const desc = p.description ? `\n${p.description}` : '';
|
|
593
|
+
if (this.showFriendlyLabels)
|
|
594
|
+
return `${friendly}${desc}`;
|
|
595
|
+
// when showing technical, include friendly for contexto if different
|
|
596
|
+
return friendly !== tech ? `${tech} \n(${friendly})${desc}` : `${tech}${desc}`;
|
|
597
|
+
}
|
|
598
|
+
edgeMid(e) {
|
|
599
|
+
const from = this.findPortAnchor(e.from.nodeId, e.from.portId);
|
|
600
|
+
const to = this.findPortAnchor(e.to.nodeId, e.to.portId);
|
|
601
|
+
return { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 };
|
|
602
|
+
}
|
|
603
|
+
findPortAnchor(nodeId, portId) {
|
|
604
|
+
const n = this.nodes().find((x) => x.id === nodeId);
|
|
605
|
+
if (!n)
|
|
606
|
+
return { x: 0, y: 0 };
|
|
607
|
+
const p = n.ports.find((pp) => pp.id === portId);
|
|
608
|
+
return p?.anchor || { x: n.bounds.x, y: n.bounds.y };
|
|
609
|
+
}
|
|
610
|
+
// Drag connect flow
|
|
611
|
+
onDragStart(ev) {
|
|
612
|
+
this.dragFrom = ev.from;
|
|
613
|
+
this.dragPath.set(`M ${ev.x} ${ev.y} L ${ev.x} ${ev.y}`);
|
|
614
|
+
try {
|
|
615
|
+
console.debug('[ConnectionGraph] drag:start', { from: ev.from, at: { x: ev.x, y: ev.y } });
|
|
616
|
+
}
|
|
617
|
+
catch { }
|
|
618
|
+
}
|
|
619
|
+
onDragMove(ev) {
|
|
620
|
+
if (!this.dragFrom)
|
|
621
|
+
return;
|
|
622
|
+
const from = this.findPortAnchor(this.dragFrom.nodeId, this.dragFrom.portId);
|
|
623
|
+
this.dragPath.set(routeElbow(from, { x: ev.x, y: ev.y }));
|
|
624
|
+
try {
|
|
625
|
+
console.debug('[ConnectionGraph] drag:move', { to: { x: ev.x, y: ev.y } });
|
|
626
|
+
}
|
|
627
|
+
catch { }
|
|
628
|
+
}
|
|
629
|
+
onDragEnd(ev) {
|
|
630
|
+
if (!this.dragFrom)
|
|
631
|
+
return;
|
|
632
|
+
this.dragPath.set('');
|
|
633
|
+
// Find target input port element (robust: handle circle or its container)
|
|
634
|
+
const endEl = ev.endEvent.target;
|
|
635
|
+
const targetInfo = { tag: endEl?.tagName, class: endEl?.className };
|
|
636
|
+
let circleEl = endEl?.closest?.('.port.input circle');
|
|
637
|
+
if (!circleEl) {
|
|
638
|
+
const portGroup = endEl?.closest?.('.port.input');
|
|
639
|
+
if (portGroup)
|
|
640
|
+
circleEl = portGroup.querySelector('circle') || null;
|
|
641
|
+
}
|
|
642
|
+
if (!circleEl && endEl && endEl.getAttribute) {
|
|
643
|
+
const hasData = Boolean(endEl.getAttribute('data-port-id'));
|
|
644
|
+
if (hasData)
|
|
645
|
+
circleEl = endEl;
|
|
646
|
+
}
|
|
647
|
+
if (!circleEl) {
|
|
648
|
+
try {
|
|
649
|
+
console.warn('[ConnectionGraph] drag:end — no target port found', targetInfo);
|
|
650
|
+
}
|
|
651
|
+
catch { }
|
|
652
|
+
this.dragFrom = undefined;
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const toNodeId = circleEl.getAttribute('data-node-id') || '';
|
|
656
|
+
const toPortId = circleEl.getAttribute('data-port-id') || '';
|
|
657
|
+
const path = circleEl.getAttribute('data-port-path') || undefined;
|
|
658
|
+
try {
|
|
659
|
+
console.debug('[ConnectionGraph] drag:end found target', { toNodeId, toPortId, path, at: { x: ev.x, y: ev.y } });
|
|
660
|
+
}
|
|
661
|
+
catch { }
|
|
662
|
+
this.pendingPopover.set({ x: ev.x, y: ev.y, from: this.dragFrom, to: { nodeId: toNodeId, portId: toPortId, path }, map: 'payload' });
|
|
663
|
+
try {
|
|
664
|
+
console.debug('[ConnectionGraph] popover:open', this.pendingPopover());
|
|
665
|
+
}
|
|
666
|
+
catch { }
|
|
667
|
+
this.dragFrom = undefined;
|
|
668
|
+
}
|
|
669
|
+
cancelPending() { this.pendingPopover.set(null); }
|
|
670
|
+
confirmPending() {
|
|
671
|
+
const pop = this.pendingPopover();
|
|
672
|
+
if (!pop)
|
|
673
|
+
return;
|
|
674
|
+
const fromOutput = pop.from.portId.replace(/^out:/, '');
|
|
675
|
+
const toInputName = pop.to.portId.replace(/^in:/, '');
|
|
676
|
+
const toPath = pop.to.path || toInputName;
|
|
677
|
+
const newConn = {
|
|
678
|
+
from: { widget: pop.from.nodeId, output: fromOutput },
|
|
679
|
+
to: { widget: this.resolveToWidget(pop.to.nodeId), input: toPath },
|
|
680
|
+
map: pop.map,
|
|
681
|
+
meta: {
|
|
682
|
+
filterExpr: pop.filter || undefined,
|
|
683
|
+
debounceMs: pop.debounceMs || undefined,
|
|
684
|
+
distinct: pop.distinct || undefined,
|
|
685
|
+
distinctBy: pop.distinctBy || undefined,
|
|
686
|
+
},
|
|
687
|
+
};
|
|
688
|
+
const next = [...this.connections(), newConn];
|
|
689
|
+
try {
|
|
690
|
+
console.debug('[ConnectionGraph] connection:add', newConn);
|
|
691
|
+
}
|
|
692
|
+
catch { }
|
|
693
|
+
this.connections.set(next);
|
|
694
|
+
this.pendingPopover.set(null);
|
|
695
|
+
try {
|
|
696
|
+
console.debug('[ConnectionGraph] popover:close');
|
|
697
|
+
}
|
|
698
|
+
catch { }
|
|
699
|
+
this.rebuildGraph();
|
|
700
|
+
this.emitPageChange();
|
|
701
|
+
this.updateDirty(next);
|
|
702
|
+
}
|
|
703
|
+
applyPreset(map) {
|
|
704
|
+
const pop = this.pendingPopover();
|
|
705
|
+
if (!pop)
|
|
706
|
+
return;
|
|
707
|
+
this.pendingPopover.set({ ...pop, map });
|
|
708
|
+
}
|
|
709
|
+
computePreview(map) {
|
|
710
|
+
try {
|
|
711
|
+
const sample = { payload: { id: 123, data: { id: 123, name: 'Exemplo' }, row: { id: 123 } } };
|
|
712
|
+
const path = (map || 'payload').split('.');
|
|
713
|
+
let cur = sample;
|
|
714
|
+
for (const k of path) {
|
|
715
|
+
if (!k)
|
|
716
|
+
continue;
|
|
717
|
+
cur = cur?.[k];
|
|
718
|
+
}
|
|
719
|
+
return JSON.stringify(cur);
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
return '—';
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
computeFilterPreview(expr) {
|
|
726
|
+
try {
|
|
727
|
+
if (!expr)
|
|
728
|
+
return '—';
|
|
729
|
+
const sample = { payload: { id: 123, data: { id: 123, name: 'Exemplo' }, row: { id: 123 } } };
|
|
730
|
+
if (typeof expr === 'string' && expr.trim().startsWith('=')) {
|
|
731
|
+
const code = expr.trim().slice(1);
|
|
732
|
+
// Controlled eval limited to sample context
|
|
733
|
+
// eslint-disable-next-line no-new-func
|
|
734
|
+
const fn = new Function('payload', `"use strict"; return (${code});`);
|
|
735
|
+
const ok = !!fn(sample.payload);
|
|
736
|
+
return ok ? 'pass' : 'skip';
|
|
737
|
+
}
|
|
738
|
+
return '—';
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
return 'erro';
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
resolveToWidget(nodeId) {
|
|
745
|
+
// Child nodes are encoded as parent#kind[index]/widgetIndex
|
|
746
|
+
const hash = nodeId.indexOf('#');
|
|
747
|
+
if (hash > 0)
|
|
748
|
+
return nodeId.slice(0, hash);
|
|
749
|
+
return nodeId;
|
|
750
|
+
}
|
|
751
|
+
removeEdge(index) {
|
|
752
|
+
const next = this.connections().slice();
|
|
753
|
+
next.splice(index, 1);
|
|
754
|
+
this.connections.set(next);
|
|
755
|
+
this.rebuildGraph();
|
|
756
|
+
this.emitPageChange();
|
|
757
|
+
this.updateDirty(next);
|
|
758
|
+
}
|
|
759
|
+
// Select edge and open details
|
|
760
|
+
selectEdge(index) {
|
|
761
|
+
this.selectedEdgeIndex.set(index);
|
|
762
|
+
this.showDetails = true;
|
|
763
|
+
}
|
|
764
|
+
currentConn() { const i = this.selectedEdgeIndex(); return (i >= 0 ? this.connections()[i] : undefined); }
|
|
765
|
+
emitPageChange() {
|
|
766
|
+
const parsed = this.parsePage(this.page) || { widgets: this.widgets || [] };
|
|
767
|
+
const updated = { ...parsed, connections: this.connections() };
|
|
768
|
+
this.pageChange.emit(updated);
|
|
769
|
+
}
|
|
770
|
+
// Settings panel value provider methods
|
|
771
|
+
getSettingsValue() { return { page: { ...(this.parsePage(this.page) || {}), connections: this.connections() } }; }
|
|
772
|
+
reset() {
|
|
773
|
+
try {
|
|
774
|
+
this.connections.set(JSON.parse(this.initialSnapshot || '[]'));
|
|
775
|
+
}
|
|
776
|
+
catch {
|
|
777
|
+
this.connections.set([]);
|
|
778
|
+
}
|
|
779
|
+
this.rebuildGraph();
|
|
780
|
+
this.isDirty$.next(false);
|
|
781
|
+
this.isValid$.next(true);
|
|
782
|
+
}
|
|
783
|
+
// Node dragging
|
|
784
|
+
nodeDrag = { active: false, index: -1, startX: 0, startY: 0, origX: 0, origY: 0 };
|
|
785
|
+
onNodeDragStart(ev, index) {
|
|
786
|
+
ev.preventDefault();
|
|
787
|
+
ev.stopPropagation();
|
|
788
|
+
const n = this.nodes()[index];
|
|
789
|
+
if (!n)
|
|
790
|
+
return;
|
|
791
|
+
this.nodeDrag = { active: true, index, startX: ev.clientX, startY: ev.clientY, origX: n.bounds.x, origY: n.bounds.y };
|
|
792
|
+
const onMove = (e) => this.onNodeDragMove(e);
|
|
793
|
+
const onUp = () => { this.nodeDrag.active = false; document.removeEventListener('mousemove', onMove); };
|
|
794
|
+
document.addEventListener('mousemove', onMove);
|
|
795
|
+
document.addEventListener('mouseup', onUp, { once: true });
|
|
796
|
+
}
|
|
797
|
+
onNodeDragMove(ev) {
|
|
798
|
+
if (!this.nodeDrag.active)
|
|
799
|
+
return;
|
|
800
|
+
const dx = (ev.clientX - this.nodeDrag.startX) / this.zoom;
|
|
801
|
+
const dy = (ev.clientY - this.nodeDrag.startY) / this.zoom;
|
|
802
|
+
const nodes = this.nodes().slice();
|
|
803
|
+
const n = { ...nodes[this.nodeDrag.index] };
|
|
804
|
+
const newX = this.nodeDrag.origX + dx;
|
|
805
|
+
const newY = this.nodeDrag.origY + dy;
|
|
806
|
+
const ddx = newX - n.bounds.x;
|
|
807
|
+
const ddy = newY - n.bounds.y;
|
|
808
|
+
n.bounds = { ...n.bounds, x: newX, y: newY };
|
|
809
|
+
// Shift port anchors by delta
|
|
810
|
+
n.ports = (n.ports || []).map(p => ({ ...p, anchor: { x: p.anchor.x + ddx, y: p.anchor.y + ddy } }));
|
|
811
|
+
nodes[this.nodeDrag.index] = n;
|
|
812
|
+
this.nodes.set(nodes);
|
|
813
|
+
}
|
|
814
|
+
trackPort(_idx, p) { return p.id; }
|
|
815
|
+
updateDirty(next) {
|
|
816
|
+
this.isDirty$.next(JSON.stringify(next) !== this.initialSnapshot);
|
|
817
|
+
this.isValid$.next(true);
|
|
818
|
+
}
|
|
819
|
+
// Builder integration
|
|
820
|
+
openBuilder() {
|
|
821
|
+
const base = this.parsePage(this.page) || { widgets: this.widgets || [] };
|
|
822
|
+
const p = { ...base, connections: this.connections() };
|
|
823
|
+
try {
|
|
824
|
+
console.debug('[ConnectionGraph] openBuilder()', { baseHasConns: !!base.connections, graphConns: this.connections().length, widgets: this.widgets?.length });
|
|
825
|
+
}
|
|
826
|
+
catch { }
|
|
827
|
+
const ref = this.settingsPanel.open({ id: 'grid-connections', title: 'Conexões (Builder)', content: { component: ConnectionBuilderComponent, inputs: { page: p, widgets: this.widgets } } });
|
|
828
|
+
ref.applied$.subscribe((val) => { const next = (val && val.page) ? val.page : undefined; if (next)
|
|
829
|
+
this.applyConnections(next); });
|
|
830
|
+
ref.saved$.subscribe((val) => { const next = (val && val.page) ? val.page : undefined; if (next)
|
|
831
|
+
this.applyConnections(next); });
|
|
832
|
+
}
|
|
833
|
+
applyConnections(next) {
|
|
834
|
+
const conns = [...(next.connections || [])];
|
|
835
|
+
try {
|
|
836
|
+
console.debug('[ConnectionGraph] applyConnections()', { conns: conns.length });
|
|
837
|
+
}
|
|
838
|
+
catch { }
|
|
839
|
+
this.connections.set(conns);
|
|
840
|
+
this.rebuildGraph();
|
|
841
|
+
this.emitPageChange();
|
|
842
|
+
this.initialSnapshot = JSON.stringify(conns);
|
|
843
|
+
this.isDirty$.next(false);
|
|
844
|
+
}
|
|
845
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: ConnectionGraphComponent, deps: [{ token: GraphMapperService }, { token: i2$1.SettingsPanelService }, { token: SETTINGS_PANEL_DATA, optional: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
846
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: ConnectionGraphComponent, isStandalone: true, selector: "praxis-connection-graph", inputs: { page: "page", widgets: "widgets" }, outputs: { pageChange: "pageChange" }, usesOnChanges: true, ngImport: i0, template: `
|
|
847
|
+
<div class="cg-root" (wheel)="onWheel($event)" (mousedown)="onPanStart($event)">
|
|
848
|
+
<div class="cg-head">
|
|
849
|
+
<div class="cg-title">Conexões</div>
|
|
850
|
+
<div class="spacer"></div>
|
|
851
|
+
<button mat-button [color]="showPortLabels ? 'primary' : undefined" (click)="showPortLabels = !showPortLabels" matTooltip="Mostrar/ocultar labels de portas" aria-label="Mostrar/ocultar labels de portas">
|
|
852
|
+
<mat-icon>label</mat-icon>
|
|
853
|
+
</button>
|
|
854
|
+
<button mat-button [color]="showFriendlyLabels ? 'primary' : undefined" (click)="showFriendlyLabels = !showFriendlyLabels" matTooltip="Alternar nome amigável/técnico" aria-label="Alternar nome amigável/técnico">
|
|
855
|
+
<mat-icon>translate</mat-icon>
|
|
856
|
+
</button>
|
|
857
|
+
<button mat-button [color]="showEdgeLabels ? 'primary' : undefined" (click)="showEdgeLabels = !showEdgeLabels" matTooltip="Mostrar/ocultar rótulos de arestas" aria-label="Mostrar/ocultar rótulos de arestas">
|
|
858
|
+
<mat-icon>text_fields</mat-icon>
|
|
859
|
+
</button>
|
|
860
|
+
<button mat-button [color]="showUnconnectedPorts ? 'primary' : undefined" (click)="showUnconnectedPorts = !showUnconnectedPorts" matTooltip="Mostrar/ocultar portas sem conexão" aria-label="Mostrar/ocultar portas sem conexão">
|
|
861
|
+
<mat-icon>tune</mat-icon>
|
|
862
|
+
</button>
|
|
863
|
+
<button mat-stroked-button color="primary" (click)="openBuilder()" aria-label="Editar no Builder"><mat-icon>tune</mat-icon><span>Editar no Builder</span></button>
|
|
864
|
+
<div class="cg-count" [attr.aria-label]="'Total de conexões: ' + connections().length">{{ connections().length }}</div>
|
|
865
|
+
</div>
|
|
866
|
+
<div class="cg-canvas">
|
|
867
|
+
<!-- Edge details panel -->
|
|
868
|
+
<div class="cg-details" *ngIf="selectedEdgeIndex() >= 0 && showDetails">
|
|
869
|
+
<div class="details-head">
|
|
870
|
+
<div class="details-title">Detalhes da conexão</div>
|
|
871
|
+
<button mat-icon-button (click)="showDetails=false" aria-label="Fechar"><mat-icon>close</mat-icon></button>
|
|
872
|
+
</div>
|
|
873
|
+
<div class="details-body" *ngIf="currentConn() as c">
|
|
874
|
+
<div><b>De:</b> {{ c.from.widget }}.{{ c.from.output }}</div>
|
|
875
|
+
<div><b>Para:</b> {{ c.to.widget }}.{{ c.to.input }}</div>
|
|
876
|
+
<div><b>map:</b> {{ c.map || 'payload' }}</div>
|
|
877
|
+
<div *ngIf="c.to.bindingOrder?.length"><b>bindingOrder:</b> {{ c.to.bindingOrder?.join(', ') }}</div>
|
|
878
|
+
<div class="details-actions">
|
|
879
|
+
<button mat-button color="primary" (click)="openBuilder()"><mat-icon>tune</mat-icon> Editar no Builder</button>
|
|
880
|
+
<button mat-button color="warn" (click)="removeEdge(selectedEdgeIndex())"><mat-icon>delete</mat-icon> Remover</button>
|
|
881
|
+
</div>
|
|
882
|
+
</div>
|
|
883
|
+
</div>
|
|
884
|
+
|
|
885
|
+
<svg [attr.width]="'100%'" [attr.height]="'100%'">
|
|
886
|
+
<defs>
|
|
887
|
+
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
|
888
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor"></path>
|
|
889
|
+
</marker>
|
|
890
|
+
</defs>
|
|
891
|
+
<g [attr.transform]="transform()">
|
|
892
|
+
<!-- Edges -->
|
|
893
|
+
<g class="edges">
|
|
894
|
+
<g *ngFor="let e of edges(); let i = index" class="edge-group" role="button" tabindex="0"
|
|
895
|
+
(mouseenter)="hoverEdgeId = e.id" (mouseleave)="hoverEdgeId = ''" (click)="selectEdge(i)">
|
|
896
|
+
<path class="edge" [attr.d]="edgePath(e)" marker-end="url(#arrow)" [class.hover]="hoverEdgeId===e.id" [attr.aria-label]="e.label || ''" [matTooltip]="edgeTooltip(e)"></path>
|
|
897
|
+
<ng-container *ngIf="showEdgeLabels && e.label">
|
|
898
|
+
<g [attr.transform]="'translate(' + edgeMid(e).x + ',' + edgeMid(e).y + ')'">
|
|
899
|
+
<text class="edge-label" x="0" y="0">{{ e.label }}</text>
|
|
900
|
+
</g>
|
|
901
|
+
</ng-container>
|
|
902
|
+
<g *ngIf="hoverEdgeId===e.id" class="edge-toolbar">
|
|
903
|
+
<rect [attr.x]="edgeMid(e).x-28" [attr.y]="edgeMid(e).y-14" width="56" height="28" rx="6" ry="6" class="edge-toolbar-bg"></rect>
|
|
904
|
+
<text [attr.x]="edgeMid(e).x-14" [attr.y]="edgeMid(e).y+4" class="icon" (click)="removeEdge(i)" aria-label="Remover">🗑</text>
|
|
905
|
+
<text [attr.x]="edgeMid(e).x+8" [attr.y]="edgeMid(e).y+4" class="icon" (click)="openBuilder()" aria-label="Editar">✎</text>
|
|
906
|
+
</g>
|
|
907
|
+
</g>
|
|
908
|
+
</g>
|
|
909
|
+
|
|
910
|
+
<!-- Temporary drag edge -->
|
|
911
|
+
<path *ngIf="dragPath()" class="edge temp" [attr.d]="dragPath()" marker-end="url(#arrow)"></path>
|
|
912
|
+
|
|
913
|
+
<!-- Nodes -->
|
|
914
|
+
<g class="nodes">
|
|
915
|
+
<g *ngFor="let n of nodes(); let ni = index" class="node" [attr.transform]="'translate(' + n.bounds.x + ',' + n.bounds.y + ')'" role="group" [attr.aria-label]="n.label">
|
|
916
|
+
<rect class="node-box" [attr.width]="n.bounds.width" [attr.height]="n.bounds.height" rx="8" ry="8"
|
|
917
|
+
(mousedown)="onNodeDragStart($event, ni)"></rect>
|
|
918
|
+
<text class="node-title" x="8" y="20">{{ n.label }}</text>
|
|
919
|
+
|
|
920
|
+
<!-- Output ports (left) -->
|
|
921
|
+
<g *ngFor="let p of outputPorts(n); trackBy: trackPort" class="port output"
|
|
922
|
+
[attr.transform]="'translate(' + (0) + ',' + (p.anchor.y - n.bounds.y - 4) + ')'">
|
|
923
|
+
<circle r="6" cx="-6" cy="8" class="port-dot" [attr.data-node-id]="n.id" [attr.data-port-id]="p.id" [matTooltip]="portTooltip(p)"></circle>
|
|
924
|
+
<rect class="port-hit" x="-16" y="0" width="20" height="16" [dragConnect]="{ nodeId: n.id, portId: p.id }" [matTooltip]="portTooltip(p)"
|
|
925
|
+
(connectDragStart)="onDragStart($event)" (connectDragMove)="onDragMove($event)" (connectDragEnd)="onDragEnd($event)"></rect>
|
|
926
|
+
<text *ngIf="showPortLabels" x="-4" y="12" class="port-label" text-anchor="end" [attr.title]="portTooltip(p)">{{ portText(p) }}</text>
|
|
927
|
+
</g>
|
|
928
|
+
|
|
929
|
+
<!-- Input ports (right) -->
|
|
930
|
+
<g *ngFor="let p of inputPorts(n); trackBy: trackPort" class="port input"
|
|
931
|
+
[attr.transform]="'translate(' + (n.bounds.width) + ',' + (p.anchor.y - n.bounds.y - 4) + ')'">
|
|
932
|
+
<circle r="6" cx="6" cy="8" class="port-dot" [attr.data-node-id]="n.id" [attr.data-port-id]="p.id" data-port-kind="input" [attr.data-port-path]="p.path || ''" [matTooltip]="portTooltip(p)"></circle>
|
|
933
|
+
<rect class="port-hit" x="-4" y="0" width="20" height="16" [matTooltip]="portTooltip(p)"></rect>
|
|
934
|
+
<text *ngIf="showPortLabels" x="8" y="12" class="port-label" [attr.title]="portTooltip(p)">{{ portText(p) }}</text>
|
|
935
|
+
</g>
|
|
936
|
+
</g>
|
|
937
|
+
</g>
|
|
938
|
+
</g>
|
|
939
|
+
</svg>
|
|
940
|
+
<!-- Pending connection popover -->
|
|
941
|
+
<div class="popover" *ngIf="pendingPopover() as pop" [ngStyle]="{ left: pop.x + 'px', top: pop.y + 'px' }">
|
|
942
|
+
<div class="popover-body">
|
|
943
|
+
<div class="popover-title">Nova conexão</div>
|
|
944
|
+
<div style="display:grid; gap:6px; min-width:260px;">
|
|
945
|
+
<label>map:
|
|
946
|
+
<input [(ngModel)]="pop.map" [attr.placeholder]="placeholderSample" />
|
|
947
|
+
</label>
|
|
948
|
+
<div style="display:flex; gap:6px; flex-wrap: wrap;">
|
|
949
|
+
<button mat-stroked-button (click)="applyPreset('payload')">payload</button>
|
|
950
|
+
<button mat-stroked-button (click)="applyPreset('payload.id')">payload.id</button>
|
|
951
|
+
<button mat-stroked-button (click)="applyPreset('payload.data')">payload.data</button>
|
|
952
|
+
</div>
|
|
953
|
+
<label>filter (opcional):
|
|
954
|
+
<input [(ngModel)]="pop.filter" placeholder="ex.: = payload != null" />
|
|
955
|
+
</label>
|
|
956
|
+
<div style="display:grid; gap:6px;">
|
|
957
|
+
<div style="display:flex; gap:8px; align-items:center;">
|
|
958
|
+
<label style="display:flex; align-items:center; gap:6px;">debounce (ms): <input type="number" min="0" style="width:90px" [(ngModel)]="pop.debounceMs" /></label>
|
|
959
|
+
<label style="display:flex; align-items:center; gap:6px;">
|
|
960
|
+
<input type="checkbox" [(ngModel)]="pop.distinct" /> distinct
|
|
961
|
+
</label>
|
|
962
|
+
</div>
|
|
963
|
+
<label>distinctBy (opcional):
|
|
964
|
+
<input [(ngModel)]="pop['distinctBy']" placeholder="ex.: payload.id" />
|
|
965
|
+
</label>
|
|
966
|
+
</div>
|
|
967
|
+
<div style="font-size:12px; opacity:.8;">Filtro: {{ computeFilterPreview(pop.filter) }}</div>
|
|
968
|
+
<div style="font-size:12px; opacity:.8;">Prévia: {{ computePreview(pop.map) }}</div>
|
|
969
|
+
</div>
|
|
970
|
+
<div class="actions">
|
|
971
|
+
<button mat-button (click)="cancelPending()">Cancelar</button>
|
|
972
|
+
<button mat-flat-button color="primary" (click)="confirmPending()">Confirmar</button>
|
|
973
|
+
</div>
|
|
974
|
+
</div>
|
|
975
|
+
</div>
|
|
976
|
+
</div>
|
|
977
|
+
</div>
|
|
978
|
+
`, isInline: true, styles: [".cg-root{display:flex;flex-direction:column;height:100%;width:100%}.cg-head{display:flex;align-items:center;gap:12px;padding:8px 12px;border-bottom:1px solid var(--md-sys-color-outline, #444)}.cg-title{font-weight:600}.spacer{flex:1}.cg-count{opacity:.8}.cg-canvas{position:relative;flex:1;min-height:300px}svg{background:var(--md-sys-color-surface-container-lowest, #111);color:var(--md-sys-color-on-surface, #eee)}.node-box{fill:var(--md-sys-color-surface-container, #1b1b1b);stroke:var(--md-sys-color-outline, #4a4a4a)}.node-title{fill:var(--md-sys-color-on-surface, #eee);font-size:14px;font-weight:600}.port-dot{fill:currentColor;r:6}.port-label{fill:var(--md-sys-color-on-surface-variant, #bbb);font-size:13px}.edge{stroke:var(--md-sys-color-primary, #56a0ff);stroke-width:2;fill:none}.edge.hover{stroke:var(--md-sys-color-tertiary, #ffa04d)}.edge.temp{stroke-dasharray:4 4;opacity:.7}.edge-label{fill:var(--md-sys-color-on-surface, #ffffff);font-size:12px;dominant-baseline:middle;text-anchor:middle}.edge-label-bg{fill:#00000059}.edge-toolbar .edge-toolbar-bg{fill:var(--md-sys-color-surface, #222);stroke:var(--md-sys-color-outline, #444)}.edge-toolbar .icon{fill:var(--md-sys-color-on-surface, #ffffff);font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none}.popover{position:absolute;transform:translate(-50%,-50%);z-index:10}.popover-body{background:var(--md-sys-color-surface, #222);color:var(--md-sys-color-on-surface, #eee);border:1px solid var(--md-sys-color-outline, #444);border-radius:8px;padding:8px;min-width:220px;box-shadow:0 8px 24px #0006}.popover-title{font-weight:600;margin-bottom:8px}.actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.port-hit{fill:transparent;cursor:crosshair;height:24px}.cg-details{position:absolute;right:8px;top:48px;width:320px;background:var(--md-sys-color-surface, #222);color:var(--md-sys-color-on-surface, #eee);border:1px solid var(--md-sys-color-outline, #444);border-radius:8px;box-shadow:0 8px 24px #00000059;z-index:12}.details-head{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-bottom:1px solid var(--md-sys-color-outline, #444)}.details-title{font-weight:600}.details-body{padding:8px 10px;display:grid;gap:6px}.details-actions{display:flex;gap:8px;margin-top:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i4.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i4.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2$2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: DragConnectDirective, selector: "[dragConnect]", inputs: ["dragConnect"], outputs: ["connectDragStart", "connectDragMove", "connectDragEnd"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
979
|
+
}
|
|
980
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: ConnectionGraphComponent, decorators: [{
|
|
981
|
+
type: Component,
|
|
982
|
+
args: [{ selector: 'praxis-connection-graph', standalone: true, imports: [CommonModule, FormsModule, MatButtonModule, MatIconModule, MatTooltipModule, DragConnectDirective], template: `
|
|
983
|
+
<div class="cg-root" (wheel)="onWheel($event)" (mousedown)="onPanStart($event)">
|
|
984
|
+
<div class="cg-head">
|
|
985
|
+
<div class="cg-title">Conexões</div>
|
|
986
|
+
<div class="spacer"></div>
|
|
987
|
+
<button mat-button [color]="showPortLabels ? 'primary' : undefined" (click)="showPortLabels = !showPortLabels" matTooltip="Mostrar/ocultar labels de portas" aria-label="Mostrar/ocultar labels de portas">
|
|
988
|
+
<mat-icon>label</mat-icon>
|
|
989
|
+
</button>
|
|
990
|
+
<button mat-button [color]="showFriendlyLabels ? 'primary' : undefined" (click)="showFriendlyLabels = !showFriendlyLabels" matTooltip="Alternar nome amigável/técnico" aria-label="Alternar nome amigável/técnico">
|
|
991
|
+
<mat-icon>translate</mat-icon>
|
|
992
|
+
</button>
|
|
993
|
+
<button mat-button [color]="showEdgeLabels ? 'primary' : undefined" (click)="showEdgeLabels = !showEdgeLabels" matTooltip="Mostrar/ocultar rótulos de arestas" aria-label="Mostrar/ocultar rótulos de arestas">
|
|
994
|
+
<mat-icon>text_fields</mat-icon>
|
|
995
|
+
</button>
|
|
996
|
+
<button mat-button [color]="showUnconnectedPorts ? 'primary' : undefined" (click)="showUnconnectedPorts = !showUnconnectedPorts" matTooltip="Mostrar/ocultar portas sem conexão" aria-label="Mostrar/ocultar portas sem conexão">
|
|
997
|
+
<mat-icon>tune</mat-icon>
|
|
998
|
+
</button>
|
|
999
|
+
<button mat-stroked-button color="primary" (click)="openBuilder()" aria-label="Editar no Builder"><mat-icon>tune</mat-icon><span>Editar no Builder</span></button>
|
|
1000
|
+
<div class="cg-count" [attr.aria-label]="'Total de conexões: ' + connections().length">{{ connections().length }}</div>
|
|
1001
|
+
</div>
|
|
1002
|
+
<div class="cg-canvas">
|
|
1003
|
+
<!-- Edge details panel -->
|
|
1004
|
+
<div class="cg-details" *ngIf="selectedEdgeIndex() >= 0 && showDetails">
|
|
1005
|
+
<div class="details-head">
|
|
1006
|
+
<div class="details-title">Detalhes da conexão</div>
|
|
1007
|
+
<button mat-icon-button (click)="showDetails=false" aria-label="Fechar"><mat-icon>close</mat-icon></button>
|
|
1008
|
+
</div>
|
|
1009
|
+
<div class="details-body" *ngIf="currentConn() as c">
|
|
1010
|
+
<div><b>De:</b> {{ c.from.widget }}.{{ c.from.output }}</div>
|
|
1011
|
+
<div><b>Para:</b> {{ c.to.widget }}.{{ c.to.input }}</div>
|
|
1012
|
+
<div><b>map:</b> {{ c.map || 'payload' }}</div>
|
|
1013
|
+
<div *ngIf="c.to.bindingOrder?.length"><b>bindingOrder:</b> {{ c.to.bindingOrder?.join(', ') }}</div>
|
|
1014
|
+
<div class="details-actions">
|
|
1015
|
+
<button mat-button color="primary" (click)="openBuilder()"><mat-icon>tune</mat-icon> Editar no Builder</button>
|
|
1016
|
+
<button mat-button color="warn" (click)="removeEdge(selectedEdgeIndex())"><mat-icon>delete</mat-icon> Remover</button>
|
|
1017
|
+
</div>
|
|
1018
|
+
</div>
|
|
1019
|
+
</div>
|
|
1020
|
+
|
|
1021
|
+
<svg [attr.width]="'100%'" [attr.height]="'100%'">
|
|
1022
|
+
<defs>
|
|
1023
|
+
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
|
1024
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor"></path>
|
|
1025
|
+
</marker>
|
|
1026
|
+
</defs>
|
|
1027
|
+
<g [attr.transform]="transform()">
|
|
1028
|
+
<!-- Edges -->
|
|
1029
|
+
<g class="edges">
|
|
1030
|
+
<g *ngFor="let e of edges(); let i = index" class="edge-group" role="button" tabindex="0"
|
|
1031
|
+
(mouseenter)="hoverEdgeId = e.id" (mouseleave)="hoverEdgeId = ''" (click)="selectEdge(i)">
|
|
1032
|
+
<path class="edge" [attr.d]="edgePath(e)" marker-end="url(#arrow)" [class.hover]="hoverEdgeId===e.id" [attr.aria-label]="e.label || ''" [matTooltip]="edgeTooltip(e)"></path>
|
|
1033
|
+
<ng-container *ngIf="showEdgeLabels && e.label">
|
|
1034
|
+
<g [attr.transform]="'translate(' + edgeMid(e).x + ',' + edgeMid(e).y + ')'">
|
|
1035
|
+
<text class="edge-label" x="0" y="0">{{ e.label }}</text>
|
|
1036
|
+
</g>
|
|
1037
|
+
</ng-container>
|
|
1038
|
+
<g *ngIf="hoverEdgeId===e.id" class="edge-toolbar">
|
|
1039
|
+
<rect [attr.x]="edgeMid(e).x-28" [attr.y]="edgeMid(e).y-14" width="56" height="28" rx="6" ry="6" class="edge-toolbar-bg"></rect>
|
|
1040
|
+
<text [attr.x]="edgeMid(e).x-14" [attr.y]="edgeMid(e).y+4" class="icon" (click)="removeEdge(i)" aria-label="Remover">🗑</text>
|
|
1041
|
+
<text [attr.x]="edgeMid(e).x+8" [attr.y]="edgeMid(e).y+4" class="icon" (click)="openBuilder()" aria-label="Editar">✎</text>
|
|
1042
|
+
</g>
|
|
1043
|
+
</g>
|
|
1044
|
+
</g>
|
|
1045
|
+
|
|
1046
|
+
<!-- Temporary drag edge -->
|
|
1047
|
+
<path *ngIf="dragPath()" class="edge temp" [attr.d]="dragPath()" marker-end="url(#arrow)"></path>
|
|
1048
|
+
|
|
1049
|
+
<!-- Nodes -->
|
|
1050
|
+
<g class="nodes">
|
|
1051
|
+
<g *ngFor="let n of nodes(); let ni = index" class="node" [attr.transform]="'translate(' + n.bounds.x + ',' + n.bounds.y + ')'" role="group" [attr.aria-label]="n.label">
|
|
1052
|
+
<rect class="node-box" [attr.width]="n.bounds.width" [attr.height]="n.bounds.height" rx="8" ry="8"
|
|
1053
|
+
(mousedown)="onNodeDragStart($event, ni)"></rect>
|
|
1054
|
+
<text class="node-title" x="8" y="20">{{ n.label }}</text>
|
|
1055
|
+
|
|
1056
|
+
<!-- Output ports (left) -->
|
|
1057
|
+
<g *ngFor="let p of outputPorts(n); trackBy: trackPort" class="port output"
|
|
1058
|
+
[attr.transform]="'translate(' + (0) + ',' + (p.anchor.y - n.bounds.y - 4) + ')'">
|
|
1059
|
+
<circle r="6" cx="-6" cy="8" class="port-dot" [attr.data-node-id]="n.id" [attr.data-port-id]="p.id" [matTooltip]="portTooltip(p)"></circle>
|
|
1060
|
+
<rect class="port-hit" x="-16" y="0" width="20" height="16" [dragConnect]="{ nodeId: n.id, portId: p.id }" [matTooltip]="portTooltip(p)"
|
|
1061
|
+
(connectDragStart)="onDragStart($event)" (connectDragMove)="onDragMove($event)" (connectDragEnd)="onDragEnd($event)"></rect>
|
|
1062
|
+
<text *ngIf="showPortLabels" x="-4" y="12" class="port-label" text-anchor="end" [attr.title]="portTooltip(p)">{{ portText(p) }}</text>
|
|
1063
|
+
</g>
|
|
1064
|
+
|
|
1065
|
+
<!-- Input ports (right) -->
|
|
1066
|
+
<g *ngFor="let p of inputPorts(n); trackBy: trackPort" class="port input"
|
|
1067
|
+
[attr.transform]="'translate(' + (n.bounds.width) + ',' + (p.anchor.y - n.bounds.y - 4) + ')'">
|
|
1068
|
+
<circle r="6" cx="6" cy="8" class="port-dot" [attr.data-node-id]="n.id" [attr.data-port-id]="p.id" data-port-kind="input" [attr.data-port-path]="p.path || ''" [matTooltip]="portTooltip(p)"></circle>
|
|
1069
|
+
<rect class="port-hit" x="-4" y="0" width="20" height="16" [matTooltip]="portTooltip(p)"></rect>
|
|
1070
|
+
<text *ngIf="showPortLabels" x="8" y="12" class="port-label" [attr.title]="portTooltip(p)">{{ portText(p) }}</text>
|
|
1071
|
+
</g>
|
|
1072
|
+
</g>
|
|
1073
|
+
</g>
|
|
1074
|
+
</g>
|
|
1075
|
+
</svg>
|
|
1076
|
+
<!-- Pending connection popover -->
|
|
1077
|
+
<div class="popover" *ngIf="pendingPopover() as pop" [ngStyle]="{ left: pop.x + 'px', top: pop.y + 'px' }">
|
|
1078
|
+
<div class="popover-body">
|
|
1079
|
+
<div class="popover-title">Nova conexão</div>
|
|
1080
|
+
<div style="display:grid; gap:6px; min-width:260px;">
|
|
1081
|
+
<label>map:
|
|
1082
|
+
<input [(ngModel)]="pop.map" [attr.placeholder]="placeholderSample" />
|
|
1083
|
+
</label>
|
|
1084
|
+
<div style="display:flex; gap:6px; flex-wrap: wrap;">
|
|
1085
|
+
<button mat-stroked-button (click)="applyPreset('payload')">payload</button>
|
|
1086
|
+
<button mat-stroked-button (click)="applyPreset('payload.id')">payload.id</button>
|
|
1087
|
+
<button mat-stroked-button (click)="applyPreset('payload.data')">payload.data</button>
|
|
1088
|
+
</div>
|
|
1089
|
+
<label>filter (opcional):
|
|
1090
|
+
<input [(ngModel)]="pop.filter" placeholder="ex.: = payload != null" />
|
|
1091
|
+
</label>
|
|
1092
|
+
<div style="display:grid; gap:6px;">
|
|
1093
|
+
<div style="display:flex; gap:8px; align-items:center;">
|
|
1094
|
+
<label style="display:flex; align-items:center; gap:6px;">debounce (ms): <input type="number" min="0" style="width:90px" [(ngModel)]="pop.debounceMs" /></label>
|
|
1095
|
+
<label style="display:flex; align-items:center; gap:6px;">
|
|
1096
|
+
<input type="checkbox" [(ngModel)]="pop.distinct" /> distinct
|
|
1097
|
+
</label>
|
|
1098
|
+
</div>
|
|
1099
|
+
<label>distinctBy (opcional):
|
|
1100
|
+
<input [(ngModel)]="pop['distinctBy']" placeholder="ex.: payload.id" />
|
|
1101
|
+
</label>
|
|
1102
|
+
</div>
|
|
1103
|
+
<div style="font-size:12px; opacity:.8;">Filtro: {{ computeFilterPreview(pop.filter) }}</div>
|
|
1104
|
+
<div style="font-size:12px; opacity:.8;">Prévia: {{ computePreview(pop.map) }}</div>
|
|
1105
|
+
</div>
|
|
1106
|
+
<div class="actions">
|
|
1107
|
+
<button mat-button (click)="cancelPending()">Cancelar</button>
|
|
1108
|
+
<button mat-flat-button color="primary" (click)="confirmPending()">Confirmar</button>
|
|
1109
|
+
</div>
|
|
1110
|
+
</div>
|
|
1111
|
+
</div>
|
|
1112
|
+
</div>
|
|
1113
|
+
</div>
|
|
1114
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".cg-root{display:flex;flex-direction:column;height:100%;width:100%}.cg-head{display:flex;align-items:center;gap:12px;padding:8px 12px;border-bottom:1px solid var(--md-sys-color-outline, #444)}.cg-title{font-weight:600}.spacer{flex:1}.cg-count{opacity:.8}.cg-canvas{position:relative;flex:1;min-height:300px}svg{background:var(--md-sys-color-surface-container-lowest, #111);color:var(--md-sys-color-on-surface, #eee)}.node-box{fill:var(--md-sys-color-surface-container, #1b1b1b);stroke:var(--md-sys-color-outline, #4a4a4a)}.node-title{fill:var(--md-sys-color-on-surface, #eee);font-size:14px;font-weight:600}.port-dot{fill:currentColor;r:6}.port-label{fill:var(--md-sys-color-on-surface-variant, #bbb);font-size:13px}.edge{stroke:var(--md-sys-color-primary, #56a0ff);stroke-width:2;fill:none}.edge.hover{stroke:var(--md-sys-color-tertiary, #ffa04d)}.edge.temp{stroke-dasharray:4 4;opacity:.7}.edge-label{fill:var(--md-sys-color-on-surface, #ffffff);font-size:12px;dominant-baseline:middle;text-anchor:middle}.edge-label-bg{fill:#00000059}.edge-toolbar .edge-toolbar-bg{fill:var(--md-sys-color-surface, #222);stroke:var(--md-sys-color-outline, #444)}.edge-toolbar .icon{fill:var(--md-sys-color-on-surface, #ffffff);font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none}.popover{position:absolute;transform:translate(-50%,-50%);z-index:10}.popover-body{background:var(--md-sys-color-surface, #222);color:var(--md-sys-color-on-surface, #eee);border:1px solid var(--md-sys-color-outline, #444);border-radius:8px;padding:8px;min-width:220px;box-shadow:0 8px 24px #0006}.popover-title{font-weight:600;margin-bottom:8px}.actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.port-hit{fill:transparent;cursor:crosshair;height:24px}.cg-details{position:absolute;right:8px;top:48px;width:320px;background:var(--md-sys-color-surface, #222);color:var(--md-sys-color-on-surface, #eee);border:1px solid var(--md-sys-color-outline, #444);border-radius:8px;box-shadow:0 8px 24px #00000059;z-index:12}.details-head{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-bottom:1px solid var(--md-sys-color-outline, #444)}.details-title{font-weight:600}.details-body{padding:8px 10px;display:grid;gap:6px}.details-actions{display:flex;gap:8px;margin-top:8px}\n"] }]
|
|
1115
|
+
}], ctorParameters: () => [{ type: GraphMapperService }, { type: i2$1.SettingsPanelService }, { type: undefined, decorators: [{
|
|
1116
|
+
type: Optional
|
|
1117
|
+
}, {
|
|
1118
|
+
type: Inject,
|
|
1119
|
+
args: [SETTINGS_PANEL_DATA]
|
|
1120
|
+
}] }], propDecorators: { page: [{
|
|
1121
|
+
type: Input
|
|
1122
|
+
}], widgets: [{
|
|
1123
|
+
type: Input
|
|
1124
|
+
}], pageChange: [{
|
|
1125
|
+
type: Output
|
|
1126
|
+
}] } });
|
|
1127
|
+
|
|
1128
|
+
export { ConnectionGraphComponent };
|
|
1129
|
+
//# sourceMappingURL=praxisui-page-builder-connection-graph.component-C6x--6--.mjs.map
|