@erd-studio/renderer 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +9 -9
- package/dist/{chunk-H2ACJRPZ.js → chunk-HWA32XG5.js} +146 -50
- package/dist/editor.js +9 -9
- package/dist/index.js +3 -3
- package/dist/styles.css +70 -0
- package/dist/types/ErdCanvas.d.ts +4 -4
- package/dist/types/canvas/useCanvasGraph.d.ts +4 -4
- package/dist/types/components/DetailPanel/AliasEditor.d.ts +1 -1
- package/dist/types/components/DetailPanel/ColumnEditor.d.ts +2 -2
- package/dist/types/components/DetailPanel/ModelRationale.d.ts +5 -5
- package/dist/types/components/DetailPanel/RoleEditor.d.ts +3 -3
- package/dist/types/components/Graph/ColumnTooltip.d.ts +1 -1
- package/dist/types/components/Graph/ModelNode.d.ts +4 -4
- package/dist/types/components/Legend/Legend.d.ts +1 -1
- package/dist/types/components/common/ColumnRowEditor.d.ts +2 -2
- package/dist/types/editor.d.ts +2 -2
- package/dist/types/hooks/useColumnReorder.d.ts +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/lib/badgeLabels.d.ts +1 -1
- package/dist/types/lib/dragHandleSides.d.ts +2 -2
- package/dist/types/lib/graphTransformer.d.ts +7 -7
- package/dist/types/lib/modelLabels.d.ts +3 -3
- package/dist/types/lib/nodeOverlays.d.ts +4 -4
- package/dist/types/lib/nodeSizing.d.ts +6 -6
- package/dist/types/store/editorStore.d.ts +8 -8
- package/dist/types/types/graph.d.ts +22 -12
- package/package.json +3 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@erd-studio/renderer`. While `0.x`, a breaking change bumps the minor version.
|
|
4
|
+
|
|
5
|
+
## 0.3.0
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Pins `@erd-studio/core` 0.3.0, which reads schema v6, the table naming ([#181](https://github.com/liam-machine/erd-studio/issues/181)). No change to the renderer's own API: a v6 subject area reaches `ErdCanvas` as the same `DisplaySubjectArea` its v5 twin does.
|
|
9
|
+
|
|
10
|
+
## 0.2.0
|
|
11
|
+
|
|
12
|
+
### Changed (breaking)
|
|
13
|
+
- **Exports use the new words: table and subject area** ([#180](https://github.com/liam-machine/erd-studio/issues/180), part 2 of [#162](https://github.com/liam-machine/erd-studio/issues/162)). What was called a *model* is a **table**, and a *domain* is a **subject area**. Pins `@erd-studio/core` 0.2.0. The types re-exported from core carry its new names. Only names changed: every function behaves as before, and object shapes, property names (`DisplaySubjectArea.models`, `domainPath`, `ErdCanvas`'s `domain` prop, …), message `type` strings and the files read on disk are the same, so a `DisplaySubjectArea` from 0.2.0 is the `DisplayDomain` of 0.1.x. There are no aliases for the old names. Rename the imports:
|
|
14
|
+
|
|
15
|
+
| 0.1.x | 0.2.0 |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `computeModelLabels` | `computeTableLabels` |
|
|
18
|
+
| `DisplayDomain` | `DisplaySubjectArea` |
|
|
19
|
+
| `DisplayModel` | `DisplayTable` |
|
|
20
|
+
| `LabelledModel` | `LabelledTable` |
|
|
21
|
+
| `matchesModelSearch` | `matchesTableSearch` |
|
|
22
|
+
| `ModelFlowNode` | `TableFlowNode` |
|
|
23
|
+
| `ModelNode` | `TableNode` |
|
|
24
|
+
| `ModelNodeData` | `TableNodeData` |
|
|
25
|
+
| `ModelRole` | `TableRole` |
|
|
26
|
+
| `transformDomain` | `transformSubjectArea` |
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @erd-studio/renderer
|
|
2
2
|
|
|
3
|
-
The entity-relationship diagram canvas from the [ERD Studio](https://github.com/liam-machine/erd-studio) VS Code extension, packaged for reuse. It is built on React Flow and renders a `
|
|
3
|
+
The entity-relationship diagram canvas from the [ERD Studio](https://github.com/liam-machine/erd-studio) VS Code extension, packaged for reuse. It is built on React Flow and renders a `DisplaySubjectArea` from `@erd-studio/core`: model nodes with their columns and key badges, FK edges with cardinality, annotations, the detail panel and the legend.
|
|
4
4
|
|
|
5
5
|
Peer dependencies: `react` and `react-dom` 18.3+, `@xyflow/react` 12.4+.
|
|
6
6
|
|
|
@@ -8,8 +8,8 @@ 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 `
|
|
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 (`
|
|
11
|
+
| `@erd-studio/renderer` | Showing a domain: the `ErdCanvas` viewer component (below), plus `transformSubjectArea` (a `DisplaySubjectArea` 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 (`DisplaySubjectArea`, `TableFlowNode`, `FkFlowEdge`, …). |
|
|
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 (`TableNode`, `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. |
|
|
15
15
|
| `@erd-studio/renderer/styles.css` | The stylesheet: every component's CSS in cascade order, then the theme tokens. It `@import`s `@xyflow/react/dist/style.css`, which your bundler resolves. |
|
|
@@ -18,14 +18,14 @@ All four JavaScript entry points share their modules, so a store or context crea
|
|
|
18
18
|
|
|
19
19
|
## Viewer: `ErdCanvas`
|
|
20
20
|
|
|
21
|
-
`ErdCanvas` renders a `
|
|
21
|
+
`ErdCanvas` renders a `DisplaySubjectArea` read-only, the way the extension shows a read-only domain: model nodes with their columns and key badges, FK edges, annotations, the minimap, the legend, and the detail panel a click on a model opens. It presents no editing affordances and posts nothing anywhere.
|
|
22
22
|
|
|
23
23
|
```tsx
|
|
24
24
|
import { useRef, useState } from 'react';
|
|
25
|
-
import { ErdCanvas, type ErdCanvasHandle, type
|
|
25
|
+
import { ErdCanvas, type ErdCanvasHandle, type DisplaySubjectArea } from '@erd-studio/renderer';
|
|
26
26
|
import '@erd-studio/renderer/styles.css';
|
|
27
27
|
|
|
28
|
-
function Diagram({ domain }: { domain:
|
|
28
|
+
function Diagram({ domain }: { domain: DisplaySubjectArea }) {
|
|
29
29
|
const canvas = useRef<ErdCanvasHandle>(null);
|
|
30
30
|
const [moved, setMoved] = useState(false);
|
|
31
31
|
return (
|
|
@@ -51,12 +51,12 @@ Each `ErdCanvas` has its own store, so several can share a page.
|
|
|
51
51
|
|
|
52
52
|
## Exporting a diagram
|
|
53
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 `
|
|
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 `DisplaySubjectArea` gives the same text everywhere ERD Studio exports it.
|
|
55
55
|
|
|
56
56
|
```ts
|
|
57
|
-
import { exportDiagram, diagramExportFileName, type
|
|
57
|
+
import { exportDiagram, diagramExportFileName, type DisplaySubjectArea } from '@erd-studio/renderer';
|
|
58
58
|
|
|
59
|
-
function downloadDbml(domain:
|
|
59
|
+
function downloadDbml(domain: DisplaySubjectArea) {
|
|
60
60
|
const text = exportDiagram(domain, 'dbml'); // or 'mermaid'
|
|
61
61
|
const url = URL.createObjectURL(new Blob([text], { type: 'text/plain' }));
|
|
62
62
|
const link = Object.assign(document.createElement('a'), { href: url, download: diagramExportFileName(domain, 'dbml') });
|
|
@@ -102,7 +102,7 @@ function foldComposites(rels) {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
// src/lib/modelLabels.ts
|
|
105
|
-
function
|
|
105
|
+
function computeTableLabels(models) {
|
|
106
106
|
const baseOf = (m) => m.alias?.trim() || m.name;
|
|
107
107
|
const counts = /* @__PURE__ */ new Map();
|
|
108
108
|
for (const m of models) {
|
|
@@ -120,7 +120,7 @@ function computeModelLabels(models) {
|
|
|
120
120
|
}
|
|
121
121
|
return labels;
|
|
122
122
|
}
|
|
123
|
-
function
|
|
123
|
+
function matchesTableSearch(data, query) {
|
|
124
124
|
return data.modelName.toLowerCase().includes(query) || data.label !== void 0 && data.label.toLowerCase().includes(query);
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -153,7 +153,7 @@ function pickHandleSides(source, target) {
|
|
|
153
153
|
return dy >= 0 ? { sourceSide: "bottom", targetSide: "top" } : { sourceSide: "top", targetSide: "bottom" };
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
-
function
|
|
156
|
+
function transformSubjectArea(domain, options) {
|
|
157
157
|
const { models, relationships, viewConfig, layer, stage, layerConfig, readOnly } = domain;
|
|
158
158
|
const positions = viewConfig.positions ?? {};
|
|
159
159
|
const discrepancyMap = /* @__PURE__ */ new Map();
|
|
@@ -176,7 +176,7 @@ function transformDomain(domain, options) {
|
|
|
176
176
|
data: { ...data, isExpanded: options?.isExpanded?.(id) ?? false },
|
|
177
177
|
measured: void 0
|
|
178
178
|
});
|
|
179
|
-
const labels =
|
|
179
|
+
const labels = computeTableLabels(models);
|
|
180
180
|
const nodes = models.map((model) => {
|
|
181
181
|
const columns = mapColumns(model);
|
|
182
182
|
const position = positionMap.get(model.name) ?? DEFAULT_POSITION;
|
|
@@ -207,6 +207,7 @@ function transformDomain(domain, options) {
|
|
|
207
207
|
// untouched. Never derive it inside an overlay pass.
|
|
208
208
|
...model.provenance ? { provenance: model.provenance } : {},
|
|
209
209
|
...model.loadError ? { loadError: model.loadError } : {},
|
|
210
|
+
...model.loadWarning ? { loadWarning: model.loadWarning } : {},
|
|
210
211
|
...disc ? { discrepancy: disc } : {},
|
|
211
212
|
...disc && options?.discrepancyReport ? {
|
|
212
213
|
discrepancySourceStage: options.discrepancyReport.sourceStage,
|
|
@@ -277,6 +278,7 @@ function transformDomain(domain, options) {
|
|
|
277
278
|
cardinality: rel.cardinality,
|
|
278
279
|
...rel.role ? { role: rel.role } : {},
|
|
279
280
|
...rel.pairs ? { pairs: rel.pairs, compositeKey: rel.compositeKey } : {},
|
|
281
|
+
...rel.keysContradict && !readOnly ? { keysContradict: true } : {},
|
|
280
282
|
stage,
|
|
281
283
|
...readOnly ? { readOnly: true } : {},
|
|
282
284
|
...discStatus ? { discrepancyStatus: discStatus } : {},
|
|
@@ -402,7 +404,7 @@ function applyNodeOverlays(nodes, edges, state) {
|
|
|
402
404
|
return node;
|
|
403
405
|
}
|
|
404
406
|
const data = node.data;
|
|
405
|
-
const searchDimmed = query ? !
|
|
407
|
+
const searchDimmed = query ? !matchesTableSearch(data, query) : false;
|
|
406
408
|
const selectionDimmed = hasSelection && !connected.has(node.id);
|
|
407
409
|
const dimmed = searchDimmed || selectionDimmed;
|
|
408
410
|
const expanded = isExpanded(node.id);
|
|
@@ -478,7 +480,7 @@ function useCanvasGraph({
|
|
|
478
480
|
nodeDimensions,
|
|
479
481
|
isExpanded: isExpandedNow
|
|
480
482
|
};
|
|
481
|
-
let { nodes: newNodes, edges: newEdges } =
|
|
483
|
+
let { nodes: newNodes, edges: newEdges } = transformSubjectArea(domain, transformOptions);
|
|
482
484
|
newNodes = newNodes.map((n) => {
|
|
483
485
|
const prev = prevById.get(n.id);
|
|
484
486
|
return prev?.measured?.width != null && prev.measured.height != null ? { ...n, measured: prev.measured } : n;
|
|
@@ -1743,8 +1745,12 @@ function describeLoadError(error) {
|
|
|
1743
1745
|
}
|
|
1744
1746
|
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.";
|
|
1745
1747
|
}
|
|
1746
|
-
function
|
|
1747
|
-
const
|
|
1748
|
+
function describeLoadWarning(warning) {
|
|
1749
|
+
const key = warning.key ? `"${warning.key}"` : "A key";
|
|
1750
|
+
return `${key} is written twice (line ${warning.line}) \u2014 the last one is shown. Remove the other copy to edit this table.`;
|
|
1751
|
+
}
|
|
1752
|
+
function TableNodeComponent({ data, selected }) {
|
|
1753
|
+
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;
|
|
1748
1754
|
const openNodeContextMenu = useEditorStore((s) => s.openNodeContextMenu);
|
|
1749
1755
|
const send = useSend();
|
|
1750
1756
|
const viewer = useIsViewer();
|
|
@@ -1796,7 +1802,7 @@ function ModelNodeComponent({ data, selected }) {
|
|
|
1796
1802
|
}, [isExpanded, columnDiscrepancyMap, discrepancy]);
|
|
1797
1803
|
const isDiscExtra = discrepancy?.status === "extra";
|
|
1798
1804
|
const stageClass = isGhost ? "ghost" : stage;
|
|
1799
|
-
const title = label ? `${label} (
|
|
1805
|
+
const title = label ? `${label} (table ${modelName})` : modelName;
|
|
1800
1806
|
const modelMeta = useMemo2(() => metaRows(meta), [meta]);
|
|
1801
1807
|
const nameTip = useHoverTip(
|
|
1802
1808
|
ghostReason ? `${title} \u2014 ${GHOST_REASON_TITLE[ghostReason]}` : isDiscExtra ? `${title} \u2014 only in ${discrepancySourceStage ?? "this stage"}, not in ${discrepancyTargetStage ?? "the stage being compared"}` : title,
|
|
@@ -1853,6 +1859,21 @@ function ModelNodeComponent({ data, selected }) {
|
|
|
1853
1859
|
badgeTip.tip
|
|
1854
1860
|
] }),
|
|
1855
1861
|
grain && /* @__PURE__ */ jsx7("div", { className: "model-node__grain", title: grain, children: grain }),
|
|
1862
|
+
loadWarning && /* @__PURE__ */ jsxs5("div", { className: "model-node__load-warning", role: "status", children: [
|
|
1863
|
+
/* @__PURE__ */ jsx7("span", { className: "model-node__load-warning-text", children: describeLoadWarning(loadWarning) }),
|
|
1864
|
+
viewer ? null : /* @__PURE__ */ jsx7(
|
|
1865
|
+
"button",
|
|
1866
|
+
{
|
|
1867
|
+
type: "button",
|
|
1868
|
+
className: "model-node__load-error-open nodrag",
|
|
1869
|
+
onClick: (e) => {
|
|
1870
|
+
e.stopPropagation();
|
|
1871
|
+
send({ type: "openModelFile", payload: { modelName } });
|
|
1872
|
+
},
|
|
1873
|
+
children: "Open file"
|
|
1874
|
+
}
|
|
1875
|
+
)
|
|
1876
|
+
] }),
|
|
1856
1877
|
/* @__PURE__ */ jsxs5("div", { className: "model-node__columns", children: [
|
|
1857
1878
|
displayColumns.map((col, idx) => /* @__PURE__ */ jsx7(
|
|
1858
1879
|
ColumnRow,
|
|
@@ -1954,7 +1975,7 @@ function ModelNodeComponent({ data, selected }) {
|
|
|
1954
1975
|
}
|
|
1955
1976
|
);
|
|
1956
1977
|
}
|
|
1957
|
-
var
|
|
1978
|
+
var TableNode = memo(TableNodeComponent);
|
|
1958
1979
|
|
|
1959
1980
|
// src/components/Graph/FkEdge.tsx
|
|
1960
1981
|
import { memo as memo2, useCallback as useCallback9, useMemo as useMemo3, useState as useState6 } from "react";
|
|
@@ -1966,6 +1987,7 @@ import {
|
|
|
1966
1987
|
useStoreApi,
|
|
1967
1988
|
useInternalNode
|
|
1968
1989
|
} from "@xyflow/react";
|
|
1990
|
+
import { backwardsRelationshipNote } from "@erd-studio/core";
|
|
1969
1991
|
|
|
1970
1992
|
// src/lib/cardinalityUtils.ts
|
|
1971
1993
|
function swapCardinality(c) {
|
|
@@ -2140,7 +2162,9 @@ function FkEdgeComponent({
|
|
|
2140
2162
|
return positions;
|
|
2141
2163
|
}, [positionsKey, neighbourIds, storeApi]);
|
|
2142
2164
|
const host = useCanvasHost();
|
|
2165
|
+
const viewer = useIsViewer();
|
|
2143
2166
|
const [hovered, setHovered] = useState6(false);
|
|
2167
|
+
const [backwardsHovered, setBackwardsHovered] = useState6(false);
|
|
2144
2168
|
const handleSwap = useCallback9(
|
|
2145
2169
|
(e) => {
|
|
2146
2170
|
if (!data) return;
|
|
@@ -2320,7 +2344,39 @@ function FkEdgeComponent({
|
|
|
2320
2344
|
children: "!"
|
|
2321
2345
|
}
|
|
2322
2346
|
),
|
|
2323
|
-
!readOnly && discrepancyStatus !== "missing" && /* @__PURE__ */
|
|
2347
|
+
viewer ? null : data.keysContradict && !readOnly && discrepancyStatus !== "missing" && /* @__PURE__ */ jsxs6(
|
|
2348
|
+
"span",
|
|
2349
|
+
{
|
|
2350
|
+
className: `fk-edge__backwards${dimmed ? " fk-edge__backwards--dimmed" : ""}`,
|
|
2351
|
+
style: {
|
|
2352
|
+
transform: `translate(-9px, -50%) translate(${midX}px, ${midY}px)`
|
|
2353
|
+
},
|
|
2354
|
+
onMouseEnter: (e) => {
|
|
2355
|
+
e.stopPropagation();
|
|
2356
|
+
setBackwardsHovered(true);
|
|
2357
|
+
},
|
|
2358
|
+
onMouseLeave: (e) => {
|
|
2359
|
+
e.stopPropagation();
|
|
2360
|
+
setBackwardsHovered(false);
|
|
2361
|
+
},
|
|
2362
|
+
children: [
|
|
2363
|
+
/* @__PURE__ */ jsx8(
|
|
2364
|
+
"span",
|
|
2365
|
+
{
|
|
2366
|
+
className: "fk-edge__backwards-icon",
|
|
2367
|
+
role: "img",
|
|
2368
|
+
"aria-label": "Probably drawn backwards",
|
|
2369
|
+
title: backwardsRelationshipNote(data, data.pairs ?? [{ fromColumn, toColumn }]),
|
|
2370
|
+
children: "\u26A0"
|
|
2371
|
+
}
|
|
2372
|
+
),
|
|
2373
|
+
backwardsHovered && // Turn round is ⇄: a backwards many-to-one becomes the one-to-many it
|
|
2374
|
+
// reads as, which the host stores the right way round (#163).
|
|
2375
|
+
/* @__PURE__ */ jsx8("button", { className: "fk-edge__backwards-btn", onClick: handleSwap, title: "Turn this relationship round", children: "Turn round" })
|
|
2376
|
+
]
|
|
2377
|
+
}
|
|
2378
|
+
),
|
|
2379
|
+
!readOnly && discrepancyStatus !== "missing" && !(data.keysContradict && !viewer) && /* @__PURE__ */ jsx8(
|
|
2324
2380
|
"span",
|
|
2325
2381
|
{
|
|
2326
2382
|
className: `fk-edge__swap-zone${dimmed ? " fk-edge__swap-zone--dimmed" : ""}`,
|
|
@@ -2552,7 +2608,7 @@ function AnnotationNodeInner({ data }) {
|
|
|
2552
2608
|
{
|
|
2553
2609
|
className: "annotation-node__link-handle nodrag",
|
|
2554
2610
|
onMouseDown: handleLinkDragStart,
|
|
2555
|
-
title: "Drag to a
|
|
2611
|
+
title: "Drag to a table to link",
|
|
2556
2612
|
children: "\u{1F517}"
|
|
2557
2613
|
}
|
|
2558
2614
|
),
|
|
@@ -2667,7 +2723,7 @@ function AnnotationEdgeInner({
|
|
|
2667
2723
|
var AnnotationEdge = memo4(AnnotationEdgeInner);
|
|
2668
2724
|
|
|
2669
2725
|
// src/canvas/flowTypes.ts
|
|
2670
|
-
var canvasNodeTypes = { model:
|
|
2726
|
+
var canvasNodeTypes = { model: TableNode, annotation: AnnotationNode };
|
|
2671
2727
|
var canvasEdgeTypes = { fk: FkEdge, annotationLink: AnnotationEdge };
|
|
2672
2728
|
|
|
2673
2729
|
// src/lib/dragHandleSides.ts
|
|
@@ -2848,7 +2904,7 @@ function hasRationaleContent(rationale) {
|
|
|
2848
2904
|
rationale.measures
|
|
2849
2905
|
].some(Boolean);
|
|
2850
2906
|
}
|
|
2851
|
-
function
|
|
2907
|
+
function TableRationale({ modelName, rationale, modelRole, grain }) {
|
|
2852
2908
|
const viewer = useIsViewer();
|
|
2853
2909
|
const [open, setOpen] = useState9(false);
|
|
2854
2910
|
const [forceShow, setForceShow] = useState9(false);
|
|
@@ -2911,7 +2967,7 @@ function ModelRationale({ modelName, rationale, modelRole, grain }) {
|
|
|
2911
2967
|
rationale,
|
|
2912
2968
|
fieldKey: "purpose",
|
|
2913
2969
|
label: "Purpose",
|
|
2914
|
-
placeholder: "What requirements does this
|
|
2970
|
+
placeholder: "What requirements does this table meet?",
|
|
2915
2971
|
autoEdit: !viewer && forceShow && !rationale?.purpose
|
|
2916
2972
|
}
|
|
2917
2973
|
),
|
|
@@ -2922,7 +2978,7 @@ function ModelRationale({ modelName, rationale, modelRole, grain }) {
|
|
|
2922
2978
|
rationale,
|
|
2923
2979
|
fieldKey: "design",
|
|
2924
2980
|
label: "Design",
|
|
2925
|
-
placeholder: "Why was this
|
|
2981
|
+
placeholder: "Why was this table designed this way?"
|
|
2926
2982
|
}
|
|
2927
2983
|
),
|
|
2928
2984
|
modelRole && /* @__PURE__ */ jsx12(
|
|
@@ -2932,7 +2988,7 @@ function ModelRationale({ modelName, rationale, modelRole, grain }) {
|
|
|
2932
2988
|
rationale,
|
|
2933
2989
|
fieldKey: "roleChoice",
|
|
2934
2990
|
label: "Role Choice",
|
|
2935
|
-
placeholder: "Why was this
|
|
2991
|
+
placeholder: "Why was this table role selected?"
|
|
2936
2992
|
}
|
|
2937
2993
|
),
|
|
2938
2994
|
grain && /* @__PURE__ */ jsx12(
|
|
@@ -3013,7 +3069,7 @@ function DescriptionEditor({ modelName, description }) {
|
|
|
3013
3069
|
{
|
|
3014
3070
|
className: "detail-panel__rationale-add-btn",
|
|
3015
3071
|
onClick: () => setEditing(true),
|
|
3016
|
-
title: "Add
|
|
3072
|
+
title: "Add table description",
|
|
3017
3073
|
children: "+ Add Description"
|
|
3018
3074
|
}
|
|
3019
3075
|
) });
|
|
@@ -3028,7 +3084,7 @@ function DescriptionEditor({ modelName, description }) {
|
|
|
3028
3084
|
value,
|
|
3029
3085
|
onChange: (e) => setValue(e.target.value),
|
|
3030
3086
|
onKeyDown: handleKeyDown,
|
|
3031
|
-
placeholder: "Describe this
|
|
3087
|
+
placeholder: "Describe this table...",
|
|
3032
3088
|
rows: 3,
|
|
3033
3089
|
autoFocus: true
|
|
3034
3090
|
}
|
|
@@ -3161,7 +3217,7 @@ function GrainEditor({ modelName, grain }) {
|
|
|
3161
3217
|
|
|
3162
3218
|
// src/components/DetailPanel/AliasEditor.tsx
|
|
3163
3219
|
import { useCallback as useCallback14, useEffect as useEffect10, useRef as useRef8, useState as useState12 } from "react";
|
|
3164
|
-
import {
|
|
3220
|
+
import { TABLE_ALIAS_MAX_LENGTH, TABLE_ALIAS_RULE, isValidTableAlias } from "@erd-studio/core";
|
|
3165
3221
|
import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
3166
3222
|
function AliasEditor({ modelName, alias, readOnly }) {
|
|
3167
3223
|
const host = useCanvasHost();
|
|
@@ -3174,7 +3230,7 @@ function AliasEditor({ modelName, alias, readOnly }) {
|
|
|
3174
3230
|
}
|
|
3175
3231
|
}, [alias, editing]);
|
|
3176
3232
|
const trimmed = value.trim();
|
|
3177
|
-
const error = trimmed && !
|
|
3233
|
+
const error = trimmed && !isValidTableAlias(trimmed) ? TABLE_ALIAS_RULE : null;
|
|
3178
3234
|
const handleSave = useCallback14(() => {
|
|
3179
3235
|
if (error || settled.current) return;
|
|
3180
3236
|
settled.current = true;
|
|
@@ -3207,7 +3263,7 @@ function AliasEditor({ modelName, alias, readOnly }) {
|
|
|
3207
3263
|
}, []);
|
|
3208
3264
|
if (editing) {
|
|
3209
3265
|
return /* @__PURE__ */ jsxs12("div", { className: "detail-panel__metadata-row detail-panel__metadata-row--alias", children: [
|
|
3210
|
-
/* @__PURE__ */ jsx15("span", { className: "detail-panel__label detail-panel__label--wide", children: "
|
|
3266
|
+
/* @__PURE__ */ jsx15("span", { className: "detail-panel__label detail-panel__label--wide", children: "Warehouse name" }),
|
|
3211
3267
|
/* @__PURE__ */ jsxs12("span", { className: "detail-panel__alias-edit", children: [
|
|
3212
3268
|
/* @__PURE__ */ jsx15(
|
|
3213
3269
|
"input",
|
|
@@ -3215,26 +3271,26 @@ function AliasEditor({ modelName, alias, readOnly }) {
|
|
|
3215
3271
|
className: `detail-panel__alias-input${error ? " detail-panel__alias-input--error" : ""}`,
|
|
3216
3272
|
value,
|
|
3217
3273
|
placeholder: modelName,
|
|
3218
|
-
maxLength:
|
|
3274
|
+
maxLength: TABLE_ALIAS_MAX_LENGTH,
|
|
3219
3275
|
onChange: (e) => setValue(e.target.value),
|
|
3220
3276
|
onKeyDown: handleKeyDown,
|
|
3221
3277
|
onBlur: () => error ? handleCancel() : handleSave(),
|
|
3222
|
-
"aria-label": "
|
|
3278
|
+
"aria-label": "Warehouse name (alias)",
|
|
3223
3279
|
"aria-invalid": error ? true : void 0,
|
|
3224
3280
|
autoFocus: true
|
|
3225
3281
|
}
|
|
3226
3282
|
),
|
|
3227
|
-
/* @__PURE__ */ jsx15("span", { className: `detail-panel__alias-hint${error ? " detail-panel__alias-hint--error" : ""}`, children: error ?? "The table dbt builds (its alias). Leave blank to use the
|
|
3283
|
+
/* @__PURE__ */ jsx15("span", { className: `detail-panel__alias-hint${error ? " detail-panel__alias-hint--error" : ""}`, children: error ?? "The table dbt builds (its alias). Leave blank to use the table's name." })
|
|
3228
3284
|
] })
|
|
3229
3285
|
] });
|
|
3230
3286
|
}
|
|
3231
3287
|
return /* @__PURE__ */ jsxs12("div", { className: "detail-panel__metadata-row detail-panel__metadata-row--alias", children: [
|
|
3232
|
-
/* @__PURE__ */ jsx15("span", { className: "detail-panel__label detail-panel__label--wide", children: "
|
|
3288
|
+
/* @__PURE__ */ jsx15("span", { className: "detail-panel__label detail-panel__label--wide", children: "Warehouse name" }),
|
|
3233
3289
|
/* @__PURE__ */ jsx15(
|
|
3234
3290
|
"span",
|
|
3235
3291
|
{
|
|
3236
3292
|
className: `detail-panel__value${alias ? "" : " detail-panel__value--muted"}`,
|
|
3237
|
-
title: alias ? `Built as ${alias} (dbt alias); the
|
|
3293
|
+
title: alias ? `Built as ${alias} (dbt alias); the table's name is ${modelName}` : "Same as the table's name",
|
|
3238
3294
|
onDoubleClick: readOnly ? void 0 : startEditing,
|
|
3239
3295
|
children: alias || modelName
|
|
3240
3296
|
}
|
|
@@ -3244,8 +3300,8 @@ function AliasEditor({ modelName, alias, readOnly }) {
|
|
|
3244
3300
|
{
|
|
3245
3301
|
className: "detail-panel__rationale-edit-btn detail-panel__alias-edit-btn",
|
|
3246
3302
|
onClick: startEditing,
|
|
3247
|
-
title: "Set the
|
|
3248
|
-
"aria-label": "Edit
|
|
3303
|
+
title: "Set the warehouse name (alias)",
|
|
3304
|
+
"aria-label": "Edit warehouse name",
|
|
3249
3305
|
children: "\u270E"
|
|
3250
3306
|
}
|
|
3251
3307
|
)
|
|
@@ -3395,7 +3451,7 @@ function KeyBadgeGroup({
|
|
|
3395
3451
|
"span",
|
|
3396
3452
|
{
|
|
3397
3453
|
className: "key-badge-group__warning",
|
|
3398
|
-
title: "Multiple primary keys detected in this
|
|
3454
|
+
title: "Multiple primary keys detected in this table",
|
|
3399
3455
|
children: "!"
|
|
3400
3456
|
}
|
|
3401
3457
|
) : null;
|
|
@@ -4288,7 +4344,7 @@ function MetaEditor({ modelName, columnName, meta, readOnly, compact }) {
|
|
|
4288
4344
|
"span",
|
|
4289
4345
|
{
|
|
4290
4346
|
className: `meta-editor__text${text ? "" : " meta-editor__text--structured"}`,
|
|
4291
|
-
title: editable && !text ? "Edit nested values in the
|
|
4347
|
+
title: editable && !text ? "Edit nested values in the table's file" : formatMetaValue(value),
|
|
4292
4348
|
onDoubleClick: editable && text ? () => startEdit(key, value) : void 0,
|
|
4293
4349
|
children: formatMetaValue(value)
|
|
4294
4350
|
}
|
|
@@ -4635,6 +4691,7 @@ function ColumnEditor({ modelName, columns, readOnly, modelRole }) {
|
|
|
4635
4691
|
}
|
|
4636
4692
|
|
|
4637
4693
|
// src/components/DetailPanel/DetailPanel.tsx
|
|
4694
|
+
import { backwardsRelationshipNote as backwardsRelationshipNote2 } from "@erd-studio/core";
|
|
4638
4695
|
import { Fragment as Fragment4, jsx as jsx22, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
4639
4696
|
function columnsText(rel, end) {
|
|
4640
4697
|
const cols = columnPairs(rel).map((p) => end === "from" ? p.fromColumn : p.toColumn);
|
|
@@ -4703,10 +4760,10 @@ function DetailPanel() {
|
|
|
4703
4760
|
return "Only the upper/lower case differs \u2014 rename to another name first";
|
|
4704
4761
|
}
|
|
4705
4762
|
if (domain && trimmed !== selectedNode && domain.models.some((m) => same(m.name))) {
|
|
4706
|
-
return "A
|
|
4763
|
+
return "A table with this name already exists";
|
|
4707
4764
|
}
|
|
4708
4765
|
if (trimmed !== selectedNode && existingModels.some((m) => m.source === "logical" && same(m.name))) {
|
|
4709
|
-
return "A
|
|
4766
|
+
return "A table with this name already exists in the Table Library";
|
|
4710
4767
|
}
|
|
4711
4768
|
return "";
|
|
4712
4769
|
},
|
|
@@ -4775,6 +4832,21 @@ function DetailPanel() {
|
|
|
4775
4832
|
},
|
|
4776
4833
|
[host]
|
|
4777
4834
|
);
|
|
4835
|
+
const handleTurnRound = useCallback22(
|
|
4836
|
+
(rel) => {
|
|
4837
|
+
host.postMessage({
|
|
4838
|
+
type: "updateRelationship",
|
|
4839
|
+
payload: {
|
|
4840
|
+
fromModel: rel.fromModel,
|
|
4841
|
+
fromColumn: rel.fromColumn,
|
|
4842
|
+
toModel: rel.toModel,
|
|
4843
|
+
toColumn: rel.toColumn,
|
|
4844
|
+
cardinality: swapCardinality(rel.cardinality)
|
|
4845
|
+
}
|
|
4846
|
+
});
|
|
4847
|
+
},
|
|
4848
|
+
[host]
|
|
4849
|
+
);
|
|
4778
4850
|
const handleRelationshipClick = useCallback22(
|
|
4779
4851
|
(x, y, rel) => {
|
|
4780
4852
|
const edgeData = {
|
|
@@ -4857,8 +4929,8 @@ function DetailPanel() {
|
|
|
4857
4929
|
{
|
|
4858
4930
|
className: "detail-panel__title-edit-btn",
|
|
4859
4931
|
onClick: handleStartRename,
|
|
4860
|
-
title: "Rename
|
|
4861
|
-
"aria-label": "Rename
|
|
4932
|
+
title: "Rename table",
|
|
4933
|
+
"aria-label": "Rename table",
|
|
4862
4934
|
children: "\u270E"
|
|
4863
4935
|
}
|
|
4864
4936
|
),
|
|
@@ -4894,7 +4966,7 @@ function DetailPanel() {
|
|
|
4894
4966
|
!isReadOnly && /* @__PURE__ */ jsx22("div", { className: "detail-panel__section", children: /* @__PURE__ */ jsx22(GrainEditor, { modelName: model.name, grain: model.grain }) }),
|
|
4895
4967
|
(!isReadOnly && !viewer || model.meta && Object.keys(model.meta).length > 0) && /* @__PURE__ */ jsx22("div", { className: "detail-panel__section", children: /* @__PURE__ */ jsx22(MetaEditor, { modelName: model.name, meta: model.meta, readOnly: isReadOnly }) }),
|
|
4896
4968
|
(!viewer || hasRationaleContent(model.rationale)) && /* @__PURE__ */ jsx22("div", { className: "detail-panel__section", children: /* @__PURE__ */ jsx22(
|
|
4897
|
-
|
|
4969
|
+
TableRationale,
|
|
4898
4970
|
{
|
|
4899
4971
|
modelName: model.name,
|
|
4900
4972
|
rationale: model.rationale,
|
|
@@ -4904,7 +4976,7 @@ function DetailPanel() {
|
|
|
4904
4976
|
) }),
|
|
4905
4977
|
!isReadOnly && /* @__PURE__ */ jsx22("div", { className: "detail-panel__section detail-panel__section--actions", children: confirmingDelete ? /* @__PURE__ */ jsxs19(Fragment4, { children: [
|
|
4906
4978
|
/* @__PURE__ */ jsxs19("span", { className: "detail-panel__confirm-label", children: [
|
|
4907
|
-
"Remove
|
|
4979
|
+
"Remove table from subject area?",
|
|
4908
4980
|
totalRelationships > 0 ? ` (${totalRelationships} relationship(s) will also be removed)` : ""
|
|
4909
4981
|
] }),
|
|
4910
4982
|
/* @__PURE__ */ jsxs19("div", { className: "detail-panel__confirm-actions", children: [
|
|
@@ -4913,7 +4985,7 @@ function DetailPanel() {
|
|
|
4913
4985
|
{
|
|
4914
4986
|
className: "detail-panel__button detail-panel__button--danger",
|
|
4915
4987
|
onClick: handleDeleteModel,
|
|
4916
|
-
"aria-label": "Confirm remove
|
|
4988
|
+
"aria-label": "Confirm remove table",
|
|
4917
4989
|
children: "Yes, Remove"
|
|
4918
4990
|
}
|
|
4919
4991
|
),
|
|
@@ -4932,9 +5004,9 @@ function DetailPanel() {
|
|
|
4932
5004
|
{
|
|
4933
5005
|
className: "detail-panel__button detail-panel__button--danger",
|
|
4934
5006
|
onClick: () => setConfirmingDelete(true),
|
|
4935
|
-
title: "Remove this
|
|
4936
|
-
"aria-label": "Remove this
|
|
4937
|
-
children: "Remove from
|
|
5007
|
+
title: "Remove this table from the subject area",
|
|
5008
|
+
"aria-label": "Remove this table from the subject area",
|
|
5009
|
+
children: "Remove from Subject Area"
|
|
4938
5010
|
}
|
|
4939
5011
|
) }),
|
|
4940
5012
|
/* @__PURE__ */ jsx22("div", { className: "detail-panel__section", children: /* @__PURE__ */ jsx22(
|
|
@@ -4980,6 +5052,18 @@ function DetailPanel() {
|
|
|
4980
5052
|
] })
|
|
4981
5053
|
] }),
|
|
4982
5054
|
/* @__PURE__ */ jsx22("span", { className: "detail-panel__rel-cardinality", children: rel.cardinality }),
|
|
5055
|
+
viewer ? null : !isReadOnly && rel.keysContradict && /* @__PURE__ */ jsx22(
|
|
5056
|
+
"button",
|
|
5057
|
+
{
|
|
5058
|
+
className: "detail-panel__rel-backwards",
|
|
5059
|
+
onClick: (e) => {
|
|
5060
|
+
e.stopPropagation();
|
|
5061
|
+
handleTurnRound(rel);
|
|
5062
|
+
},
|
|
5063
|
+
title: `${backwardsRelationshipNote2(rel, rel.pairs ?? [rel])} Click to turn it round.`,
|
|
5064
|
+
children: "\u26A0 Turn round"
|
|
5065
|
+
}
|
|
5066
|
+
),
|
|
4983
5067
|
!isReadOnly && /* @__PURE__ */ jsx22(
|
|
4984
5068
|
"button",
|
|
4985
5069
|
{
|
|
@@ -5024,6 +5108,18 @@ function DetailPanel() {
|
|
|
5024
5108
|
/* @__PURE__ */ jsx22("span", { className: "detail-panel__rel-local", title: columnsText(rel, "to"), children: columnsText(rel, "to") })
|
|
5025
5109
|
] }),
|
|
5026
5110
|
/* @__PURE__ */ jsx22("span", { className: "detail-panel__rel-cardinality", children: rel.cardinality }),
|
|
5111
|
+
viewer ? null : !isReadOnly && rel.keysContradict && /* @__PURE__ */ jsx22(
|
|
5112
|
+
"button",
|
|
5113
|
+
{
|
|
5114
|
+
className: "detail-panel__rel-backwards",
|
|
5115
|
+
onClick: (e) => {
|
|
5116
|
+
e.stopPropagation();
|
|
5117
|
+
handleTurnRound(rel);
|
|
5118
|
+
},
|
|
5119
|
+
title: `${backwardsRelationshipNote2(rel, rel.pairs ?? [rel])} Click to turn it round.`,
|
|
5120
|
+
children: "\u26A0 Turn round"
|
|
5121
|
+
}
|
|
5122
|
+
),
|
|
5027
5123
|
!isReadOnly && /* @__PURE__ */ jsx22(
|
|
5028
5124
|
"button",
|
|
5029
5125
|
{
|
|
@@ -5135,7 +5231,7 @@ function Legend() {
|
|
|
5135
5231
|
] }),
|
|
5136
5232
|
/* @__PURE__ */ jsxs20("div", { className: "legend__item", children: [
|
|
5137
5233
|
/* @__PURE__ */ jsx23("svg", { className: "legend__edge-sample", viewBox: "0 0 48 8", "aria-hidden": "true", children: /* @__PURE__ */ jsx23("line", { x1: "0", y1: "4", x2: "48", y2: "4", className: "legend__edge legend__edge--annotation" }) }),
|
|
5138
|
-
/* @__PURE__ */ jsx23("span", { className: "legend__item-desc", children: "Note linked to
|
|
5234
|
+
/* @__PURE__ */ jsx23("span", { className: "legend__item-desc", children: "Note linked to table" })
|
|
5139
5235
|
] })
|
|
5140
5236
|
] })
|
|
5141
5237
|
] }),
|
|
@@ -5273,10 +5369,10 @@ function Legend() {
|
|
|
5273
5369
|
|
|
5274
5370
|
// src/ErdCanvas.tsx
|
|
5275
5371
|
import { jsx as jsx24, jsxs as jsxs21 } from "react/jsx-runtime";
|
|
5276
|
-
function
|
|
5372
|
+
function toRenderableSubjectArea(domain) {
|
|
5277
5373
|
let modelsChanged = false;
|
|
5278
5374
|
const models = (domain.models ?? []).map((model) => {
|
|
5279
|
-
const renderable =
|
|
5375
|
+
const renderable = toRenderableTable(model);
|
|
5280
5376
|
if (renderable !== model) modelsChanged = true;
|
|
5281
5377
|
return renderable;
|
|
5282
5378
|
});
|
|
@@ -5292,7 +5388,7 @@ function toRenderableDomain(domain) {
|
|
|
5292
5388
|
readOnly: true
|
|
5293
5389
|
};
|
|
5294
5390
|
}
|
|
5295
|
-
function
|
|
5391
|
+
function toRenderableTable(model) {
|
|
5296
5392
|
if (model.columns == null) return { ...model, columns: [] };
|
|
5297
5393
|
if (model.columns.every((col) => col.dataType != null)) return model;
|
|
5298
5394
|
return { ...model, columns: model.columns.map(toRenderableColumn) };
|
|
@@ -5481,7 +5577,7 @@ function ErdCanvasInner({
|
|
|
5481
5577
|
);
|
|
5482
5578
|
}
|
|
5483
5579
|
var ErdCanvas = forwardRef(function ErdCanvas2(props, ref) {
|
|
5484
|
-
const renderable = useMemo7(() =>
|
|
5580
|
+
const renderable = useMemo7(() => toRenderableSubjectArea(props.domain), [props.domain]);
|
|
5485
5581
|
const [store] = useState20(() => createCanvasStore({ domain: renderable }));
|
|
5486
5582
|
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 }) }) }) });
|
|
5487
5583
|
});
|
|
@@ -5506,10 +5602,10 @@ export {
|
|
|
5506
5602
|
relationshipDisplayKey,
|
|
5507
5603
|
relationshipEdgeId,
|
|
5508
5604
|
foldComposites,
|
|
5509
|
-
|
|
5510
|
-
|
|
5605
|
+
computeTableLabels,
|
|
5606
|
+
matchesTableSearch,
|
|
5511
5607
|
pickHandleSides,
|
|
5512
|
-
|
|
5608
|
+
transformSubjectArea,
|
|
5513
5609
|
applyNodeOverlays,
|
|
5514
5610
|
useCanvasGraph,
|
|
5515
5611
|
STAGE_HEX,
|
|
@@ -5531,7 +5627,7 @@ export {
|
|
|
5531
5627
|
ADDITIVE_TITLE,
|
|
5532
5628
|
DISCREPANCY_PHRASE,
|
|
5533
5629
|
edgeHoverText,
|
|
5534
|
-
|
|
5630
|
+
TableNode,
|
|
5535
5631
|
swapCardinality,
|
|
5536
5632
|
FkEdge,
|
|
5537
5633
|
ANNOTATION_COLORS,
|