@zentto/report-designer 1.6.5 → 1.6.7
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/dist/data-panel/er-diagram.d.ts +21 -11
- package/dist/data-panel/er-diagram.d.ts.map +1 -1
- package/dist/data-panel/er-diagram.js +324 -294
- package/dist/data-panel/er-diagram.js.map +1 -1
- package/dist/data-panel/formula-editor.d.ts.map +1 -1
- package/dist/data-panel/formula-editor.js +5 -2
- package/dist/data-panel/formula-editor.js.map +1 -1
- package/dist/data-panel/zentto-connector.d.ts +3 -3
- package/dist/data-panel/zentto-connector.d.ts.map +1 -1
- package/dist/data-panel/zentto-connector.js +14 -14
- package/dist/data-panel/zentto-connector.js.map +1 -1
- package/dist/zentto-report-designer.d.ts.map +1 -1
- package/dist/zentto-report-designer.js +19 -3
- package/dist/zentto-report-designer.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @zentto/report-designer — Visual ER Diagram
|
|
2
|
-
// Interactive SVG canvas
|
|
2
|
+
// Interactive SVG canvas: tables, drag-to-link fields, relation arrows with cardinality
|
|
3
3
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
4
4
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
5
5
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -8,20 +8,24 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
8
8
|
};
|
|
9
9
|
import { LitElement, html, css, svg, nothing } from 'lit';
|
|
10
10
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
11
|
-
// ───
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
11
|
+
// ─── Constants ────────────────────────────────────────────────────
|
|
12
|
+
const TW = 190; // table width
|
|
13
|
+
const TH_HEADER = 30; // header height
|
|
14
|
+
const TH_FIELD = 18; // field row height
|
|
15
|
+
const TH_PAD = 8; // bottom padding
|
|
16
16
|
const GRID_COLS = 3;
|
|
17
|
-
const
|
|
18
|
-
const
|
|
17
|
+
const GAP_X = 230;
|
|
18
|
+
const GAP_Y = 40;
|
|
19
19
|
const JOIN_COLORS = {
|
|
20
|
-
inner: '#1976d2',
|
|
21
|
-
left: '#2e7d32',
|
|
22
|
-
right: '#e65100',
|
|
23
|
-
full: '#7b1fa2',
|
|
20
|
+
inner: '#1976d2', left: '#2e7d32', right: '#e65100', full: '#7b1fa2',
|
|
24
21
|
};
|
|
22
|
+
const CARDINALITY = {
|
|
23
|
+
inner: { start: '1', end: '1' },
|
|
24
|
+
left: { start: '1', end: '*' },
|
|
25
|
+
right: { start: '*', end: '1' },
|
|
26
|
+
full: { start: '*', end: '*' },
|
|
27
|
+
};
|
|
28
|
+
// ─── Component ────────────────────────────────────────────────────
|
|
25
29
|
let ErDiagram = class ErDiagram extends LitElement {
|
|
26
30
|
constructor() {
|
|
27
31
|
super(...arguments);
|
|
@@ -29,145 +33,102 @@ let ErDiagram = class ErDiagram extends LitElement {
|
|
|
29
33
|
this.relations = [];
|
|
30
34
|
this._tables = [];
|
|
31
35
|
this._dragging = null;
|
|
32
|
-
this.
|
|
33
|
-
this.
|
|
34
|
-
this.
|
|
36
|
+
this._linkDrag = null;
|
|
37
|
+
this._hoveredRel = null;
|
|
38
|
+
this._selectedRel = null;
|
|
39
|
+
this._svgW = 800;
|
|
40
|
+
this._svgH = 600;
|
|
41
|
+
this._tool = 'select';
|
|
35
42
|
}
|
|
36
43
|
static { this.styles = css `
|
|
37
|
-
:host {
|
|
38
|
-
display: block;
|
|
39
|
-
height: 100%;
|
|
40
|
-
overflow: hidden;
|
|
41
|
-
font-family: 'Segoe UI', Roboto, Arial, sans-serif;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
.er-container {
|
|
45
|
-
width: 100%;
|
|
46
|
-
height: 100%;
|
|
47
|
-
overflow: auto;
|
|
48
|
-
background:
|
|
49
|
-
linear-gradient(90deg, var(--zrd-grid-line, #f0f0f0) 1px, transparent 1px),
|
|
50
|
-
linear-gradient(var(--zrd-grid-line, #f0f0f0) 1px, transparent 1px);
|
|
51
|
-
background-size: 20px 20px;
|
|
52
|
-
cursor: default;
|
|
53
|
-
}
|
|
44
|
+
:host { display: flex; flex-direction: column; height: 100%; overflow: hidden; font-family: 'Segoe UI', Roboto, Arial, sans-serif; }
|
|
54
45
|
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
/* ─── Toolbar ─────────────────────────────── */
|
|
47
|
+
.er-toolbar {
|
|
48
|
+
display: flex; align-items: center; gap: 4px;
|
|
49
|
+
padding: 6px 10px; background: #f5f5f5;
|
|
50
|
+
border-bottom: 1px solid #ddd; flex-shrink: 0;
|
|
57
51
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
.table-box:hover .table-border {
|
|
64
|
-
stroke: var(--zrd-primary, #1976d2);
|
|
65
|
-
stroke-width: 2;
|
|
52
|
+
.tb-btn {
|
|
53
|
+
padding: 4px 10px; border: 1px solid #ccc; border-radius: 4px;
|
|
54
|
+
background: #fff; cursor: pointer; font-size: 11px; font-weight: 500;
|
|
55
|
+
display: flex; align-items: center; gap: 4px;
|
|
66
56
|
}
|
|
57
|
+
.tb-btn:hover { background: #e3f2fd; border-color: #1976d2; }
|
|
58
|
+
.tb-btn.active { background: #1976d2; color: #fff; border-color: #1976d2; }
|
|
59
|
+
.tb-btn.danger { color: #c62828; }
|
|
60
|
+
.tb-btn.danger:hover { background: #ffebee; }
|
|
61
|
+
.tb-sep { width: 1px; height: 20px; background: #ddd; margin: 0 4px; }
|
|
62
|
+
.tb-spacer { flex: 1; }
|
|
63
|
+
.tb-info { font-size: 10px; color: #999; }
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.table-header-bg {
|
|
78
|
-
rx: 6;
|
|
79
|
-
ry: 6;
|
|
80
|
-
}
|
|
81
|
-
.table-header-mask {
|
|
82
|
-
fill: var(--zrd-panel-bg, #fff);
|
|
65
|
+
/* ─── Canvas ──────────────────────────────── */
|
|
66
|
+
.er-canvas {
|
|
67
|
+
flex: 1; overflow: auto; cursor: default;
|
|
68
|
+
background:
|
|
69
|
+
linear-gradient(90deg, #f5f5f5 1px, transparent 1px),
|
|
70
|
+
linear-gradient(#f5f5f5 1px, transparent 1px);
|
|
71
|
+
background-size: 20px 20px; background-color: #fafafa;
|
|
83
72
|
}
|
|
73
|
+
.er-canvas.link-mode { cursor: crosshair; }
|
|
74
|
+
svg { display: block; }
|
|
84
75
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
76
|
+
/* ─── Table ───────────────────────────────── */
|
|
77
|
+
.tbl { cursor: move; }
|
|
78
|
+
.tbl:hover .tbl-border { stroke: #1976d2; stroke-width: 2; }
|
|
79
|
+
.tbl-border {
|
|
80
|
+
fill: #fff; stroke: #ccc; stroke-width: 1; rx: 6; ry: 6;
|
|
81
|
+
filter: drop-shadow(0 2px 6px rgba(0,0,0,0.1));
|
|
89
82
|
}
|
|
83
|
+
.tbl-hdr { rx: 6; ry: 6; }
|
|
84
|
+
.tbl-hdr-mask { fill: #fff; }
|
|
85
|
+
.tbl-name { font-size: 11px; font-weight: 600; fill: #fff; }
|
|
86
|
+
.tbl-count { font-size: 9px; fill: rgba(255,255,255,0.7); }
|
|
90
87
|
|
|
91
|
-
.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
.
|
|
96
|
-
|
|
97
|
-
fill: var(--zrd-text-muted, #999);
|
|
98
|
-
}
|
|
99
|
-
.field-pk {
|
|
100
|
-
font-size: 9px;
|
|
101
|
-
fill: #ff9800;
|
|
102
|
-
}
|
|
88
|
+
.fld-row { cursor: default; }
|
|
89
|
+
.fld-row:hover rect { fill: rgba(25,118,210,0.06); }
|
|
90
|
+
.fld-name { font-size: 10px; fill: #333; }
|
|
91
|
+
.fld-type { font-size: 8.5px; fill: #999; }
|
|
92
|
+
.fld-pk { font-size: 9px; fill: #ff9800; }
|
|
93
|
+
.fld-sep { stroke: #eee; stroke-width: 0.5; }
|
|
103
94
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.field-separator {
|
|
109
|
-
stroke: var(--zrd-border, #eee);
|
|
110
|
-
stroke-width: 0.5;
|
|
95
|
+
/* Link handle (dot on field) */
|
|
96
|
+
.fld-link-dot {
|
|
97
|
+
fill: #1976d2; opacity: 0; cursor: crosshair; transition: opacity 0.15s;
|
|
111
98
|
}
|
|
99
|
+
.tbl:hover .fld-link-dot, .er-canvas.link-mode .fld-link-dot { opacity: 0.6; }
|
|
100
|
+
.fld-link-dot:hover { opacity: 1 !important; r: 5; }
|
|
112
101
|
|
|
113
102
|
/* ─── Relation Lines ──────────────────────── */
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
.relation-line:hover,
|
|
121
|
-
.relation-line.hovered {
|
|
122
|
-
stroke-width: 3;
|
|
123
|
-
}
|
|
103
|
+
.rel-path { fill: none; stroke-width: 1.8; transition: stroke-width 0.15s; }
|
|
104
|
+
.rel-path.hovered, .rel-path.selected { stroke-width: 3; }
|
|
105
|
+
.rel-hit { fill: none; stroke: transparent; stroke-width: 14; cursor: pointer; }
|
|
106
|
+
.rel-label { font-size: 9px; font-weight: 600; pointer-events: none; }
|
|
107
|
+
.rel-label-bg { rx: 3; ry: 3; }
|
|
108
|
+
.rel-card { font-size: 10px; font-weight: 700; pointer-events: none; }
|
|
124
109
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
font-weight: 600;
|
|
128
|
-
pointer-events: none;
|
|
129
|
-
}
|
|
130
|
-
.relation-label-bg {
|
|
131
|
-
fill: var(--zrd-panel-bg, #fff);
|
|
132
|
-
rx: 3;
|
|
133
|
-
ry: 3;
|
|
134
|
-
}
|
|
110
|
+
/* Link drag preview line */
|
|
111
|
+
.link-preview { stroke: #1976d2; stroke-width: 2; stroke-dasharray: 6 3; fill: none; }
|
|
135
112
|
|
|
136
|
-
|
|
137
|
-
.empty-state {
|
|
138
|
-
display: flex;
|
|
139
|
-
align-items: center;
|
|
140
|
-
justify-content: center;
|
|
141
|
-
height: 100%;
|
|
142
|
-
color: var(--zrd-text-muted, #999);
|
|
143
|
-
font-size: 12px;
|
|
144
|
-
text-align: center;
|
|
145
|
-
padding: 20px;
|
|
146
|
-
}
|
|
113
|
+
.empty { display: flex; align-items: center; justify-content: center; height: 100%; color: #999; font-size: 12px; text-align: center; padding: 20px; }
|
|
147
114
|
`; }
|
|
148
115
|
// ─── Lifecycle ────────────────────────────────────────────────
|
|
149
116
|
updated(changed) {
|
|
150
|
-
if (changed.has('dataSources'))
|
|
117
|
+
if (changed.has('dataSources'))
|
|
151
118
|
this._layoutTables();
|
|
152
|
-
}
|
|
153
119
|
}
|
|
154
|
-
// ─── Auto Layout ──────────────────────────────────────────────
|
|
155
120
|
_layoutTables() {
|
|
156
121
|
const tables = [];
|
|
157
|
-
let col = 0;
|
|
158
|
-
let row = 0;
|
|
122
|
+
let col = 0, row = 0;
|
|
159
123
|
for (const ds of this.dataSources) {
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
// Check if existing position is stored (preserve user moves)
|
|
124
|
+
const fc = ds.fields?.length || 0;
|
|
125
|
+
const h = TH_HEADER + fc * TH_FIELD + TH_PAD;
|
|
163
126
|
const existing = this._tables.find(t => t.dsId === ds.id);
|
|
164
127
|
tables.push({
|
|
165
128
|
dsId: ds.id,
|
|
166
|
-
x: existing?.x ?? (
|
|
167
|
-
y: existing?.y ?? (
|
|
168
|
-
width:
|
|
169
|
-
height,
|
|
170
|
-
ds,
|
|
129
|
+
x: existing?.x ?? (12 + col * GAP_X),
|
|
130
|
+
y: existing?.y ?? (12 + row * (Math.max(h, 100) + GAP_Y)),
|
|
131
|
+
width: TW, height: h, ds,
|
|
171
132
|
});
|
|
172
133
|
col++;
|
|
173
134
|
if (col >= GRID_COLS) {
|
|
@@ -176,224 +137,284 @@ let ErDiagram = class ErDiagram extends LitElement {
|
|
|
176
137
|
}
|
|
177
138
|
}
|
|
178
139
|
this._tables = tables;
|
|
179
|
-
this.
|
|
140
|
+
this._updateSize();
|
|
180
141
|
}
|
|
181
|
-
|
|
182
|
-
let
|
|
183
|
-
let maxY = 300;
|
|
142
|
+
_updateSize() {
|
|
143
|
+
let mx = 600, my = 400;
|
|
184
144
|
for (const t of this._tables) {
|
|
185
|
-
|
|
186
|
-
|
|
145
|
+
mx = Math.max(mx, t.x + t.width + 60);
|
|
146
|
+
my = Math.max(my, t.y + t.height + 60);
|
|
187
147
|
}
|
|
188
|
-
this.
|
|
189
|
-
this.
|
|
148
|
+
this._svgW = mx;
|
|
149
|
+
this._svgH = my;
|
|
190
150
|
}
|
|
191
|
-
// ─── Drag
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
151
|
+
// ─── Table Drag ───────────────────────────────────────────────
|
|
152
|
+
_onTableDown(e, dsId) {
|
|
153
|
+
if (this._tool === 'link')
|
|
154
|
+
return; // in link mode, don't drag tables
|
|
155
|
+
const t = this._tables.find(t => t.dsId === dsId);
|
|
156
|
+
if (!t)
|
|
195
157
|
return;
|
|
196
158
|
e.preventDefault();
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const rect = svgEl.getBoundingClientRect();
|
|
201
|
-
this._dragging = {
|
|
202
|
-
dsId,
|
|
203
|
-
offsetX: e.clientX - rect.left - table.x,
|
|
204
|
-
offsetY: e.clientY - rect.top - table.y,
|
|
205
|
-
};
|
|
159
|
+
e.stopPropagation();
|
|
160
|
+
const svgRect = this.renderRoot.querySelector('svg').getBoundingClientRect();
|
|
161
|
+
this._dragging = { dsId, ox: e.clientX - svgRect.left - t.x, oy: e.clientY - svgRect.top - t.y };
|
|
206
162
|
const onMove = (ev) => {
|
|
207
163
|
if (!this._dragging)
|
|
208
164
|
return;
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
this._tables = this._tables.map(
|
|
212
|
-
this.
|
|
165
|
+
const nx = Math.max(0, ev.clientX - svgRect.left - this._dragging.ox);
|
|
166
|
+
const ny = Math.max(0, ev.clientY - svgRect.top - this._dragging.oy);
|
|
167
|
+
this._tables = this._tables.map(tb => tb.dsId === this._dragging.dsId ? { ...tb, x: nx, y: ny } : tb);
|
|
168
|
+
this._updateSize();
|
|
169
|
+
};
|
|
170
|
+
const onUp = () => { this._dragging = null; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
|
|
171
|
+
document.addEventListener('mousemove', onMove);
|
|
172
|
+
document.addEventListener('mouseup', onUp);
|
|
173
|
+
}
|
|
174
|
+
// ─── Link Drag (create relation by dragging field to field) ──
|
|
175
|
+
_onFieldLinkStart(e, dsId, fieldName) {
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
e.stopPropagation();
|
|
178
|
+
const svgRect = this.renderRoot.querySelector('svg').getBoundingClientRect();
|
|
179
|
+
const x = e.clientX - svgRect.left;
|
|
180
|
+
const y = e.clientY - svgRect.top;
|
|
181
|
+
this._linkDrag = { fromDsId: dsId, fromField: fieldName, fromX: x, fromY: y, currentX: x, currentY: y };
|
|
182
|
+
const onMove = (ev) => {
|
|
183
|
+
if (!this._linkDrag)
|
|
184
|
+
return;
|
|
185
|
+
this._linkDrag = { ...this._linkDrag, currentX: ev.clientX - svgRect.left, currentY: ev.clientY - svgRect.top };
|
|
213
186
|
};
|
|
214
|
-
const onUp = () => {
|
|
215
|
-
this._dragging = null;
|
|
187
|
+
const onUp = (ev) => {
|
|
216
188
|
document.removeEventListener('mousemove', onMove);
|
|
217
189
|
document.removeEventListener('mouseup', onUp);
|
|
190
|
+
if (!this._linkDrag)
|
|
191
|
+
return;
|
|
192
|
+
// Find drop target field
|
|
193
|
+
const target = this._findFieldAt(ev.clientX - svgRect.left, ev.clientY - svgRect.top);
|
|
194
|
+
if (target && target.dsId !== this._linkDrag.fromDsId) {
|
|
195
|
+
this._createRelation(this._linkDrag.fromDsId, this._linkDrag.fromField, target.dsId, target.fieldName);
|
|
196
|
+
}
|
|
197
|
+
this._linkDrag = null;
|
|
218
198
|
};
|
|
219
199
|
document.addEventListener('mousemove', onMove);
|
|
220
200
|
document.addEventListener('mouseup', onUp);
|
|
221
201
|
}
|
|
222
|
-
|
|
223
|
-
this.
|
|
202
|
+
_findFieldAt(x, y) {
|
|
203
|
+
for (const t of this._tables) {
|
|
204
|
+
if (x < t.x || x > t.x + t.width)
|
|
205
|
+
continue;
|
|
206
|
+
const fields = t.ds.fields || [];
|
|
207
|
+
for (let i = 0; i < fields.length; i++) {
|
|
208
|
+
const fy = t.y + TH_HEADER + i * TH_FIELD;
|
|
209
|
+
if (y >= fy && y <= fy + TH_FIELD) {
|
|
210
|
+
return { dsId: t.dsId, fieldName: fields[i].name };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
_createRelation(parentDs, parentField, childDs, childField) {
|
|
217
|
+
const rel = {
|
|
218
|
+
id: `rel_${Date.now()}`,
|
|
219
|
+
parentSource: parentDs,
|
|
220
|
+
childSource: childDs,
|
|
221
|
+
joins: [{ parentField, childField }],
|
|
222
|
+
joinType: 'inner',
|
|
223
|
+
};
|
|
224
|
+
this.dispatchEvent(new CustomEvent('relation-create', {
|
|
224
225
|
detail: { relation: rel },
|
|
225
226
|
bubbles: true, composed: true,
|
|
226
227
|
}));
|
|
227
228
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
229
|
+
// ─── Relation Actions ─────────────────────────────────────────
|
|
230
|
+
_onRelClick(rel) {
|
|
231
|
+
this._selectedRel = this._selectedRel === rel.id ? null : rel.id;
|
|
232
|
+
}
|
|
233
|
+
_editRelation() {
|
|
234
|
+
const rel = (this.relations || []).find(r => r.id === this._selectedRel);
|
|
235
|
+
if (rel) {
|
|
236
|
+
this.dispatchEvent(new CustomEvent('relation-edit', { detail: { relation: rel }, bubbles: true, composed: true }));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
_deleteRelation() {
|
|
240
|
+
if (this._selectedRel) {
|
|
241
|
+
this.dispatchEvent(new CustomEvent('relation-delete', { detail: { relationId: this._selectedRel }, bubbles: true, composed: true }));
|
|
242
|
+
this._selectedRel = null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
_emit(action) {
|
|
246
|
+
this.dispatchEvent(new CustomEvent(action, { bubbles: true, composed: true }));
|
|
247
|
+
}
|
|
248
|
+
// ─── Connection Points (field-level) ──────────────────────────
|
|
249
|
+
_getFieldY(dsId, fieldName) {
|
|
250
|
+
const t = this._tables.find(tb => tb.dsId === dsId);
|
|
251
|
+
if (!t)
|
|
252
|
+
return null;
|
|
253
|
+
const fields = t.ds.fields || [];
|
|
254
|
+
const idx = fields.findIndex(f => f.name === fieldName);
|
|
255
|
+
if (idx < 0)
|
|
256
|
+
return t.y + t.height / 2; // fallback to center
|
|
257
|
+
return t.y + TH_HEADER + idx * TH_FIELD + TH_FIELD / 2;
|
|
233
258
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
if (!parent || !child)
|
|
259
|
+
_getRelPoints(rel) {
|
|
260
|
+
const pt = this._tables.find(t => t.dsId === rel.parentSource);
|
|
261
|
+
const ct = this._tables.find(t => t.dsId === rel.childSource);
|
|
262
|
+
if (!pt || !ct)
|
|
239
263
|
return null;
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
const
|
|
264
|
+
const pField = rel.joins[0]?.parentField;
|
|
265
|
+
const cField = rel.joins[0]?.childField;
|
|
266
|
+
const py = this._getFieldY(rel.parentSource, pField) ?? (pt.y + pt.height / 2);
|
|
267
|
+
const cy = this._getFieldY(rel.childSource, cField) ?? (ct.y + ct.height / 2);
|
|
268
|
+
// Determine connection sides
|
|
243
269
|
let x1, x2;
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
x2 = child.x;
|
|
270
|
+
if (pt.x + pt.width + 20 < ct.x) {
|
|
271
|
+
x1 = pt.x + pt.width;
|
|
272
|
+
x2 = ct.x;
|
|
248
273
|
}
|
|
249
|
-
else if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
x2 = child.x + child.width;
|
|
274
|
+
else if (ct.x + ct.width + 20 < pt.x) {
|
|
275
|
+
x1 = pt.x;
|
|
276
|
+
x2 = ct.x + ct.width;
|
|
253
277
|
}
|
|
254
278
|
else {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
x2 = child.x + child.width / 2;
|
|
279
|
+
x1 = pt.x + pt.width;
|
|
280
|
+
x2 = ct.x + ct.width;
|
|
258
281
|
}
|
|
259
|
-
return { x1, y1:
|
|
282
|
+
return { x1, y1: py, x2, y2: cy };
|
|
260
283
|
}
|
|
261
|
-
// ───
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
<
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
<
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
<
|
|
281
|
-
|
|
282
|
-
|
|
284
|
+
// ─── Render ───────────────────────────────────────────────────
|
|
285
|
+
render() {
|
|
286
|
+
const hasDS = this.dataSources.length > 0;
|
|
287
|
+
return html `
|
|
288
|
+
<!-- Toolbar -->
|
|
289
|
+
<div class="er-toolbar">
|
|
290
|
+
<button class="tb-btn ${this._tool === 'select' ? 'active' : ''}"
|
|
291
|
+
@click=${() => { this._tool = 'select'; }}>\u{1F5B1} Select</button>
|
|
292
|
+
<button class="tb-btn ${this._tool === 'link' ? 'active' : ''}"
|
|
293
|
+
@click=${() => { this._tool = 'link'; }}>\u{1F517} Link</button>
|
|
294
|
+
<span class="tb-sep"></span>
|
|
295
|
+
<button class="tb-btn" @click=${() => this._emit('add-datasource')}>\u2795 Table</button>
|
|
296
|
+
<button class="tb-btn" @click=${() => this._emit('add-zentto')}>\u{1F310} Zentto</button>
|
|
297
|
+
<button class="tb-btn" @click=${() => this._emit('add-rest')}>\u{1F517} REST</button>
|
|
298
|
+
<span class="tb-sep"></span>
|
|
299
|
+
${this._selectedRel ? html `
|
|
300
|
+
<button class="tb-btn" @click=${this._editRelation}>\u270E Edit Link</button>
|
|
301
|
+
<button class="tb-btn danger" @click=${this._deleteRelation}>\u{1F5D1} Delete Link</button>
|
|
302
|
+
` : nothing}
|
|
303
|
+
<span class="tb-spacer"></span>
|
|
304
|
+
<button class="tb-btn" @click=${() => { this._tables = []; this._layoutTables(); }}>\u{2B73} Auto Layout</button>
|
|
305
|
+
<span class="tb-info">${this.dataSources.length} tables \u2022 ${(this.relations || []).length} relations</span>
|
|
306
|
+
</div>
|
|
283
307
|
|
|
284
|
-
|
|
285
|
-
<
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
308
|
+
${!hasDS ? html `
|
|
309
|
+
<div class="empty">No data sources. Use the toolbar to add tables, Zentto modules, or REST endpoints.</div>
|
|
310
|
+
` : html `
|
|
311
|
+
<div class="er-canvas ${this._tool === 'link' ? 'link-mode' : ''}">
|
|
312
|
+
<svg width=${this._svgW} height=${this._svgH} xmlns="http://www.w3.org/2000/svg">
|
|
313
|
+
${this._renderDefs()}
|
|
314
|
+
${(this.relations || []).map(r => this._renderRelLine(r))}
|
|
315
|
+
${this._tables.map(t => this._renderTable(t))}
|
|
316
|
+
${this._linkDrag ? this._renderLinkPreview() : nothing}
|
|
317
|
+
</svg>
|
|
318
|
+
</div>
|
|
319
|
+
`}
|
|
320
|
+
`;
|
|
321
|
+
}
|
|
322
|
+
_renderDefs() {
|
|
323
|
+
return svg `<defs>
|
|
324
|
+
${Object.entries(JOIN_COLORS).map(([type, color]) => svg `
|
|
325
|
+
<marker id="arr-${type}" viewBox="0 0 12 8" refX="11" refY="4"
|
|
326
|
+
markerWidth="10" markerHeight="8" orient="auto-start-reverse">
|
|
327
|
+
<path d="M 0 0 L 12 4 L 0 8 z" fill=${color} />
|
|
328
|
+
</marker>
|
|
329
|
+
`)}
|
|
330
|
+
<marker id="arr-preview" viewBox="0 0 12 8" refX="11" refY="4"
|
|
331
|
+
markerWidth="10" markerHeight="8" orient="auto-start-reverse">
|
|
332
|
+
<path d="M 0 0 L 12 4 L 0 8 z" fill="#1976d2" opacity="0.5" />
|
|
333
|
+
</marker>
|
|
334
|
+
</defs>`;
|
|
335
|
+
}
|
|
336
|
+
// ─── Table Box ────────────────────────────────────────────────
|
|
337
|
+
_renderTable(t) {
|
|
338
|
+
const fields = t.ds.fields || [];
|
|
339
|
+
const hdrColor = '#1976d2';
|
|
340
|
+
return svg `
|
|
341
|
+
<g class="tbl" @mousedown=${(e) => this._onTableDown(e, t.dsId)}>
|
|
342
|
+
<rect class="tbl-border" x=${t.x} y=${t.y} width=${t.width} height=${t.height} />
|
|
343
|
+
<rect class="tbl-hdr" x=${t.x} y=${t.y} width=${t.width} height=${TH_HEADER} fill=${hdrColor} />
|
|
344
|
+
<rect class="tbl-hdr-mask" x=${t.x} y=${t.y + TH_HEADER - 6} width=${t.width} height="6" />
|
|
345
|
+
<text class="tbl-name" x=${t.x + 8} y=${t.y + 19}>${t.ds.table || t.ds.name}</text>
|
|
346
|
+
<text class="tbl-count" x=${t.x + t.width - 8} y=${t.y + 19} text-anchor="end">${fields.length}</text>
|
|
289
347
|
|
|
290
|
-
<!-- Fields -->
|
|
291
348
|
${fields.map((f, i) => {
|
|
292
|
-
const fy =
|
|
349
|
+
const fy = t.y + TH_HEADER + i * TH_FIELD;
|
|
293
350
|
return svg `
|
|
294
|
-
<g class="
|
|
295
|
-
${i > 0 ? svg
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
${f.label || f.name}
|
|
308
|
-
</text>
|
|
309
|
-
<text class="field-type"
|
|
310
|
-
x=${table.x + table.width - 8}
|
|
311
|
-
y=${fy + 14}
|
|
312
|
-
text-anchor="end">
|
|
313
|
-
${f.nativeType || f.type}
|
|
314
|
-
</text>
|
|
351
|
+
<g class="fld-row">
|
|
352
|
+
${i > 0 ? svg `<line class="fld-sep" x1=${t.x + 4} y1=${fy} x2=${t.x + t.width - 4} y2=${fy} />` : nothing}
|
|
353
|
+
<!-- Hover highlight bg -->
|
|
354
|
+
<rect x=${t.x + 1} y=${fy} width=${t.width - 2} height=${TH_FIELD} fill="transparent" />
|
|
355
|
+
${f.isPrimaryKey ? svg `<text class="fld-pk" x=${t.x + 6} y=${fy + 13}>\u{1F511}</text>` : nothing}
|
|
356
|
+
<text class="fld-name" x=${t.x + (f.isPrimaryKey ? 20 : 8)} y=${fy + 13}>${f.label || f.name}</text>
|
|
357
|
+
<text class="fld-type" x=${t.x + t.width - 16} y=${fy + 13} text-anchor="end">${f.nativeType || f.type}</text>
|
|
358
|
+
<!-- Link dot (right edge) -->
|
|
359
|
+
<circle class="fld-link-dot" cx=${t.x + t.width - 4} cy=${fy + TH_FIELD / 2} r="4"
|
|
360
|
+
@mousedown=${(e) => this._onFieldLinkStart(e, t.dsId, f.name)} />
|
|
361
|
+
<!-- Link dot (left edge) -->
|
|
362
|
+
<circle class="fld-link-dot" cx=${t.x + 4} cy=${fy + TH_FIELD / 2} r="4"
|
|
363
|
+
@mousedown=${(e) => this._onFieldLinkStart(e, t.dsId, f.name)} />
|
|
315
364
|
</g>
|
|
316
365
|
`;
|
|
317
366
|
})}
|
|
318
367
|
</g>
|
|
319
368
|
`;
|
|
320
369
|
}
|
|
321
|
-
|
|
322
|
-
|
|
370
|
+
// ─── Relation Line ────────────────────────────────────────────
|
|
371
|
+
_renderRelLine(rel) {
|
|
372
|
+
const pts = this._getRelPoints(rel);
|
|
323
373
|
if (!pts)
|
|
324
374
|
return nothing;
|
|
325
375
|
const { x1, y1, x2, y2 } = pts;
|
|
326
376
|
const color = JOIN_COLORS[rel.joinType] || '#666';
|
|
377
|
+
const card = CARDINALITY[rel.joinType];
|
|
378
|
+
const isHovered = this._hoveredRel === rel.id;
|
|
379
|
+
const isSelected = this._selectedRel === rel.id;
|
|
380
|
+
const cx1 = x1 + (x2 - x1) * 0.35;
|
|
381
|
+
const cx2 = x1 + (x2 - x1) * 0.65;
|
|
382
|
+
const d = `M ${x1} ${y1} C ${cx1} ${y1}, ${cx2} ${y2}, ${x2} ${y2}`;
|
|
327
383
|
const midX = (x1 + x2) / 2;
|
|
328
384
|
const midY = (y1 + y2) / 2;
|
|
329
|
-
|
|
330
|
-
const cx1 = x1 + (x2 - x1) * 0.4;
|
|
331
|
-
const cx2 = x1 + (x2 - x1) * 0.6;
|
|
332
|
-
const isHovered = this._hoveredRelation === rel.id;
|
|
333
|
-
const label = rel.joinType.toUpperCase();
|
|
385
|
+
const joinLabel = rel.joins.map(j => `${j.parentField} = ${j.childField}`).join(', ');
|
|
334
386
|
return svg `
|
|
335
387
|
<g>
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
<!--
|
|
346
|
-
<
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
x=${midX - 18} y=${midY - 8}
|
|
355
|
-
width="36" height="16"
|
|
356
|
-
stroke=${color} stroke-width="0.5" />
|
|
357
|
-
<text class="relation-label"
|
|
358
|
-
x=${midX} y=${midY + 4}
|
|
359
|
-
text-anchor="middle"
|
|
360
|
-
fill=${color}>
|
|
361
|
-
${label}
|
|
388
|
+
<path class="rel-hit" d=${d}
|
|
389
|
+
@click=${() => this._onRelClick(rel)}
|
|
390
|
+
@mouseenter=${() => { this._hoveredRel = rel.id; }}
|
|
391
|
+
@mouseleave=${() => { this._hoveredRel = null; }} />
|
|
392
|
+
<path class="rel-path ${isHovered ? 'hovered' : ''} ${isSelected ? 'selected' : ''}"
|
|
393
|
+
d=${d} stroke=${color}
|
|
394
|
+
marker-start="url(#arr-${rel.joinType})"
|
|
395
|
+
marker-end="url(#arr-${rel.joinType})" />
|
|
396
|
+
|
|
397
|
+
<!-- Cardinality labels near endpoints -->
|
|
398
|
+
<text class="rel-card" x=${x1 + (x1 < x2 ? 14 : -14)} y=${y1 - 6} fill=${color} text-anchor=${x1 < x2 ? 'start' : 'end'}>${card.start}</text>
|
|
399
|
+
<text class="rel-card" x=${x2 + (x2 > x1 ? -14 : 14)} y=${y2 - 6} fill=${color} text-anchor=${x2 > x1 ? 'end' : 'start'}>${card.end}</text>
|
|
400
|
+
|
|
401
|
+
<!-- Center label -->
|
|
402
|
+
<rect class="rel-label-bg" x=${midX - 40} y=${midY - 9} width="80" height="18"
|
|
403
|
+
fill="#fff" stroke=${color} stroke-width="0.5" />
|
|
404
|
+
<text class="rel-label" x=${midX} y=${midY + 3} text-anchor="middle" fill=${color}>
|
|
405
|
+
${rel.joinType.toUpperCase()} ${joinLabel.length < 20 ? joinLabel : ''}
|
|
362
406
|
</text>
|
|
363
407
|
</g>
|
|
364
408
|
`;
|
|
365
409
|
}
|
|
366
|
-
|
|
410
|
+
// ─── Link Drag Preview ────────────────────────────────────────
|
|
411
|
+
_renderLinkPreview() {
|
|
412
|
+
if (!this._linkDrag)
|
|
413
|
+
return nothing;
|
|
414
|
+
const { fromX, fromY, currentX, currentY } = this._linkDrag;
|
|
367
415
|
return svg `
|
|
368
|
-
<
|
|
369
|
-
|
|
370
|
-
<marker id="arrow-${type}" viewBox="0 0 10 6"
|
|
371
|
-
refX="9" refY="3"
|
|
372
|
-
markerWidth="8" markerHeight="6"
|
|
373
|
-
orient="auto-start-reverse">
|
|
374
|
-
<path d="M 0 0 L 10 3 L 0 6 z" fill=${color} />
|
|
375
|
-
</marker>
|
|
376
|
-
`)}
|
|
377
|
-
</defs>
|
|
378
|
-
`;
|
|
379
|
-
}
|
|
380
|
-
render() {
|
|
381
|
-
if (this.dataSources.length === 0) {
|
|
382
|
-
return html `<div class="empty-state">No data sources to display.<br/>Add tables to see the ER diagram.</div>`;
|
|
383
|
-
}
|
|
384
|
-
return html `
|
|
385
|
-
<div class="er-container">
|
|
386
|
-
<svg width=${this._svgWidth} height=${this._svgHeight}
|
|
387
|
-
xmlns="http://www.w3.org/2000/svg">
|
|
388
|
-
${this._renderArrowDefs()}
|
|
389
|
-
|
|
390
|
-
<!-- Relation lines (behind tables) -->
|
|
391
|
-
${(this.relations || []).map(rel => this._renderRelationLine(rel))}
|
|
392
|
-
|
|
393
|
-
<!-- Table boxes -->
|
|
394
|
-
${this._tables.map(table => this._renderTableBox(table))}
|
|
395
|
-
</svg>
|
|
396
|
-
</div>
|
|
416
|
+
<line class="link-preview" x1=${fromX} y1=${fromY} x2=${currentX} y2=${currentY}
|
|
417
|
+
marker-end="url(#arr-preview)" />
|
|
397
418
|
`;
|
|
398
419
|
}
|
|
399
420
|
};
|
|
@@ -411,13 +432,22 @@ __decorate([
|
|
|
411
432
|
], ErDiagram.prototype, "_dragging", void 0);
|
|
412
433
|
__decorate([
|
|
413
434
|
state()
|
|
414
|
-
], ErDiagram.prototype, "
|
|
435
|
+
], ErDiagram.prototype, "_linkDrag", void 0);
|
|
436
|
+
__decorate([
|
|
437
|
+
state()
|
|
438
|
+
], ErDiagram.prototype, "_hoveredRel", void 0);
|
|
439
|
+
__decorate([
|
|
440
|
+
state()
|
|
441
|
+
], ErDiagram.prototype, "_selectedRel", void 0);
|
|
442
|
+
__decorate([
|
|
443
|
+
state()
|
|
444
|
+
], ErDiagram.prototype, "_svgW", void 0);
|
|
415
445
|
__decorate([
|
|
416
446
|
state()
|
|
417
|
-
], ErDiagram.prototype, "
|
|
447
|
+
], ErDiagram.prototype, "_svgH", void 0);
|
|
418
448
|
__decorate([
|
|
419
449
|
state()
|
|
420
|
-
], ErDiagram.prototype, "
|
|
450
|
+
], ErDiagram.prototype, "_tool", void 0);
|
|
421
451
|
ErDiagram = __decorate([
|
|
422
452
|
customElement('zrd-er-diagram')
|
|
423
453
|
], ErDiagram);
|