fix(diagram): display relationship cardinalities
This commit is contained in:
12 files changed
+316
-28
No files matched your search
@@ -14,7 +14,7 @@ import { DIAGRAM_SQL_TYPES, isSchemaAware as isSchemaAwareDatabase } from "@/lib
|
||||
import { databaseOptionsForConnection } from "@/composables/useDatabaseOptions";
|
||||
import { buildDiagramJoinSql, buildDiagramRelationships, filterDiagramTables, layoutDiagramTables, normalizeCustomDiagramRelationship, type CustomDiagramRelationship, type DiagramPosition, type DiagramRelationship, type DiagramTable } from "@/lib/diagram/erDiagram";
|
||||
import { buildEngineeringDiagram } from "@/lib/diagram/engineeringDiagram";
|
||||
import { buildEngineeringDiagramSvg, buildTableDiagramSvg, diagramSvgFileName } from "@/lib/export/diagramSvgExport";
|
||||
import { buildEngineeringDiagramSvg, buildTableDiagramSvg, diagramSvgFileName, type TableDiagramRelationshipLayout } from "@/lib/export/diagramSvgExport";
|
||||
import { clampDiagramZoom, zoomFromGestureScale, zoomFromWheelDelta } from "@/lib/diagram/diagramZoom";
|
||||
import { Copy, Download, KeyRound, Link2, Loader2, Maximize2, Network, Plus, RefreshCw, Search, Table2, Trash2, X, ZoomIn, ZoomOut } from "@lucide/vue";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
@@ -88,7 +88,7 @@ const relationshipDraft = ref({
|
||||
sourceColumn: "",
|
||||
targetTable: "",
|
||||
targetColumn: "",
|
||||
cardinality: "one-to-many" as "one-to-one" | "one-to-many" | "many-to-one",
|
||||
cardinality: "one-to-many" as "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many",
|
||||
});
|
||||
|
||||
const sqlConnections = computed(() => store.connections.filter((connection) => DIAGRAM_SQL_TYPES.has(connection.db_type)));
|
||||
@@ -253,6 +253,7 @@ function defaultRelationshipName(relationship: Omit<CustomDiagramRelationship, "
|
||||
function relationshipCardinality(): Pick<CustomDiagramRelationship, "sourceCardinality" | "targetCardinality"> {
|
||||
if (relationshipDraft.value.cardinality === "one-to-one") return { sourceCardinality: "1", targetCardinality: "1" };
|
||||
if (relationshipDraft.value.cardinality === "many-to-one") return { sourceCardinality: "N", targetCardinality: "1" };
|
||||
if (relationshipDraft.value.cardinality === "many-to-many") return { sourceCardinality: "N", targetCardinality: "N" };
|
||||
return { sourceCardinality: "1", targetCardinality: "N" };
|
||||
}
|
||||
|
||||
@@ -423,10 +424,17 @@ function candidateRouteXs(source: TableRect, target: TableRect): number[] {
|
||||
});
|
||||
}
|
||||
|
||||
function relationshipPath(relationship: DiagramRelationship): string {
|
||||
function relationshipLayout(relationship: DiagramRelationship): TableDiagramRelationshipLayout {
|
||||
const source = getTableRect(relationship.sourceTable);
|
||||
const target = getTableRect(relationship.targetTable);
|
||||
if (!source || !target) return "";
|
||||
if (!source || !target) {
|
||||
return {
|
||||
path: "",
|
||||
routePoints: [],
|
||||
sourceCardinality: { x: 0, y: 0 },
|
||||
targetCardinality: { x: 0, y: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const y1 = columnAnchorY(relationship.sourceTable, relationship.sourceColumn);
|
||||
const y2 = columnAnchorY(relationship.targetTable, relationship.targetColumn);
|
||||
@@ -444,9 +452,29 @@ function relationshipPath(relationship: DiagramRelationship): string {
|
||||
|
||||
const x1 = routeSideX(source, routeX, 2);
|
||||
const x2 = routeSideX(target, routeX, 2);
|
||||
return `M ${x1} ${y1} L ${routeX} ${y1} L ${routeX} ${y2} L ${x2} ${y2}`;
|
||||
const cardinalityOffset = 14;
|
||||
const cardinalityLift = 10;
|
||||
return {
|
||||
path: `M ${x1} ${y1} L ${routeX} ${y1} L ${routeX} ${y2} L ${x2} ${y2}`,
|
||||
routePoints: [
|
||||
{ x: x1, y: y1 },
|
||||
{ x: routeX, y: y1 },
|
||||
{ x: routeX, y: y2 },
|
||||
{ x: x2, y: y2 },
|
||||
],
|
||||
sourceCardinality: {
|
||||
x: x1 + (routeX < source.x ? -cardinalityOffset : cardinalityOffset),
|
||||
y: y1 - cardinalityLift,
|
||||
},
|
||||
targetCardinality: {
|
||||
x: x2 + (routeX < target.x ? -cardinalityOffset : cardinalityOffset),
|
||||
y: y2 - cardinalityLift,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const tableRelationshipLayouts = computed<Record<string, TableDiagramRelationshipLayout>>(() => Object.fromEntries(visibleRelationships.value.map((relationship) => [relationship.id, relationshipLayout(relationship)])));
|
||||
|
||||
function engineeringEntityCenter(tableName: string): DiagramPosition {
|
||||
const entity = engineeringDiagram.value.entities.find((item) => item.name === tableName);
|
||||
return entity ? { x: entity.x + entity.width / 2, y: entity.y + entity.height / 2 } : { x: 0, y: 0 };
|
||||
@@ -541,8 +569,12 @@ async function setSchema(value: string) {
|
||||
|
||||
async function loadTableDiagramData(tableName: string, querySchema: string): Promise<DiagramTable> {
|
||||
try {
|
||||
const [columns, foreignKeys] = await Promise.all([api.getColumns(connectionId.value, database.value, querySchema, tableName), api.listForeignKeys(connectionId.value, database.value, querySchema, tableName).catch(() => [])]);
|
||||
return { name: tableName, columns, foreignKeys };
|
||||
const [columns, foreignKeys, indexes] = await Promise.all([
|
||||
api.getColumns(connectionId.value, database.value, querySchema, tableName),
|
||||
api.listForeignKeys(connectionId.value, database.value, querySchema, tableName).catch(() => []),
|
||||
api.listIndexes(connectionId.value, database.value, querySchema, tableName).catch(() => []),
|
||||
]);
|
||||
return { name: tableName, columns, foreignKeys, indexes };
|
||||
} catch (e) {
|
||||
failedTableCount.value += 1;
|
||||
console.warn(`[diagram] failed to load table metadata: ${tableName}`, e);
|
||||
@@ -655,10 +687,6 @@ function resetZoomAndLayout() {
|
||||
resetLayout();
|
||||
}
|
||||
|
||||
function tableRelationshipPaths(): Record<string, string> {
|
||||
return Object.fromEntries(visibleRelationships.value.map((relationship) => [relationship.id, relationshipPath(relationship)]));
|
||||
}
|
||||
|
||||
function currentDiagramSvg(): string {
|
||||
if (diagramMode.value === "engineering") {
|
||||
return buildEngineeringDiagramSvg(engineeringDiagram.value);
|
||||
@@ -668,7 +696,7 @@ function currentDiagramSvg(): string {
|
||||
tables: visibleTables.value,
|
||||
relationships: visibleRelationships.value,
|
||||
positions: positions.value,
|
||||
relationshipPaths: tableRelationshipPaths(),
|
||||
relationshipLayouts: tableRelationshipLayouts.value,
|
||||
canvas: canvasSize.value,
|
||||
cardWidth: CARD_WIDTH,
|
||||
cardHeaderHeight: CARD_HEADER_HEIGHT,
|
||||
@@ -941,6 +969,7 @@ onUnmounted(stopDrag);
|
||||
<SelectContent>
|
||||
<SelectItem value="one-to-many">{{ t("diagram.cardinalityOneToMany") }}</SelectItem>
|
||||
<SelectItem value="many-to-one">{{ t("diagram.cardinalityManyToOne") }}</SelectItem>
|
||||
<SelectItem value="many-to-many">{{ t("diagram.cardinalityManyToMany") }}</SelectItem>
|
||||
<SelectItem value="one-to-one">{{ t("diagram.cardinalityOneToOne") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -1026,9 +1055,39 @@ onUnmounted(stopDrag);
|
||||
<path d="M 0 0 L 8 4 L 0 8 z" class="fill-primary/70" />
|
||||
</marker>
|
||||
</defs>
|
||||
<path v-for="relationship in visibleRelationships" :key="relationship.id" :d="relationshipPath(relationship)" class="fill-none stroke-primary/55" stroke-width="1.6" marker-end="url(#diagram-arrow)">
|
||||
<title>{{ relationshipTitle(relationship) }}</title>
|
||||
</path>
|
||||
<template v-for="relationship in visibleRelationships" :key="relationship.id">
|
||||
<path :d="tableRelationshipLayouts[relationship.id]?.path" class="fill-none stroke-primary/55" stroke-width="1.6" marker-end="url(#diagram-arrow)">
|
||||
<title>{{ relationshipTitle(relationship) }}</title>
|
||||
</path>
|
||||
<text
|
||||
:data-relationship-id="relationship.id"
|
||||
data-cardinality-end="source"
|
||||
:x="tableRelationshipLayouts[relationship.id]?.sourceCardinality.x"
|
||||
:y="tableRelationshipLayouts[relationship.id]?.sourceCardinality.y"
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle"
|
||||
paint-order="stroke"
|
||||
stroke-width="4"
|
||||
stroke-linejoin="round"
|
||||
class="fill-foreground stroke-background text-[12px] font-semibold"
|
||||
>
|
||||
{{ relationship.sourceCardinality }}
|
||||
</text>
|
||||
<text
|
||||
:data-relationship-id="relationship.id"
|
||||
data-cardinality-end="target"
|
||||
:x="tableRelationshipLayouts[relationship.id]?.targetCardinality.x"
|
||||
:y="tableRelationshipLayouts[relationship.id]?.targetCardinality.y"
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle"
|
||||
paint-order="stroke"
|
||||
stroke-width="4"
|
||||
stroke-linejoin="round"
|
||||
class="fill-foreground stroke-background text-[12px] font-semibold"
|
||||
>
|
||||
{{ relationship.targetCardinality }}
|
||||
</text>
|
||||
</template>
|
||||
</svg>
|
||||
|
||||
<div
|
||||
|
||||
Reference in new issue
Block a user