@erd-studio/renderer 0.1.2 → 0.1.4
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/README.md +19 -1
- package/dist/{chunk-5OQLRX7R.js → chunk-IAFK7ZZV.js} +195 -48
- package/dist/editor.js +21 -1
- package/dist/index.js +13 -1
- package/dist/styles.css +37 -0
- package/dist/types/editor.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/lib/badgeLabels.d.ts +6 -0
- package/dist/types/lib/dbtKeyTitle.d.ts +7 -0
- package/dist/types/lib/relationshipDisplayKey.d.ts +37 -0
- package/dist/types/types/graph.d.ts +22 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Peer dependencies: `react` and `react-dom` 18.3+, `@xyflow/react` 12.4+.
|
|
|
8
8
|
|
|
9
9
|
| Import | What it is for |
|
|
10
10
|
|---|---|
|
|
11
|
-
| `@erd-studio/renderer` | Showing a domain: the `ErdCanvas` viewer component (below), plus `transformDomain` (a `DisplayDomain` to React Flow nodes and edges), `pickHandleSides`, `repickHandleSides` / `nodeRect`, and the display, domain and graph types (`DisplayDomain`, `ModelFlowNode`, `FkFlowEdge`, …). |
|
|
11
|
+
| `@erd-studio/renderer` | Showing a domain: the `ErdCanvas` viewer component (below), plus `transformDomain` (a `DisplayDomain` to React Flow nodes and edges), `pickHandleSides`, `repickHandleSides` / `nodeRect`, the diagram exporters re-exported from `@erd-studio/core` (below), and the display, domain and graph types (`DisplayDomain`, `ModelFlowNode`, `FkFlowEdge`, …). |
|
|
12
12
|
| `@erd-studio/renderer/editor` | Everything above plus what a host needs to build a full editor around the canvas, as the extension's webview does: the store (below), the host adapter, `useCanvasGraph`, `CanvasBackdrop`, `canvasNodeTypes` / `canvasEdgeTypes`, the components (`ModelNode`, `FkEdge`, `AnnotationNode`, `AnnotationEdge`, `DetailPanel`, `Legend`, `KeyBadge`, `KeyBadgeGroup`, `DataTypeSelect`, `ColumnRowEditor`), the canvas hooks and the pure helpers. |
|
|
13
13
|
| `@erd-studio/renderer/store` | The canvas store on its own, with no React Flow runtime: `createCanvasSlice`, `createCanvasStore`, `CanvasStoreProvider`, `useEditorStore`, `useEditorStoreApi` and their types. |
|
|
14
14
|
| `@erd-studio/renderer/sizing` | Model node size estimation (`NODE_WIDTH`, `estimateNodeWidth`, `estimateNodeHeight`, `countVisibleColumnRows`, `resolveNodeDimensions`), for layout code that needs node sizes before React Flow has measured them. No components. |
|
|
@@ -49,6 +49,24 @@ The ref exposes `resetLayout()`, which puts every node back at its position in t
|
|
|
49
49
|
|
|
50
50
|
Each `ErdCanvas` has its own store, so several can share a page.
|
|
51
51
|
|
|
52
|
+
## Exporting a diagram
|
|
53
|
+
|
|
54
|
+
The `.` entry re-exports `@erd-studio/core`'s one-way exporters, so a page that shows a diagram with `ErdCanvas` can offer it as text without depending on core itself: `toMermaid`, `toDbml`, `exportDiagram`, `diagramExportFileName`, `DIAGRAM_EXPORT_FORMATS`, `DIAGRAM_EXPORT_FILE_EXTENSIONS` and the `DiagramExportFormat` type. They are pure functions with no React or DOM use; the same `DisplayDomain` gives the same text everywhere ERD Studio exports it.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { exportDiagram, diagramExportFileName, type DisplayDomain } from '@erd-studio/renderer';
|
|
58
|
+
|
|
59
|
+
function downloadDbml(domain: DisplayDomain) {
|
|
60
|
+
const text = exportDiagram(domain, 'dbml'); // or 'mermaid'
|
|
61
|
+
const url = URL.createObjectURL(new Blob([text], { type: 'text/plain' }));
|
|
62
|
+
const link = Object.assign(document.createElement('a'), { href: url, download: diagramExportFileName(domain, 'dbml') });
|
|
63
|
+
link.click();
|
|
64
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
See the [`@erd-studio/core` README](https://github.com/liam-machine/erd-studio/blob/main/packages/core/README.md#exporting-a-diagram) for what each format carries.
|
|
69
|
+
|
|
52
70
|
## Editor: store and host
|
|
53
71
|
|
|
54
72
|
The canvas components read their state from the store supplied by the nearest `CanvasStoreProvider`, and post the edits a user makes on the canvas (renaming a column, moving an annotation, changing a cardinality, …) as `CanvasEditMessage`s to the `CanvasHost` supplied by `CanvasEnvironmentProvider`.
|
|
@@ -66,6 +66,41 @@ function useIsViewer() {
|
|
|
66
66
|
// src/canvas/useCanvasGraph.ts
|
|
67
67
|
import { useCallback as useCallback2, useEffect, useRef } from "react";
|
|
68
68
|
|
|
69
|
+
// src/lib/relationshipDisplayKey.ts
|
|
70
|
+
function columnPairs(rel) {
|
|
71
|
+
return rel.pairs && rel.pairs.length > 0 ? rel.pairs.map((p) => ({ fromColumn: p.fromColumn, toColumn: p.toColumn })) : [{ fromColumn: rel.fromColumn, toColumn: rel.toColumn }];
|
|
72
|
+
}
|
|
73
|
+
function relationshipDisplayKey(rel) {
|
|
74
|
+
const pairs = columnPairs(rel);
|
|
75
|
+
return `${rel.fromModel}|${pairs.map((p) => p.fromColumn).join("+")}|${rel.toModel}|${pairs.map((p) => p.toColumn).join("+")}`;
|
|
76
|
+
}
|
|
77
|
+
function relationshipEdgeId(rel) {
|
|
78
|
+
const pairs = columnPairs(rel);
|
|
79
|
+
return `fk-${rel.fromModel}-${pairs.map((p) => p.fromColumn).join("+")}-${rel.toModel}-${pairs.map((p) => p.toColumn).join("+")}`;
|
|
80
|
+
}
|
|
81
|
+
function foldComposites(rels) {
|
|
82
|
+
const groups = /* @__PURE__ */ new Map();
|
|
83
|
+
const idOf = (r) => [r.fromModel, r.toModel, r.compositeKey].map((s) => s.toLowerCase()).join("\0");
|
|
84
|
+
for (const r of rels) {
|
|
85
|
+
if (!r.compositeKey) continue;
|
|
86
|
+
groups.set(idOf(r), [...groups.get(idOf(r)) ?? [], r]);
|
|
87
|
+
}
|
|
88
|
+
const out = [];
|
|
89
|
+
const placed = /* @__PURE__ */ new Set();
|
|
90
|
+
for (const r of rels) {
|
|
91
|
+
if (!r.compositeKey) {
|
|
92
|
+
out.push(r);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const id = idOf(r);
|
|
96
|
+
if (placed.has(id)) continue;
|
|
97
|
+
placed.add(id);
|
|
98
|
+
const members = groups.get(id);
|
|
99
|
+
out.push(members.length < 2 ? r : { ...members[0], pairs: members.map((m) => ({ fromColumn: m.fromColumn, toColumn: m.toColumn })) });
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
69
104
|
// src/lib/modelLabels.ts
|
|
70
105
|
function computeModelLabels(models) {
|
|
71
106
|
const baseOf = (m) => m.alias?.trim() || m.name;
|
|
@@ -102,7 +137,8 @@ function mapColumns(model) {
|
|
|
102
137
|
isNaturalKey: col.isNaturalKey,
|
|
103
138
|
...col.scdType != null ? { scdType: col.scdType } : {},
|
|
104
139
|
...col.additiveType ? { additiveType: col.additiveType } : {},
|
|
105
|
-
...hasMeta(col.meta) ? { meta: col.meta } : {}
|
|
140
|
+
...hasMeta(col.meta) ? { meta: col.meta } : {},
|
|
141
|
+
...col.dbtKey ? { dbtKey: col.dbtKey } : {}
|
|
106
142
|
}));
|
|
107
143
|
return mapped;
|
|
108
144
|
}
|
|
@@ -171,6 +207,7 @@ function transformDomain(domain, options) {
|
|
|
171
207
|
// untouched. Never derive it inside an overlay pass.
|
|
172
208
|
...model.provenance ? { provenance: model.provenance } : {},
|
|
173
209
|
...model.loadError ? { loadError: model.loadError } : {},
|
|
210
|
+
...model.loadWarning ? { loadWarning: model.loadWarning } : {},
|
|
174
211
|
...disc ? { discrepancy: disc } : {},
|
|
175
212
|
...disc && options?.discrepancyReport ? {
|
|
176
213
|
discrepancySourceStage: options.discrepancyReport.sourceStage,
|
|
@@ -218,20 +255,16 @@ function transformDomain(domain, options) {
|
|
|
218
255
|
const relDiscrepancyMap = /* @__PURE__ */ new Map();
|
|
219
256
|
if (options?.discrepancyReport) {
|
|
220
257
|
for (const rd of options.discrepancyReport.relationships) {
|
|
221
|
-
if (rd.status !== "matched")
|
|
222
|
-
const key = `${rd.fromModel}|${rd.fromColumn}|${rd.toModel}|${rd.toColumn}`;
|
|
223
|
-
relDiscrepancyMap.set(key, rd.status);
|
|
224
|
-
}
|
|
258
|
+
if (rd.status !== "matched") relDiscrepancyMap.set(relationshipDisplayKey(rd), rd.status);
|
|
225
259
|
}
|
|
226
260
|
}
|
|
227
261
|
const allNodeNames = new Set(positionMap.keys());
|
|
228
|
-
const edges = relationships.filter((rel) => allNodeNames.has(rel.fromModel) && allNodeNames.has(rel.toModel)).map((rel) => {
|
|
262
|
+
const edges = foldComposites(relationships).filter((rel) => allNodeNames.has(rel.fromModel) && allNodeNames.has(rel.toModel)).map((rel) => {
|
|
229
263
|
const isSelfLoop = rel.fromModel === rel.toModel;
|
|
230
264
|
const { sourceSide, targetSide } = isSelfLoop ? { sourceSide: "top", targetSide: "right" } : pickHandleSides(rectOf(rel.fromModel), rectOf(rel.toModel));
|
|
231
|
-
const
|
|
232
|
-
const discStatus = relDiscrepancyMap.get(relKey);
|
|
265
|
+
const discStatus = relDiscrepancyMap.get(relationshipDisplayKey(rel));
|
|
233
266
|
return {
|
|
234
|
-
id:
|
|
267
|
+
id: relationshipEdgeId(rel),
|
|
235
268
|
type: "fk",
|
|
236
269
|
source: rel.fromModel,
|
|
237
270
|
target: rel.toModel,
|
|
@@ -243,6 +276,8 @@ function transformDomain(domain, options) {
|
|
|
243
276
|
toModel: rel.toModel,
|
|
244
277
|
toColumn: rel.toColumn,
|
|
245
278
|
cardinality: rel.cardinality,
|
|
279
|
+
...rel.role ? { role: rel.role } : {},
|
|
280
|
+
...rel.pairs ? { pairs: rel.pairs, compositeKey: rel.compositeKey } : {},
|
|
246
281
|
stage,
|
|
247
282
|
...readOnly ? { readOnly: true } : {},
|
|
248
283
|
...discStatus ? { discrepancyStatus: discStatus } : {},
|
|
@@ -256,8 +291,10 @@ function transformDomain(domain, options) {
|
|
|
256
291
|
if (!positionMap.has(rd.fromModel) || !positionMap.has(rd.toModel)) continue;
|
|
257
292
|
const isSelfLoop = rd.fromModel === rd.toModel;
|
|
258
293
|
const { sourceSide, targetSide } = isSelfLoop ? { sourceSide: "top", targetSide: "right" } : pickHandleSides(rectOf(rd.fromModel), rectOf(rd.toModel));
|
|
294
|
+
const pairs = columnPairs(rd);
|
|
295
|
+
const composite = pairs.length > 1 ? { pairs, ...rd.compositeKey ? { compositeKey: rd.compositeKey } : {} } : {};
|
|
259
296
|
edges.push({
|
|
260
|
-
id: `ghost
|
|
297
|
+
id: `ghost-${relationshipEdgeId({ ...rd, pairs })}`,
|
|
261
298
|
type: "fk",
|
|
262
299
|
source: rd.fromModel,
|
|
263
300
|
target: rd.toModel,
|
|
@@ -269,6 +306,7 @@ function transformDomain(domain, options) {
|
|
|
269
306
|
toModel: rd.toModel,
|
|
270
307
|
toColumn: rd.toColumn,
|
|
271
308
|
cardinality: rd.sourceCardinality ?? rd.targetCardinality ?? "many-to-one",
|
|
309
|
+
...composite,
|
|
272
310
|
stage,
|
|
273
311
|
discrepancyStatus: "missing",
|
|
274
312
|
...isSelfLoop ? { isSelfLoop: true } : {}
|
|
@@ -277,6 +315,16 @@ function transformDomain(domain, options) {
|
|
|
277
315
|
}
|
|
278
316
|
}
|
|
279
317
|
}
|
|
318
|
+
const loopsByNode = /* @__PURE__ */ new Map();
|
|
319
|
+
for (const edge of edges) {
|
|
320
|
+
if (edge.type !== "fk" || !edge.data?.isSelfLoop) continue;
|
|
321
|
+
loopsByNode.set(edge.source, [...loopsByNode.get(edge.source) ?? [], edge]);
|
|
322
|
+
}
|
|
323
|
+
for (const loops of loopsByNode.values()) {
|
|
324
|
+
[...loops].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).forEach((edge, loopIndex) => {
|
|
325
|
+
edge.data = { ...edge.data, loopIndex };
|
|
326
|
+
});
|
|
327
|
+
}
|
|
280
328
|
const annotations = domain.viewConfig.annotations ?? [];
|
|
281
329
|
for (const ann of annotations) {
|
|
282
330
|
const annNodeId = `annotation-${ann.id}`;
|
|
@@ -499,8 +547,10 @@ function useCanvasGraph({
|
|
|
499
547
|
if (edge.type !== "fk") return;
|
|
500
548
|
if (edge.data) {
|
|
501
549
|
const cols = /* @__PURE__ */ new Set();
|
|
502
|
-
|
|
503
|
-
|
|
550
|
+
for (const pair of columnPairs(edge.data)) {
|
|
551
|
+
cols.add(`${edge.data.fromModel}:${pair.fromColumn}`);
|
|
552
|
+
cols.add(`${edge.data.toModel}:${pair.toColumn}`);
|
|
553
|
+
}
|
|
504
554
|
setHighlightedColumns(cols);
|
|
505
555
|
selectNode(null);
|
|
506
556
|
setDetailPanelOpen(false);
|
|
@@ -662,6 +712,19 @@ function useLongPressDrag(options = {}) {
|
|
|
662
712
|
};
|
|
663
713
|
}
|
|
664
714
|
|
|
715
|
+
// src/lib/dbtKeyTitle.ts
|
|
716
|
+
function dbtKeyTitle(hint) {
|
|
717
|
+
switch (hint.because) {
|
|
718
|
+
case "unique-test":
|
|
719
|
+
case "unique-combination":
|
|
720
|
+
return "dbt: tested as unique";
|
|
721
|
+
case "part-of-unique-combination":
|
|
722
|
+
return "dbt: part of a unique combination";
|
|
723
|
+
case "relationships-test":
|
|
724
|
+
return "dbt: has a relationships test";
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
665
728
|
// src/hooks/useColumnReorder.ts
|
|
666
729
|
import { useCallback as useCallback4, useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
|
|
667
730
|
function useColumnReorder({
|
|
@@ -1249,9 +1312,12 @@ var DISCREPANCY_PHRASE = {
|
|
|
1249
1312
|
"cardinality-mismatch": "cardinality differs between stages"
|
|
1250
1313
|
};
|
|
1251
1314
|
function edgeHoverText(edge) {
|
|
1315
|
+
const composite = edge.pairs && edge.pairs.length > 1;
|
|
1316
|
+
const ends = composite ? `${edge.fromModel} (${edge.pairs.map((p) => p.fromColumn).join(", ")}) \u2192 ${edge.toModel} (${edge.pairs.map((p) => p.toColumn).join(", ")})` : `${edge.fromModel}.${edge.fromColumn} \u2192 ${edge.toModel}.${edge.toColumn}`;
|
|
1252
1317
|
return [
|
|
1253
|
-
|
|
1318
|
+
ends,
|
|
1254
1319
|
edge.cardinality.replace(/-/g, " "),
|
|
1320
|
+
edge.role ? `role: ${edge.role}` : "",
|
|
1255
1321
|
edge.discrepancyStatus ? DISCREPANCY_PHRASE[edge.discrepancyStatus] : ""
|
|
1256
1322
|
].filter(Boolean).join(" \xB7 ");
|
|
1257
1323
|
}
|
|
@@ -1454,21 +1520,21 @@ function ColumnRow({ column, modelName, readOnly, existingColumnNames, discrepan
|
|
|
1454
1520
|
if (columnRow && modelNode) {
|
|
1455
1521
|
const targetColumnName = columnRow.dataset.columnName;
|
|
1456
1522
|
const targetModelName = modelNode.dataset.modelName;
|
|
1457
|
-
if (targetColumnName && targetModelName
|
|
1458
|
-
|
|
1459
|
-
new CustomEvent("column-relationship-drop"
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1523
|
+
if (targetColumnName && targetModelName) {
|
|
1524
|
+
if (targetModelName === modelName && targetColumnName.toLowerCase() === column.name.toLowerCase()) {
|
|
1525
|
+
window.dispatchEvent(new CustomEvent("column-relationship-self-drop"));
|
|
1526
|
+
} else {
|
|
1527
|
+
window.dispatchEvent(
|
|
1528
|
+
new CustomEvent("column-relationship-drop", {
|
|
1529
|
+
detail: {
|
|
1530
|
+
fromModel: modelName,
|
|
1531
|
+
fromColumn: column.name,
|
|
1532
|
+
toModel: targetModelName,
|
|
1533
|
+
toColumn: targetColumnName
|
|
1534
|
+
}
|
|
1535
|
+
})
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1472
1538
|
}
|
|
1473
1539
|
}
|
|
1474
1540
|
endDrag();
|
|
@@ -1567,7 +1633,8 @@ function ColumnRow({ column, modelName, readOnly, existingColumnNames, discrepan
|
|
|
1567
1633
|
"span",
|
|
1568
1634
|
{
|
|
1569
1635
|
className: `model-node__col-name${!readOnly ? " model-node__col-name--editable" : ""}`,
|
|
1570
|
-
title: column.name
|
|
1636
|
+
title: !viewer && column.dbtKey ? `${column.name}
|
|
1637
|
+
${dbtKeyTitle(column.dbtKey)}` : column.name,
|
|
1571
1638
|
onDoubleClick: !readOnly ? handleDoubleClickName : void 0,
|
|
1572
1639
|
children: column.name
|
|
1573
1640
|
}
|
|
@@ -1672,10 +1739,17 @@ function describeLoadError(error) {
|
|
|
1672
1739
|
if (error.kind === "read") {
|
|
1673
1740
|
return "ERD Studio can't read this file.";
|
|
1674
1741
|
}
|
|
1742
|
+
if (error.mergeConflict) {
|
|
1743
|
+
return error.line !== void 0 ? `Unresolved git merge conflict on line ${error.line} \u2014 ERD Studio can't read this file.` : "Unresolved git merge conflict \u2014 ERD Studio can't read this file.";
|
|
1744
|
+
}
|
|
1675
1745
|
return error.line !== void 0 ? `YAML error on line ${error.line} \u2014 ERD Studio can't read this file.` : "YAML error \u2014 ERD Studio can't read this file.";
|
|
1676
1746
|
}
|
|
1747
|
+
function describeLoadWarning(warning) {
|
|
1748
|
+
const key = warning.key ? `"${warning.key}"` : "A key";
|
|
1749
|
+
return `${key} is written twice (line ${warning.line}) \u2014 the last one is shown. Remove the other copy to edit this model.`;
|
|
1750
|
+
}
|
|
1677
1751
|
function ModelNodeComponent({ data, selected }) {
|
|
1678
|
-
const { modelName, label, stage, layer, layerConfig, schema, columns, grain, meta, dimmed, readOnly, isGhost, ghostReason, provenance, loadError, isStub, isExpanded = false, onToggleExpansion, discrepancy, discrepancySourceStage, discrepancyTargetStage } = data;
|
|
1752
|
+
const { modelName, label, stage, layer, layerConfig, schema, columns, grain, meta, dimmed, readOnly, isGhost, ghostReason, provenance, loadError, loadWarning, isStub, isExpanded = false, onToggleExpansion, discrepancy, discrepancySourceStage, discrepancyTargetStage } = data;
|
|
1679
1753
|
const openNodeContextMenu = useEditorStore((s) => s.openNodeContextMenu);
|
|
1680
1754
|
const send = useSend();
|
|
1681
1755
|
const viewer = useIsViewer();
|
|
@@ -1784,6 +1858,21 @@ function ModelNodeComponent({ data, selected }) {
|
|
|
1784
1858
|
badgeTip.tip
|
|
1785
1859
|
] }),
|
|
1786
1860
|
grain && /* @__PURE__ */ jsx7("div", { className: "model-node__grain", title: grain, children: grain }),
|
|
1861
|
+
loadWarning && /* @__PURE__ */ jsxs5("div", { className: "model-node__load-warning", role: "status", children: [
|
|
1862
|
+
/* @__PURE__ */ jsx7("span", { className: "model-node__load-warning-text", children: describeLoadWarning(loadWarning) }),
|
|
1863
|
+
viewer ? null : /* @__PURE__ */ jsx7(
|
|
1864
|
+
"button",
|
|
1865
|
+
{
|
|
1866
|
+
type: "button",
|
|
1867
|
+
className: "model-node__load-error-open nodrag",
|
|
1868
|
+
onClick: (e) => {
|
|
1869
|
+
e.stopPropagation();
|
|
1870
|
+
send({ type: "openModelFile", payload: { modelName } });
|
|
1871
|
+
},
|
|
1872
|
+
children: "Open file"
|
|
1873
|
+
}
|
|
1874
|
+
)
|
|
1875
|
+
] }),
|
|
1787
1876
|
/* @__PURE__ */ jsxs5("div", { className: "model-node__columns", children: [
|
|
1788
1877
|
displayColumns.map((col, idx) => /* @__PURE__ */ jsx7(
|
|
1789
1878
|
ColumnRow,
|
|
@@ -2017,6 +2106,7 @@ import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs6 } from "react/jsx-run
|
|
|
2017
2106
|
var PATH_GAP = 20;
|
|
2018
2107
|
var LABEL_OFFSET = 8;
|
|
2019
2108
|
var SELF_LOOP_RADIUS = 55;
|
|
2109
|
+
var SELF_LOOP_STEP = 18;
|
|
2020
2110
|
var NO_SIBLINGS = Object.freeze([]);
|
|
2021
2111
|
function offsetPoint(x, y, position, distance) {
|
|
2022
2112
|
switch (position) {
|
|
@@ -2089,7 +2179,8 @@ function FkEdgeComponent({
|
|
|
2089
2179
|
[host, data]
|
|
2090
2180
|
);
|
|
2091
2181
|
if (!data) return null;
|
|
2092
|
-
const { cardinality, stage, discrepancyStatus, dimmed, readOnly, isSelfLoop, fromColumn, toColumn } = data;
|
|
2182
|
+
const { cardinality, role, stage, discrepancyStatus, dimmed, readOnly, isSelfLoop, fromColumn, toColumn } = data;
|
|
2183
|
+
const loopRadius = SELF_LOOP_RADIUS + SELF_LOOP_STEP * (data.loopIndex ?? 0);
|
|
2093
2184
|
let adjustedSourceX = sourceX;
|
|
2094
2185
|
let adjustedSourceY = sourceY;
|
|
2095
2186
|
let adjustedTargetX = targetX;
|
|
@@ -2122,7 +2213,7 @@ function FkEdgeComponent({
|
|
|
2122
2213
|
}
|
|
2123
2214
|
const src = offsetPoint(adjustedSourceX, adjustedSourceY, sourcePosition, PATH_GAP);
|
|
2124
2215
|
const tgt = offsetPoint(adjustedTargetX, adjustedTargetY, targetPosition, PATH_GAP);
|
|
2125
|
-
const edgePath = isSelfLoop ? `M ${src.x},${src.y} C ${src.x +
|
|
2216
|
+
const edgePath = isSelfLoop ? `M ${src.x},${src.y} C ${src.x + loopRadius},${src.y - loopRadius} ${tgt.x + loopRadius},${tgt.y - loopRadius} ${tgt.x},${tgt.y}` : getSmoothStepPath({
|
|
2126
2217
|
sourceX: src.x,
|
|
2127
2218
|
sourceY: src.y,
|
|
2128
2219
|
targetX: tgt.x,
|
|
@@ -2138,8 +2229,11 @@ function FkEdgeComponent({
|
|
|
2138
2229
|
toModel,
|
|
2139
2230
|
toColumn,
|
|
2140
2231
|
cardinality,
|
|
2141
|
-
|
|
2232
|
+
role,
|
|
2233
|
+
discrepancyStatus,
|
|
2234
|
+
pairs: data.pairs
|
|
2142
2235
|
});
|
|
2236
|
+
const compositeSize = data.pairs && data.pairs.length > 1 ? data.pairs.length : 0;
|
|
2143
2237
|
let sourceLabel;
|
|
2144
2238
|
let targetLabel;
|
|
2145
2239
|
switch (cardinality) {
|
|
@@ -2168,8 +2262,8 @@ function FkEdgeComponent({
|
|
|
2168
2262
|
const labelColorClass = discrepancyStatus ? `fk-edge__label--discrepancy-${discrepancyStatus}` : `fk-edge__label--${stage ?? "logical"}`;
|
|
2169
2263
|
const srcLabel = offsetPoint(adjustedSourceX, adjustedSourceY, sourcePosition, LABEL_OFFSET);
|
|
2170
2264
|
const tgtLabel = offsetPoint(adjustedTargetX, adjustedTargetY, targetPosition, LABEL_OFFSET);
|
|
2171
|
-
const midX = isSelfLoop ? (src.x + tgt.x) / 2 +
|
|
2172
|
-
const midY = isSelfLoop ? (src.y + tgt.y) / 2 -
|
|
2265
|
+
const midX = isSelfLoop ? (src.x + tgt.x) / 2 + loopRadius * 0.75 : (adjustedSourceX + adjustedTargetX) / 2;
|
|
2266
|
+
const midY = isSelfLoop ? (src.y + tgt.y) / 2 - loopRadius * 0.75 : (adjustedSourceY + adjustedTargetY) / 2;
|
|
2173
2267
|
return /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
2174
2268
|
/* @__PURE__ */ jsx8(
|
|
2175
2269
|
"path",
|
|
@@ -2211,6 +2305,30 @@ function FkEdgeComponent({
|
|
|
2211
2305
|
children: targetLabel
|
|
2212
2306
|
}
|
|
2213
2307
|
),
|
|
2308
|
+
role && /* @__PURE__ */ jsx8(
|
|
2309
|
+
"span",
|
|
2310
|
+
{
|
|
2311
|
+
className: `fk-edge__role ${labelColorClass}${dimmed ? " fk-edge__label--dimmed" : ""}`,
|
|
2312
|
+
style: {
|
|
2313
|
+
transform: `translate(-50%, calc(-100% - 10px)) translate(${midX}px, ${midY}px)`
|
|
2314
|
+
},
|
|
2315
|
+
children: role
|
|
2316
|
+
}
|
|
2317
|
+
),
|
|
2318
|
+
compositeSize > 0 && /* @__PURE__ */ jsxs6(
|
|
2319
|
+
"span",
|
|
2320
|
+
{
|
|
2321
|
+
className: `fk-edge__composite ${labelColorClass}${dimmed ? " fk-edge__label--dimmed" : ""}`,
|
|
2322
|
+
style: {
|
|
2323
|
+
transform: `translate(-50%, calc(100% + 6px)) translate(${midX}px, ${midY}px)`
|
|
2324
|
+
},
|
|
2325
|
+
title: `Composite foreign key: ${compositeSize} column pairs`,
|
|
2326
|
+
children: [
|
|
2327
|
+
"\u29C9 ",
|
|
2328
|
+
compositeSize
|
|
2329
|
+
]
|
|
2330
|
+
}
|
|
2331
|
+
),
|
|
2214
2332
|
discrepancyStatus === "cardinality-mismatch" && /* @__PURE__ */ jsx8(
|
|
2215
2333
|
"span",
|
|
2216
2334
|
{
|
|
@@ -4538,6 +4656,10 @@ function ColumnEditor({ modelName, columns, readOnly, modelRole }) {
|
|
|
4538
4656
|
|
|
4539
4657
|
// src/components/DetailPanel/DetailPanel.tsx
|
|
4540
4658
|
import { Fragment as Fragment4, jsx as jsx22, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
4659
|
+
function columnsText(rel, end) {
|
|
4660
|
+
const cols = columnPairs(rel).map((p) => end === "from" ? p.fromColumn : p.toColumn);
|
|
4661
|
+
return cols.length === 1 ? cols[0] : `(${cols.join(", ")})`;
|
|
4662
|
+
}
|
|
4541
4663
|
var SOURCE_NAME = {
|
|
4542
4664
|
catalog: "warehouse catalog",
|
|
4543
4665
|
yml: "your dbt .yml",
|
|
@@ -4659,6 +4781,7 @@ function DetailPanel() {
|
|
|
4659
4781
|
handleClose();
|
|
4660
4782
|
}, [selectedNode, host, handleClose]);
|
|
4661
4783
|
const handleDeleteRelationship = useCallback22(
|
|
4784
|
+
// A composite sends its first pair; the host removes the whole group (#133 L2).
|
|
4662
4785
|
(rel) => {
|
|
4663
4786
|
host.postMessage({
|
|
4664
4787
|
type: "removeRelationship",
|
|
@@ -4679,7 +4802,10 @@ function DetailPanel() {
|
|
|
4679
4802
|
fromColumn: rel.fromColumn,
|
|
4680
4803
|
toModel: rel.toModel,
|
|
4681
4804
|
toColumn: rel.toColumn,
|
|
4682
|
-
cardinality: rel.cardinality
|
|
4805
|
+
cardinality: rel.cardinality,
|
|
4806
|
+
// Carried so Edit opens with it — the dialog saves what it shows.
|
|
4807
|
+
...rel.role ? { role: rel.role } : {},
|
|
4808
|
+
...rel.pairs ? { pairs: rel.pairs, ...rel.compositeKey ? { compositeKey: rel.compositeKey } : {} } : {}
|
|
4683
4809
|
};
|
|
4684
4810
|
openEdgeContextMenu(x, y, edgeData);
|
|
4685
4811
|
},
|
|
@@ -4693,8 +4819,9 @@ function DetailPanel() {
|
|
|
4693
4819
|
if (!domain || !selectedNode) {
|
|
4694
4820
|
return { outgoing: [], incoming: [] };
|
|
4695
4821
|
}
|
|
4696
|
-
const
|
|
4697
|
-
const
|
|
4822
|
+
const rels = foldComposites(domain.relationships);
|
|
4823
|
+
const outgoing2 = rels.filter((r) => r.fromModel === selectedNode);
|
|
4824
|
+
const incoming2 = rels.filter((r) => r.toModel === selectedNode && r.fromModel !== selectedNode);
|
|
4698
4825
|
return { outgoing: outgoing2, incoming: incoming2 };
|
|
4699
4826
|
}, [domain, selectedNode]);
|
|
4700
4827
|
const isReadOnly = domain?.readOnly ?? false;
|
|
@@ -4862,14 +4989,14 @@ function DetailPanel() {
|
|
|
4862
4989
|
},
|
|
4863
4990
|
title: viewer ? void 0 : "Click to edit cardinality",
|
|
4864
4991
|
children: [
|
|
4865
|
-
/* @__PURE__ */ jsx22("span", { className: "detail-panel__rel-direction", title: "Outgoing FK", children: "\u2192" }),
|
|
4992
|
+
/* @__PURE__ */ jsx22("span", { className: "detail-panel__rel-direction", title: rel.toModel === rel.fromModel ? "Self-reference" : "Outgoing FK", children: rel.toModel === rel.fromModel ? "\u21BB" : "\u2192" }),
|
|
4866
4993
|
/* @__PURE__ */ jsxs19("span", { className: "detail-panel__rel-columns", children: [
|
|
4867
|
-
/* @__PURE__ */ jsx22("span", { className: "detail-panel__rel-local", title: rel
|
|
4994
|
+
/* @__PURE__ */ jsx22("span", { className: "detail-panel__rel-local", title: columnsText(rel, "from"), children: columnsText(rel, "from") }),
|
|
4868
4995
|
/* @__PURE__ */ jsx22("span", { className: "detail-panel__rel-arrow", children: "\u2192" }),
|
|
4869
|
-
/* @__PURE__ */ jsxs19("span", { className: "detail-panel__rel-target", title: `${rel.toModel}.${rel
|
|
4996
|
+
/* @__PURE__ */ jsxs19("span", { className: "detail-panel__rel-target", title: `${rel.toModel}.${columnsText(rel, "to")}`, children: [
|
|
4870
4997
|
rel.toModel,
|
|
4871
4998
|
".",
|
|
4872
|
-
rel
|
|
4999
|
+
columnsText(rel, "to")
|
|
4873
5000
|
] })
|
|
4874
5001
|
] }),
|
|
4875
5002
|
/* @__PURE__ */ jsx22("span", { className: "detail-panel__rel-cardinality", children: rel.cardinality }),
|
|
@@ -4888,7 +5015,7 @@ function DetailPanel() {
|
|
|
4888
5015
|
)
|
|
4889
5016
|
]
|
|
4890
5017
|
},
|
|
4891
|
-
`out-${rel
|
|
5018
|
+
`out-${relationshipEdgeId(rel)}`
|
|
4892
5019
|
)),
|
|
4893
5020
|
incoming.map((rel) => /* @__PURE__ */ jsxs19(
|
|
4894
5021
|
"div",
|
|
@@ -4908,13 +5035,13 @@ function DetailPanel() {
|
|
|
4908
5035
|
children: [
|
|
4909
5036
|
/* @__PURE__ */ jsx22("span", { className: "detail-panel__rel-direction", title: "Incoming FK", children: "\u2190" }),
|
|
4910
5037
|
/* @__PURE__ */ jsxs19("span", { className: "detail-panel__rel-columns", children: [
|
|
4911
|
-
/* @__PURE__ */ jsxs19("span", { className: "detail-panel__rel-target", title: `${rel.fromModel}.${rel
|
|
5038
|
+
/* @__PURE__ */ jsxs19("span", { className: "detail-panel__rel-target", title: `${rel.fromModel}.${columnsText(rel, "from")}`, children: [
|
|
4912
5039
|
rel.fromModel,
|
|
4913
5040
|
".",
|
|
4914
|
-
rel
|
|
5041
|
+
columnsText(rel, "from")
|
|
4915
5042
|
] }),
|
|
4916
5043
|
/* @__PURE__ */ jsx22("span", { className: "detail-panel__rel-arrow", children: "\u2192" }),
|
|
4917
|
-
/* @__PURE__ */ jsx22("span", { className: "detail-panel__rel-local", title: rel
|
|
5044
|
+
/* @__PURE__ */ jsx22("span", { className: "detail-panel__rel-local", title: columnsText(rel, "to"), children: columnsText(rel, "to") })
|
|
4918
5045
|
] }),
|
|
4919
5046
|
/* @__PURE__ */ jsx22("span", { className: "detail-panel__rel-cardinality", children: rel.cardinality }),
|
|
4920
5047
|
!isReadOnly && /* @__PURE__ */ jsx22(
|
|
@@ -4932,7 +5059,7 @@ function DetailPanel() {
|
|
|
4932
5059
|
)
|
|
4933
5060
|
]
|
|
4934
5061
|
},
|
|
4935
|
-
`in-${rel
|
|
5062
|
+
`in-${relationshipEdgeId(rel)}`
|
|
4936
5063
|
))
|
|
4937
5064
|
] })
|
|
4938
5065
|
] })
|
|
@@ -5379,12 +5506,26 @@ var ErdCanvas = forwardRef(function ErdCanvas2(props, ref) {
|
|
|
5379
5506
|
return /* @__PURE__ */ jsx24(CanvasStoreProvider, { store, children: /* @__PURE__ */ jsx24(CanvasEnvironmentProvider, { host: NOOP_CANVAS_HOST, viewer: true, children: /* @__PURE__ */ jsx24(ReactFlowProvider, { children: /* @__PURE__ */ jsx24(ErdCanvasInner, { ...props, domain: renderable, handleRef: ref }) }) }) });
|
|
5380
5507
|
});
|
|
5381
5508
|
|
|
5509
|
+
// src/index.ts
|
|
5510
|
+
import {
|
|
5511
|
+
toMermaid,
|
|
5512
|
+
toDbml,
|
|
5513
|
+
exportDiagram,
|
|
5514
|
+
diagramExportFileName,
|
|
5515
|
+
DIAGRAM_EXPORT_FORMATS,
|
|
5516
|
+
DIAGRAM_EXPORT_FILE_EXTENSIONS
|
|
5517
|
+
} from "@erd-studio/core";
|
|
5518
|
+
|
|
5382
5519
|
export {
|
|
5383
5520
|
NOOP_CANVAS_HOST,
|
|
5384
5521
|
CanvasEnvironmentProvider,
|
|
5385
5522
|
useCanvasHost,
|
|
5386
5523
|
useSend,
|
|
5387
5524
|
useIsViewer,
|
|
5525
|
+
columnPairs,
|
|
5526
|
+
relationshipDisplayKey,
|
|
5527
|
+
relationshipEdgeId,
|
|
5528
|
+
foldComposites,
|
|
5388
5529
|
computeModelLabels,
|
|
5389
5530
|
matchesModelSearch,
|
|
5390
5531
|
pickHandleSides,
|
|
@@ -5425,5 +5566,11 @@ export {
|
|
|
5425
5566
|
ColumnRowEditor,
|
|
5426
5567
|
DetailPanel,
|
|
5427
5568
|
Legend,
|
|
5428
|
-
ErdCanvas
|
|
5569
|
+
ErdCanvas,
|
|
5570
|
+
toMermaid,
|
|
5571
|
+
toDbml,
|
|
5572
|
+
exportDiagram,
|
|
5573
|
+
diagramExportFileName,
|
|
5574
|
+
DIAGRAM_EXPORT_FORMATS,
|
|
5575
|
+
DIAGRAM_EXPORT_FILE_EXTENSIONS
|
|
5429
5576
|
};
|
package/dist/editor.js
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
CanvasBackdrop,
|
|
9
9
|
CanvasEnvironmentProvider,
|
|
10
10
|
ColumnRowEditor,
|
|
11
|
+
DIAGRAM_EXPORT_FILE_EXTENSIONS,
|
|
12
|
+
DIAGRAM_EXPORT_FORMATS,
|
|
11
13
|
DISCREPANCY_PHRASE,
|
|
12
14
|
DataTypeSelect,
|
|
13
15
|
DetailPanel,
|
|
@@ -28,15 +30,23 @@ import {
|
|
|
28
30
|
applyNodeOverlays,
|
|
29
31
|
canvasEdgeTypes,
|
|
30
32
|
canvasNodeTypes,
|
|
33
|
+
columnPairs,
|
|
31
34
|
computeModelLabels,
|
|
35
|
+
diagramExportFileName,
|
|
32
36
|
edgeHoverText,
|
|
37
|
+
exportDiagram,
|
|
38
|
+
foldComposites,
|
|
33
39
|
getDataTypeColor,
|
|
34
40
|
matchesModelSearch,
|
|
35
41
|
nodeRect,
|
|
36
42
|
pickHandleSides,
|
|
43
|
+
relationshipDisplayKey,
|
|
44
|
+
relationshipEdgeId,
|
|
37
45
|
repickHandleSides,
|
|
38
46
|
stageNodeColor,
|
|
39
47
|
swapCardinality,
|
|
48
|
+
toDbml,
|
|
49
|
+
toMermaid,
|
|
40
50
|
transformDomain,
|
|
41
51
|
useCanvasGraph,
|
|
42
52
|
useCanvasHost,
|
|
@@ -45,7 +55,7 @@ import {
|
|
|
45
55
|
useIsViewer,
|
|
46
56
|
useLongPressDrag,
|
|
47
57
|
useSend
|
|
48
|
-
} from "./chunk-
|
|
58
|
+
} from "./chunk-IAFK7ZZV.js";
|
|
49
59
|
import "./chunk-QZGU5NZH.js";
|
|
50
60
|
import "./chunk-6XQXDZ4R.js";
|
|
51
61
|
import {
|
|
@@ -77,6 +87,8 @@ export {
|
|
|
77
87
|
CanvasEnvironmentProvider,
|
|
78
88
|
CanvasStoreProvider,
|
|
79
89
|
ColumnRowEditor,
|
|
90
|
+
DIAGRAM_EXPORT_FILE_EXTENSIONS,
|
|
91
|
+
DIAGRAM_EXPORT_FORMATS,
|
|
80
92
|
DISCREPANCY_PHRASE,
|
|
81
93
|
DataTypeSelect,
|
|
82
94
|
DetailPanel,
|
|
@@ -99,21 +111,29 @@ export {
|
|
|
99
111
|
applyNodeOverlays,
|
|
100
112
|
canvasEdgeTypes,
|
|
101
113
|
canvasNodeTypes,
|
|
114
|
+
columnPairs,
|
|
102
115
|
computeModelLabels,
|
|
103
116
|
countVisibleColumnRows,
|
|
104
117
|
createCanvasSlice,
|
|
105
118
|
createCanvasStore,
|
|
119
|
+
diagramExportFileName,
|
|
106
120
|
edgeHoverText,
|
|
107
121
|
estimateNodeHeight,
|
|
108
122
|
estimateNodeWidth,
|
|
123
|
+
exportDiagram,
|
|
124
|
+
foldComposites,
|
|
109
125
|
getDataTypeColor,
|
|
110
126
|
matchesModelSearch,
|
|
111
127
|
nodeRect,
|
|
112
128
|
pickHandleSides,
|
|
129
|
+
relationshipDisplayKey,
|
|
130
|
+
relationshipEdgeId,
|
|
113
131
|
repickHandleSides,
|
|
114
132
|
resolveNodeDimensions,
|
|
115
133
|
stageNodeColor,
|
|
116
134
|
swapCardinality,
|
|
135
|
+
toDbml,
|
|
136
|
+
toMermaid,
|
|
117
137
|
transformDomain,
|
|
118
138
|
useCanvasGraph,
|
|
119
139
|
useCanvasHost,
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DIAGRAM_EXPORT_FILE_EXTENSIONS,
|
|
3
|
+
DIAGRAM_EXPORT_FORMATS,
|
|
2
4
|
ErdCanvas,
|
|
5
|
+
diagramExportFileName,
|
|
6
|
+
exportDiagram,
|
|
3
7
|
nodeRect,
|
|
4
8
|
pickHandleSides,
|
|
5
9
|
repickHandleSides,
|
|
10
|
+
toDbml,
|
|
11
|
+
toMermaid,
|
|
6
12
|
transformDomain
|
|
7
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-IAFK7ZZV.js";
|
|
8
14
|
import "./chunk-DUQ2TTQH.js";
|
|
9
15
|
import "./chunk-WOTSQZCQ.js";
|
|
10
16
|
export {
|
|
17
|
+
DIAGRAM_EXPORT_FILE_EXTENSIONS,
|
|
18
|
+
DIAGRAM_EXPORT_FORMATS,
|
|
11
19
|
ErdCanvas,
|
|
20
|
+
diagramExportFileName,
|
|
21
|
+
exportDiagram,
|
|
12
22
|
nodeRect,
|
|
13
23
|
pickHandleSides,
|
|
14
24
|
repickHandleSides,
|
|
25
|
+
toDbml,
|
|
26
|
+
toMermaid,
|
|
15
27
|
transformDomain
|
|
16
28
|
};
|
package/dist/styles.css
CHANGED
|
@@ -693,6 +693,20 @@
|
|
|
693
693
|
color: var(--warning-fg);
|
|
694
694
|
text-align: center;
|
|
695
695
|
}
|
|
696
|
+
.model-node__load-warning {
|
|
697
|
+
display: flex;
|
|
698
|
+
align-items: center;
|
|
699
|
+
gap: 8px;
|
|
700
|
+
padding: 4px 10px;
|
|
701
|
+
background: var(--warning-bg);
|
|
702
|
+
color: var(--warning-fg);
|
|
703
|
+
border-bottom: 1px solid var(--warning-fg);
|
|
704
|
+
}
|
|
705
|
+
.model-node__load-warning-text {
|
|
706
|
+
flex: 1;
|
|
707
|
+
font-size: 11px;
|
|
708
|
+
line-height: 1.4;
|
|
709
|
+
}
|
|
696
710
|
.model-node__load-error-text {
|
|
697
711
|
font-size: 11px;
|
|
698
712
|
line-height: 1.4;
|
|
@@ -818,6 +832,29 @@
|
|
|
818
832
|
.fk-edge__label--discrepancy-cardinality-mismatch {
|
|
819
833
|
color: var(--discrepancy-mismatch);
|
|
820
834
|
}
|
|
835
|
+
.fk-edge__role {
|
|
836
|
+
position: absolute;
|
|
837
|
+
pointer-events: none;
|
|
838
|
+
padding: 1px 6px;
|
|
839
|
+
border-radius: 8px;
|
|
840
|
+
background: var(--panel-bg);
|
|
841
|
+
font-size: 11px;
|
|
842
|
+
font-weight: 600;
|
|
843
|
+
line-height: 1.4;
|
|
844
|
+
white-space: nowrap;
|
|
845
|
+
}
|
|
846
|
+
.fk-edge__composite {
|
|
847
|
+
position: absolute;
|
|
848
|
+
pointer-events: none;
|
|
849
|
+
padding: 0 5px;
|
|
850
|
+
border: 1px solid currentColor;
|
|
851
|
+
border-radius: 8px;
|
|
852
|
+
background: var(--panel-bg);
|
|
853
|
+
font-size: 10px;
|
|
854
|
+
font-weight: 600;
|
|
855
|
+
line-height: 1.4;
|
|
856
|
+
white-space: nowrap;
|
|
857
|
+
}
|
|
821
858
|
.fk-edge__mismatch-badge {
|
|
822
859
|
position: absolute;
|
|
823
860
|
display: flex;
|
package/dist/types/editor.d.ts
CHANGED
|
@@ -24,5 +24,6 @@ export * from './lib/stageColors.js';
|
|
|
24
24
|
export * from './lib/dataTypeColors.js';
|
|
25
25
|
export * from './lib/badgeLabels.js';
|
|
26
26
|
export { swapCardinality } from './lib/cardinalityUtils.js';
|
|
27
|
+
export { columnPairs, foldComposites, relationshipDisplayKey, relationshipEdgeId } from './lib/relationshipDisplayKey.js';
|
|
27
28
|
export { computeModelLabels, matchesModelSearch, type LabelledModel } from './lib/modelLabels.js';
|
|
28
29
|
export { ANNOTATION_COLORS } from './lib/annotationColors.js';
|
package/dist/types/index.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ export { ErdCanvas, type ErdCanvasProps, type ErdCanvasHandle } from './ErdCanva
|
|
|
2
2
|
export { transformDomain, pickHandleSides, type TransformResult, type TransformOptions, type NodeRect, type NodeDimensions, } from './lib/graphTransformer.js';
|
|
3
3
|
export { repickHandleSides, nodeRect } from './lib/dragHandleSides.js';
|
|
4
4
|
export type { DisplayDomain, DisplayModel, DisplayColumn, DisplayRelationship, PhysicalColumnSource, PhysicalProvenance, Annotation, AnnotationColor, Cardinality, Layer, ModelRole, NodePosition, Rationale, Stage, ViewConfig, LayerConfig, } from '@erd-studio/core';
|
|
5
|
+
export { toMermaid, toDbml, exportDiagram, diagramExportFileName, DIAGRAM_EXPORT_FORMATS, DIAGRAM_EXPORT_FILE_EXTENSIONS, type DiagramExportFormat, } from '@erd-studio/core';
|
|
5
6
|
export type { ModelFlowNode, ModelNodeData, FkFlowEdge, FkEdgeData, AnnotationFlowNode, AnnotationFlowEdge, ColumnDisplay, } from './types/graph.js';
|
|
@@ -52,7 +52,13 @@ export interface EdgeHoverInput {
|
|
|
52
52
|
toModel: string;
|
|
53
53
|
toColumn: string;
|
|
54
54
|
cardinality: string;
|
|
55
|
+
role?: string;
|
|
55
56
|
discrepancyStatus?: 'extra' | 'missing' | 'cardinality-mismatch';
|
|
57
|
+
/** A composite foreign key's column pairs (#133 L2). */
|
|
58
|
+
pairs?: ReadonlyArray<{
|
|
59
|
+
fromColumn: string;
|
|
60
|
+
toColumn: string;
|
|
61
|
+
}>;
|
|
56
62
|
}
|
|
57
63
|
/**
|
|
58
64
|
* Hover text for a relationship line: which columns it joins, its cardinality,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DbtKeyHint } from '@erd-studio/core';
|
|
2
|
+
/**
|
|
3
|
+
* The line a column's hover text gains for what dbt's tests say about it
|
|
4
|
+
* (#133 L1). It informs a new relationship's direction only, so it is never
|
|
5
|
+
* a badge: the key flags are the user's design decision.
|
|
6
|
+
*/
|
|
7
|
+
export declare function dbtKeyTitle(hint: DbtKeyHint): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One line per relationship, a composite foreign key included (#133 L2).
|
|
3
|
+
*
|
|
4
|
+
* The members of a composite (entries sharing a `compositeKey`) are folded
|
|
5
|
+
* into one relationship carrying all its column pairs: edges attach to node
|
|
6
|
+
* sides, not column rows, so N member lines between two nodes would only
|
|
7
|
+
* crowd the side. `fromColumn` / `toColumn` stay the first pair, which is what
|
|
8
|
+
* a delete or ⇄ sends — the host expands it to the group.
|
|
9
|
+
*/
|
|
10
|
+
import type { ColumnPair } from '@erd-studio/core';
|
|
11
|
+
type Ends = {
|
|
12
|
+
fromModel: string;
|
|
13
|
+
fromColumn: string;
|
|
14
|
+
toModel: string;
|
|
15
|
+
toColumn: string;
|
|
16
|
+
};
|
|
17
|
+
type WithPairs = Ends & {
|
|
18
|
+
pairs?: readonly ColumnPair[];
|
|
19
|
+
};
|
|
20
|
+
/** The column pairs a (possibly folded) relationship joins: its `pairs`, else its one pair. */
|
|
21
|
+
export declare function columnPairs(rel: WithPairs): ColumnPair[];
|
|
22
|
+
/** `from|a+b|to|c+d`: how an edge and its discrepancy entry find each other. */
|
|
23
|
+
export declare function relationshipDisplayKey(rel: WithPairs): string;
|
|
24
|
+
/** The React Flow edge id graphTransformer gives a relationship. */
|
|
25
|
+
export declare function relationshipEdgeId(rel: WithPairs): string;
|
|
26
|
+
/**
|
|
27
|
+
* Fold the members of each composite foreign key into one relationship with
|
|
28
|
+
* `pairs` (two or more), placed where its first member falls; everything else
|
|
29
|
+
* passes through unchanged. Members are grouped by from-model, to-model and
|
|
30
|
+
* `compositeKey`, without case.
|
|
31
|
+
*/
|
|
32
|
+
export declare function foldComposites<T extends Ends & {
|
|
33
|
+
compositeKey?: string;
|
|
34
|
+
}>(rels: readonly T[]): Array<T & {
|
|
35
|
+
pairs?: ColumnPair[];
|
|
36
|
+
}>;
|
|
37
|
+
export {};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* these shapes.
|
|
7
7
|
*/
|
|
8
8
|
import type { Node, Edge } from '@xyflow/react';
|
|
9
|
-
import type { AnnotationColor, Cardinality, Layer, Meta, ModelLoadError, ModelRole, Stage } from '@erd-studio/core';
|
|
9
|
+
import type { AnnotationColor, Cardinality, ColumnPair, DbtKeyHint, Layer, Meta, ModelLoadError, ModelLoadWarning, ModelRole, Stage } from '@erd-studio/core';
|
|
10
10
|
import type { LayerConfig } from '@erd-studio/core';
|
|
11
11
|
import type { ModelDiscrepancy } from '@erd-studio/core';
|
|
12
12
|
import type { PhysicalProvenance } from '@erd-studio/core';
|
|
@@ -27,6 +27,8 @@ export interface ColumnDisplay {
|
|
|
27
27
|
additiveType?: 'additive' | 'semi-additive' | 'non-additive';
|
|
28
28
|
/** Structured metadata (`meta:`) — shown in the tooltip. Absent when empty. */
|
|
29
29
|
meta?: Meta;
|
|
30
|
+
/** What dbt's tests say about the column (editable logical canvas only) — one line of its hover text. */
|
|
31
|
+
dbtKey?: DbtKeyHint;
|
|
30
32
|
}
|
|
31
33
|
/** Data payload for a ModelNode React Flow node. */
|
|
32
34
|
export type ModelNodeData = {
|
|
@@ -92,6 +94,11 @@ export type ModelNodeData = {
|
|
|
92
94
|
* node shows the error and an "Open file" button instead of "No columns".
|
|
93
95
|
*/
|
|
94
96
|
loadError?: ModelLoadError;
|
|
97
|
+
/**
|
|
98
|
+
* The model's file was read but needs fixing (logical stage only): a key
|
|
99
|
+
* written twice, whose last value is shown (#164). Absent from an older core.
|
|
100
|
+
*/
|
|
101
|
+
loadWarning?: ModelLoadWarning;
|
|
95
102
|
/** Whether this model is in stub display mode (PK/NK columns only). */
|
|
96
103
|
isStub: boolean;
|
|
97
104
|
/** Per-model discrepancy data when a cross-stage comparison report is active. */
|
|
@@ -119,6 +126,15 @@ export type FkEdgeData = {
|
|
|
119
126
|
toModel: string;
|
|
120
127
|
toColumn: string;
|
|
121
128
|
cardinality: Cardinality;
|
|
129
|
+
/** Optional label for the link, e.g. `ship date`, drawn at the edge's midpoint. */
|
|
130
|
+
role?: string;
|
|
131
|
+
/**
|
|
132
|
+
* A composite foreign key's column pairs (two or more; #133 L2). Absent for
|
|
133
|
+
* a single-column link; `fromColumn` / `toColumn` are always the first pair.
|
|
134
|
+
*/
|
|
135
|
+
pairs?: ColumnPair[];
|
|
136
|
+
/** The composite's `compositeKey` name, alongside `pairs`. */
|
|
137
|
+
compositeKey?: string;
|
|
122
138
|
/** Stage of the owning domain canvas — drives CSS colour class. */
|
|
123
139
|
stage?: Stage;
|
|
124
140
|
/** Discrepancy status for ghost/extra/mismatch edges. */
|
|
@@ -129,6 +145,11 @@ export type FkEdgeData = {
|
|
|
129
145
|
readOnly?: boolean;
|
|
130
146
|
/** True when source === target — renders a loop arc over the top-right corner. */
|
|
131
147
|
isSelfLoop?: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* For a self-loop: its place among the node's self-loops, by edge id (#133
|
|
150
|
+
* L3). Each one is drawn wider than the last, so loops nest, never cross.
|
|
151
|
+
*/
|
|
152
|
+
loopIndex?: number;
|
|
132
153
|
/** Index signature required by React Flow's Edge generic. */
|
|
133
154
|
[key: string]: unknown;
|
|
134
155
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erd-studio/renderer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "ERD Studio's React Flow canvas: the entity-relationship diagram renderer shared by the VS Code extension, with a read-only viewer mode.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"prepack": "npm run build"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@erd-studio/core": "0.1.
|
|
49
|
+
"@erd-studio/core": "0.1.4",
|
|
50
50
|
"zustand": "^5.0.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|