mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-02 02:34:42 +08:00
fix(diagram): align multi-schema export positions with canvas
This commit is contained in:
@@ -1843,7 +1843,9 @@ function currentDiagramSvg(): string {
|
||||
|
||||
prepareTableDiagramExportGeometry();
|
||||
|
||||
const exportTables = canvasVisibleTables.value;
|
||||
// Positions are keyed by the diagram table id (`getTableKey`), which is schema-qualified in
|
||||
// multi-schema mode; hand that id to the exporter so cards are not all drawn at the origin.
|
||||
const exportTables = canvasVisibleTables.value.map((table) => ({ ...table, diagramKey: getTableKey(table) }));
|
||||
const exportRelationships = canvasVisibleRelationships.value;
|
||||
const layers = exportSvgLayers();
|
||||
const geometryInput = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { EngineeringDiagram, EngineeringEntityNode } from "@/lib/diagram/engineeringDiagram";
|
||||
import type { DiagramPosition, DiagramRelationship, DiagramTable } from "@/lib/diagram/erDiagram";
|
||||
import { isDroppedColumn, type DiagramPosition, type DiagramRelationship, type DiagramTable } from "@/lib/diagram/erDiagram";
|
||||
import { pickHandles } from "@/lib/diagram/vue-flow-adapter";
|
||||
import { pointAlongPolyline, pointsToSvgPath, type Point } from "@/lib/diagram/edge-obstacle-router";
|
||||
import { CARD_BOTTOM_PADDING, CARD_HEADER_HEIGHT, CARD_WIDTH, COLUMN_ROW_HEIGHT, MARGIN } from "@/lib/diagram/diagram-constants";
|
||||
@@ -25,8 +25,27 @@ export interface DiagramSvgLayer {
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Table plus the id the canvas positions it by.
|
||||
*
|
||||
* Diagram positions are keyed by the diagram table id (`getTableKey`), which is
|
||||
* schema-qualified once more than one schema is selected. Export code used to look positions
|
||||
* up by the bare table name, so in multi-schema mode every card silently fell back to
|
||||
* `{ x: 0, y: 0 }` and the whole diagram was exported stacked on the origin (issue #9445).
|
||||
*/
|
||||
export type DiagramSvgTable = DiagramTable & { diagramKey?: string };
|
||||
|
||||
function tableExportKey(table: DiagramSvgTable): string {
|
||||
return table.diagramKey || table.name;
|
||||
}
|
||||
|
||||
/** Columns the card actually renders: pending-drop columns are hidden on the canvas too. */
|
||||
function tableExportColumns(table: DiagramTable): DiagramTable["columns"] {
|
||||
return table.columns.filter((column) => !isDroppedColumn(table, column.name));
|
||||
}
|
||||
|
||||
export interface TableDiagramSvgOptions {
|
||||
tables: DiagramTable[];
|
||||
tables: DiagramSvgTable[];
|
||||
relationships: DiagramRelationship[];
|
||||
positions: Record<string, DiagramPosition>;
|
||||
relationshipPaths: Record<string, string>;
|
||||
@@ -112,7 +131,7 @@ function orthogonalPointsBetweenTables(sourcePos: DiagramPosition, targetPos: Di
|
||||
type RelationshipGeometryInput = {
|
||||
relationships: DiagramRelationship[];
|
||||
positions: Record<string, DiagramPosition>;
|
||||
tables: DiagramTable[];
|
||||
tables: DiagramSvgTable[];
|
||||
waypoints?: Record<string, Point[]>;
|
||||
cardWidth?: number;
|
||||
cardHeaderHeight?: number;
|
||||
@@ -130,7 +149,14 @@ export function buildTableRelationshipPolylines(input: RelationshipGeometryInput
|
||||
columnRowHeight: input.columnRowHeight ?? COLUMN_ROW_HEIGHT,
|
||||
cardBottomPadding: input.cardBottomPadding ?? CARD_BOTTOM_PADDING,
|
||||
};
|
||||
const heightByName = new Map(input.tables.map((t) => [t.name, svgCardHeight(t.columns.length, metrics)]));
|
||||
// Relationships address tables by their diagram id, cards by their table; index both so the
|
||||
// routed edges match the drawn card heights in multi-schema mode too.
|
||||
const heightByName = new Map<string, number>();
|
||||
for (const table of input.tables) {
|
||||
const height = svgCardHeight(tableExportColumns(table).length, metrics);
|
||||
heightByName.set(tableExportKey(table), height);
|
||||
heightByName.set(table.name, height);
|
||||
}
|
||||
const polylines: Record<string, Point[]> = {};
|
||||
|
||||
for (const rel of input.relationships) {
|
||||
@@ -163,7 +189,7 @@ export function buildTableRelationshipPaths(input: RelationshipGeometryInput): R
|
||||
|
||||
/** Compute canvas size that fits tables + layers + relationship polylines with padding. */
|
||||
export function computeTableDiagramCanvas(
|
||||
tables: DiagramTable[],
|
||||
tables: DiagramSvgTable[],
|
||||
positions: Record<string, DiagramPosition>,
|
||||
options: {
|
||||
cardWidth: number;
|
||||
@@ -194,8 +220,8 @@ export function computeTableDiagramCanvas(
|
||||
}
|
||||
|
||||
for (const table of tables) {
|
||||
const pos = positions[table.name] ?? { x: 0, y: 0 };
|
||||
const height = svgCardHeight(table.columns.length, options);
|
||||
const pos = positions[tableExportKey(table)] ?? positions[table.name] ?? { x: 0, y: 0 };
|
||||
const height = svgCardHeight(tableExportColumns(table).length, options);
|
||||
expand(pos.x, pos.y, pos.x + options.cardWidth, pos.y + height);
|
||||
}
|
||||
|
||||
@@ -276,22 +302,28 @@ export function buildTableDiagramSvg(options: TableDiagramSvgOptions): string {
|
||||
parts.push("</g>");
|
||||
|
||||
for (const table of options.tables) {
|
||||
const position = options.positions[table.name] ?? { x: 0, y: 0 };
|
||||
const height = svgCardHeight(table.columns.length, options);
|
||||
const position = options.positions[tableExportKey(table)] ?? options.positions[table.name] ?? { x: 0, y: 0 };
|
||||
const columns = tableExportColumns(table);
|
||||
const height = svgCardHeight(columns.length, options);
|
||||
const showSchema = Boolean(table.schema) && tableExportKey(table) !== table.name;
|
||||
const nameY = options.cardHeaderHeight / 2 + (showSchema ? 6 : 0);
|
||||
parts.push(`<g transform="translate(${svgNumber(position.x)} ${svgNumber(position.y)})">`);
|
||||
parts.push(`<rect width="${options.cardWidth}" height="${svgNumber(height)}" rx="6" fill="#ffffff" stroke="#d4d4d8"/>`);
|
||||
parts.push(`<rect width="${options.cardWidth}" height="${options.cardHeaderHeight}" rx="6" fill="#f4f4f5"/>`);
|
||||
parts.push(`<path d="M 0 ${options.cardHeaderHeight} H ${options.cardWidth}" stroke="#e4e4e7"/>`);
|
||||
parts.push(svgText(table.name, 36, options.cardHeaderHeight / 2, { size: 13, weight: "600" }));
|
||||
if (showSchema) {
|
||||
parts.push(svgText(table.schema ?? "", 36, options.cardHeaderHeight / 2 - 6, { size: 10, fill: "#71717a", family: "Menlo, Consolas, monospace" }));
|
||||
}
|
||||
parts.push(svgText(table.name, 36, nameY, { size: 13, weight: "600" }));
|
||||
parts.push(
|
||||
svgText(String(table.columns.length), options.cardWidth - 18, options.cardHeaderHeight / 2, {
|
||||
svgText(String(columns.length), options.cardWidth - 18, options.cardHeaderHeight / 2, {
|
||||
size: 10,
|
||||
anchor: "end",
|
||||
fill: "#52525b",
|
||||
}),
|
||||
);
|
||||
|
||||
table.columns.forEach((column, index) => {
|
||||
columns.forEach((column, index) => {
|
||||
const rowTop = options.cardHeaderHeight + index * options.columnRowHeight;
|
||||
const rowCenter = rowTop + options.columnRowHeight / 2;
|
||||
parts.push(`<path d="M 0 ${svgNumber(rowTop)} H ${options.cardWidth}" stroke="#f0f0f1"/>`);
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
+88
@@ -378,3 +378,91 @@ test("buildTableDiagramSvg normalizes negative-origin content to viewBox 0 0", (
|
||||
assert.match(svg, new RegExp(`transform="translate\\(${-(canvas.originX ?? 0)} ${-(canvas.originY ?? 0)}\\)"`));
|
||||
assert.match(svg, /users/);
|
||||
});
|
||||
|
||||
// Regression for #9445: in multi-schema mode positions are keyed by the schema-qualified
|
||||
// diagram id, while the exporter used to look them up by the bare table name. Every card then
|
||||
// fell back to { x: 0, y: 0 } and the exported PNG showed all tables stacked on the origin.
|
||||
const multiSchemaTables: DiagramTable[] = [
|
||||
{
|
||||
name: "aaa",
|
||||
schema: "sales",
|
||||
columns: [
|
||||
{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
|
||||
{ name: "name", data_type: "varchar(255)", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
|
||||
{ name: "sex", data_type: "varchar(255)", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
|
||||
{ name: "desc1", data_type: "varchar(255)", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
|
||||
],
|
||||
foreignKeys: [],
|
||||
},
|
||||
{
|
||||
name: "bbb",
|
||||
schema: "ops",
|
||||
columns: [{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
|
||||
foreignKeys: [],
|
||||
},
|
||||
];
|
||||
|
||||
const multiSchemaPositions = { "sales.aaa": { x: 40, y: 40 }, "ops.bbb": { x: 480, y: 40 } };
|
||||
|
||||
test("multi-schema export positions cards by the schema-qualified diagram key", () => {
|
||||
const exportTables = multiSchemaTables.map((table) => ({ ...table, diagramKey: `${table.schema}.${table.name}` }));
|
||||
const canvas = computeTableDiagramCanvas(exportTables, multiSchemaPositions, {
|
||||
cardWidth: CARD_WIDTH,
|
||||
cardHeaderHeight: CARD_HEADER_HEIGHT,
|
||||
columnRowHeight: COLUMN_ROW_HEIGHT,
|
||||
cardBottomPadding: CARD_BOTTOM_PADDING,
|
||||
});
|
||||
// Both cards sit on one row: 40 .. 840 plus MARGIN padding on each side.
|
||||
assert.equal(canvas.width, 480 + CARD_WIDTH + 2 * MARGIN - 40);
|
||||
assert.equal(canvas.originX, 0);
|
||||
assert.equal(canvas.height, 40 + (CARD_HEADER_HEIGHT + 4 * COLUMN_ROW_HEIGHT + CARD_BOTTOM_PADDING) + MARGIN);
|
||||
|
||||
const svg = buildTableDiagramSvg({
|
||||
tables: exportTables,
|
||||
relationships: [],
|
||||
positions: multiSchemaPositions,
|
||||
relationshipPaths: {},
|
||||
canvas,
|
||||
cardWidth: CARD_WIDTH,
|
||||
cardHeaderHeight: CARD_HEADER_HEIGHT,
|
||||
columnRowHeight: COLUMN_ROW_HEIGHT,
|
||||
cardBottomPadding: CARD_BOTTOM_PADDING,
|
||||
});
|
||||
|
||||
assert.match(svg, /<g transform="translate\(40 40\)">/);
|
||||
assert.match(svg, /<g transform="translate\(480 40\)">/);
|
||||
// Cards must not collapse onto the origin, and the schema badge must survive the export.
|
||||
assert.doesNotMatch(svg, /<g transform="translate\(0 0\)"><rect width="360"/);
|
||||
assert.match(svg, />sales</);
|
||||
assert.match(svg, />ops</);
|
||||
});
|
||||
|
||||
test("multi-schema relationship routing measures card heights by diagram key", () => {
|
||||
const exportTables = multiSchemaTables.map((table) => ({ ...table, diagramKey: `${table.schema}.${table.name}` }));
|
||||
const relationships = [
|
||||
{
|
||||
id: "fk:aaa:bbb",
|
||||
name: "aaa_bbb_fk",
|
||||
sourceTable: "sales.aaa",
|
||||
sourceColumn: "id",
|
||||
targetTable: "ops.bbb",
|
||||
targetColumn: "id",
|
||||
},
|
||||
] as unknown as Parameters<typeof buildTableRelationshipPaths>[0]["relationships"];
|
||||
|
||||
const paths = buildTableRelationshipPaths({
|
||||
tables: exportTables,
|
||||
relationships,
|
||||
positions: multiSchemaPositions,
|
||||
cardWidth: CARD_WIDTH,
|
||||
cardHeaderHeight: CARD_HEADER_HEIGHT,
|
||||
columnRowHeight: COLUMN_ROW_HEIGHT,
|
||||
cardBottomPadding: CARD_BOTTOM_PADDING,
|
||||
});
|
||||
|
||||
const path = paths[relationships[0].id];
|
||||
assert.ok(path, "relationship path should be routed");
|
||||
// The route must follow the source card's real vertical centre, not the 0-column height.
|
||||
const sourceCenter = 40 + (CARD_HEADER_HEIGHT + 4 * COLUMN_ROW_HEIGHT + CARD_BOTTOM_PADDING) / 2;
|
||||
assert.ok(path.includes(`${sourceCenter}`), `path ${path} should use the source card centre ${sourceCenter}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user