This commit is contained in:
Francisco Galindo 2025-05-08 04:59:59 +00:00 committed by GitHub
commit 4247ab3950
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 482 additions and 73 deletions

View File

@ -2,6 +2,7 @@ import { useRef } from "react";
import { import {
Cardinality, Cardinality,
darkBgTheme, darkBgTheme,
Notation,
ObjectType, ObjectType,
Tab, Tab,
} from "../../data/constants"; } from "../../data/constants";
@ -10,6 +11,8 @@ import { useDiagram, useSettings, useLayout, useSelect } from "../../hooks";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { SideSheet } from "@douyinfe/semi-ui"; import { SideSheet } from "@douyinfe/semi-ui";
import RelationshipInfo from "../EditorSidePanel/RelationshipsTab/RelationshipInfo"; import RelationshipInfo from "../EditorSidePanel/RelationshipsTab/RelationshipInfo";
import { CrowOM, CrowOO, IDEFZM, DefaultNotation } from "./RelationshipFormat";
const labelFontSize = 16; const labelFontSize = 16;
@ -24,28 +27,70 @@ export default function Relationship({ data }) {
const pathRef = useRef(); const pathRef = useRef();
const labelRef = useRef(); const labelRef = useRef();
const type = settings.notation === 'default' ? 0 : 10;
const relationshipType=(5,type);
let direction = 1;
let cardinalityStart = "1"; let cardinalityStart = "1";
let cardinalityEnd = "1"; let cardinalityEnd = "1";
const formats = {
notation: {
default: {
one_to_one: DefaultNotation,
one_to_many: DefaultNotation,
many_to_one: DefaultNotation,
},
crows_foot: {
one_to_one: CrowOO,
one_to_many: CrowOM,
many_to_one: CrowOM,
},
idef1x: {
one_to_one: IDEFZM,
one_to_many: IDEFZM,
many_to_one: IDEFZM,
},
}
}
let format;
switch (data.cardinality) { switch (data.cardinality) {
// the translated values are to ensure backwards compatibility // the translated values are to ensure backwards compatibility
case t(Cardinality.MANY_TO_ONE): case t(Cardinality.MANY_TO_ONE):
case Cardinality.MANY_TO_ONE: case Cardinality.MANY_TO_ONE:
if (settings.notation === Notation.DEFAULT) {
cardinalityStart = "n"; cardinalityStart = "n";
cardinalityEnd = "1"; cardinalityEnd = "1";
} else {
cardinalityStart = "(1,*)";
cardinalityEnd = "(1,1)";
}
format = formats.notation[settings.notation].many_to_one;
break; break;
case t(Cardinality.ONE_TO_MANY): case t(Cardinality.ONE_TO_MANY):
case Cardinality.ONE_TO_MANY: case Cardinality.ONE_TO_MANY:
if (settings.notation === Notation.DEFAULT) {
cardinalityStart = "1"; cardinalityStart = "1";
cardinalityEnd = "n"; cardinalityEnd = "n";
} else {
cardinalityStart = "(1,1)";
cardinalityEnd = "(1,*)";
}
format = formats.notation[settings.notation].one_to_many;
break; break;
case t(Cardinality.ONE_TO_ONE): case t(Cardinality.ONE_TO_ONE):
case Cardinality.ONE_TO_ONE: case Cardinality.ONE_TO_ONE:
if (settings.notation === Notation.DEFAULT) {
cardinalityStart = "1"; cardinalityStart = "1";
cardinalityEnd = "1"; cardinalityEnd = "1";
} else {
cardinalityStart = "(1,1)";
cardinalityEnd = "(1,1)";
}
format = formats.notation[settings.notation].one_to_one;
break; break;
default: default:
format = formats.default.one_to_one;
break; break;
} }
@ -61,8 +106,9 @@ export default function Relationship({ data }) {
const cardinalityOffset = 28; const cardinalityOffset = 28;
if (pathRef.current) { if (pathRef.current) {
const pathLength = pathRef.current.getTotalLength(); const pathLength = pathRef.current.getTotalLength() - cardinalityOffset;
const labelPoint = pathRef.current.getPointAtLength(pathLength / 2); const labelPoint = pathRef.current.getPointAtLength(pathLength / 2);
labelX = labelPoint.x - (labelWidth ?? 0) / 2; labelX = labelPoint.x - (labelWidth ?? 0) / 2;
@ -71,8 +117,9 @@ export default function Relationship({ data }) {
const point1 = pathRef.current.getPointAtLength(cardinalityOffset); const point1 = pathRef.current.getPointAtLength(cardinalityOffset);
cardinalityStartX = point1.x; cardinalityStartX = point1.x;
cardinalityStartY = point1.y; cardinalityStartY = point1.y;
const point2 = pathRef.current.getPointAtLength( const point2 = pathRef.current.getPointAtLength(
pathLength - cardinalityOffset, pathLength,
); );
cardinalityEndX = point2.x; cardinalityEndX = point2.x;
cardinalityEndY = point2.y; cardinalityEndY = point2.y;
@ -101,6 +148,10 @@ export default function Relationship({ data }) {
} }
}; };
if ((settings.notation === Notation.CROWS_FOOT || settings.notation === Notation.IDEF1X) && cardinalityEndX < cardinalityStartX){
direction = -1;
}
return ( return (
<> <>
<g className="select-none group" onDoubleClick={edit}> <g className="select-none group" onDoubleClick={edit}>
@ -123,9 +174,24 @@ export default function Relationship({ data }) {
stroke="gray" stroke="gray"
className="group-hover:stroke-sky-700" className="group-hover:stroke-sky-700"
fill="none" fill="none"
strokeDasharray={relationshipType}
strokeWidth={2} strokeWidth={2}
cursor="pointer" cursor="pointer"
/> />
{settings.showCardinality && (
<>
{format(
pathRef,
cardinalityEndX,
cardinalityEndY,
cardinalityStartX,
cardinalityStartY,
direction,
cardinalityStart,
cardinalityEnd,
)}
</>
)}
{settings.showRelationshipLabels && ( {settings.showRelationshipLabels && (
<> <>
<rect <rect
@ -148,45 +214,8 @@ export default function Relationship({ data }) {
</text> </text>
</> </>
)} )}
{pathRef.current && settings.showCardinality && (
<>
<circle
cx={cardinalityStartX}
cy={cardinalityStartY}
r="12"
fill="grey"
className="group-hover:fill-sky-700"
/>
<text
x={cardinalityStartX}
y={cardinalityStartY}
fill="white"
strokeWidth="0.5"
textAnchor="middle"
alignmentBaseline="middle"
>
{cardinalityStart}
</text>
<circle
cx={cardinalityEndX}
cy={cardinalityEndY}
r="12"
fill="grey"
className="group-hover:fill-sky-700"
/>
<text
x={cardinalityEndX}
y={cardinalityEndY}
fill="white"
strokeWidth="0.5"
textAnchor="middle"
alignmentBaseline="middle"
>
{cardinalityEnd}
</text>
</>
)}
</g> </g>
<SideSheet <SideSheet
title={t("edit")} title={t("edit")}
size="small" size="small"

View File

@ -0,0 +1,256 @@
export function CrowOM(
pathRef,
cardinalityEndX,
cardinalityEndY,
cardinalityStartX,
cardinalityStartY,
direction,
cardinalityStart,
cardinalityEnd
) {
return(
pathRef &&(
<>
<line
x1={cardinalityEndX-(20*direction)}
y1={cardinalityEndY+15}
x2={cardinalityEndX-(20*direction)}
y2={cardinalityEndY-15}
stroke="gray"
strokeWidth='2'
className="group-hover:fill-sky-700"
/>
<line
x1={cardinalityEndX-(20*direction)}
y1={cardinalityEndY}
x2={cardinalityEndX+1}
y2={cardinalityEndY-10}
stroke="gray"
strokeWidth='2'
className="group-hover:fill-sky-700"
/>
<line
x1={cardinalityEndX-20*direction}
y1={cardinalityEndY}
x2={cardinalityEndX+1}
y2={cardinalityEndY+10}
stroke="gray"
strokeWidth='2'
className="group-hover:fill-sky-700"
/>
<text
x={cardinalityStartX-5}
y={cardinalityStartY-20}
fill= "gray"
strokeWidth="0.5"
textAnchor="middle"
alignmentBaseline="middle"
>
{cardinalityStart}
</text>
<text
x={cardinalityEndX-8}
y={cardinalityEndY-24}
fill="gray"
strokeWidth="0.5"
textAnchor="middle"
alignmentBaseline="middle"
>
{cardinalityEnd}
</text>
<line
x1={cardinalityStartX-(15*direction)}
y1={cardinalityStartY+10}
x2={cardinalityStartX-(15*direction)}
y2={cardinalityStartY-10}
stroke="gray"
strokeWidth='2'
className="group-hover:fill-sky-700"
/>
<line
x1={cardinalityStartX-(10*direction)}
y1={cardinalityStartY+10}
x2={cardinalityStartX-(10*direction)}
y2={cardinalityStartY-10}
stroke="gray"
strokeWidth='2'
className="group-hover:fill-sky-700"
/>
</>
)
)
}
export function CrowOO(
pathRef,
cardinalityEndX,
cardinalityEndY,
cardinalityStartX,
cardinalityStartY,
direction,
cardinalitySart,
cardinalityEnd
) {
return(
pathRef && (
<>
<line
x1={cardinalityEndX-(15*direction)}
y1={cardinalityEndY+10}
x2={cardinalityEndX-(15*direction)}
y2={cardinalityEndY-10}
stroke="gray"
strokeWidth='2'
className="group-hover:fill-sky-700"
/>
<line
x1={cardinalityEndX-(10*direction)}
y1={cardinalityEndY+10}
x2={cardinalityEndX-(10*direction)}
y2={cardinalityEndY-10}
stroke="gray"
strokeWidth='2'
className="group-hover:fill-sky-700"
/>
<line
x1={cardinalityStartX-(15*direction)}
y1={cardinalityStartY+10}
x2={cardinalityStartX-(15*direction)}
y2={cardinalityStartY-10}
stroke="gray"
strokeWidth='2'
className="group-hover:fill-sky-700"
/>
<line
x1={cardinalityStartX-(10*direction)}
y1={cardinalityStartY+10}
x2={cardinalityStartX-(10*direction)}
y2={cardinalityStartY-10}
stroke="gray"
strokeWidth='2'
className="group-hover:fill-sky-700"
/>
<text
x={cardinalityStartX-8}
y={cardinalityStartY-20}
fill="gray"
strokeWidth="0.5"
textAnchor="middle"
alignmentBaseline="middle"
>
{cardinalitySart}
</text>
<text
x={cardinalityEndX-15}
y={cardinalityEndY-20}
fill="gray"
strokeWidth="0.5"
textAnchor="middle"
alignmentBaseline="middle"
>
{cardinalityEnd}
</text>
</>
)
)
}
export function DefaultNotation(
pathRef,
cardinalityEndX,
cardinalityEndY, cardinalityStartX,
cardinalityStartY,
cardinalityStart,
cardinalityEnd
) {
return(
pathRef && (
<>
<circle
cx={cardinalityStartX}
cy={cardinalityStartY}
r="12"
fill="grey"
className="group-hover:fill-sky-700"
/>
<text
x={cardinalityStartX}
y={cardinalityStartY}
fill="white"
strokeWidth="0.5"
textAnchor="middle"
alignmentBaseline="middle"
>
{cardinalityStart}
</text>
<circle
cx={cardinalityEndX}
cy={cardinalityEndY}
r="12"
fill="grey"
className="group-hover:fill-sky-700"
/>
<text
x={cardinalityEndX}
y={cardinalityEndY}
fill="white"
strokeWidth="0.5"
textAnchor="middle"
alignmentBaseline="middle"
>
{cardinalityEnd}
</text>
</>
)
)
}
export function IDEFZM(
pathRef,
cardinalityEndX,
cardinalityEndY,
cardinalityStartX,
cardinalityStartY,
direction,
cardinalityStart,
cardinalityEnd
) {
return(
pathRef && (
<>
<circle
cx={cardinalityEndX-(3*direction)}
cy={cardinalityEndY}
r="4"
stroke="gray"
strokeWidth='2'
fill="grey"
className="group-hover:fill-sky-700"
/>
<text
x={cardinalityStartX-8}
y={cardinalityStartY-20}
fill="grey"
strokeWidth="0.5"
textAnchor="middle"
alignmentBaseline="middle"
>
{cardinalityStart}
</text>
<text
x={cardinalityEndX-15}
y={cardinalityEndY-20}
fill="grey"
strokeWidth="0.5"
textAnchor="middle"
alignmentBaseline="middle"
>
{cardinalityEnd}
</text>
</>
)
)
}

View File

@ -5,6 +5,7 @@ import {
tableFieldHeight, tableFieldHeight,
tableHeaderHeight, tableHeaderHeight,
tableColorStripHeight, tableColorStripHeight,
Notation,
} from "../../data/constants"; } from "../../data/constants";
import { import {
IconEdit, IconEdit,
@ -77,7 +78,7 @@ export default function Table(props) {
.scrollIntoView({ behavior: "smooth" }); .scrollIntoView({ behavior: "smooth" });
} }
}; };
const primaryKeyCount = tableData.fields.filter(field => field.primary).length;
return ( return (
<> <>
<foreignObject <foreignObject
@ -86,12 +87,27 @@ export default function Table(props) {
y={tableData.y} y={tableData.y}
width={settings.tableWidth} width={settings.tableWidth}
height={height} height={height}
className="group drop-shadow-lg rounded-md cursor-move" className="group drop-shadow-lg cursor-move"
onPointerDown={onPointerDown} onPointerDown={onPointerDown}
> >
<div <div
onDoubleClick={openEditor} onDoubleClick={openEditor}
className={`border-2 hover:border-dashed hover:border-blue-500 className={`border-2 hover:border-dashed hover:border-blue-500
select-none w-full ${
settings.notation !== Notation.DEFAULT
? "border-none"
: "rounded-lg"
} ${
selectedElement.id === tableData.id &&
selectedElement.element === ObjectType.TABLE
? "border-solid border-blue-500"
: "border-zinc-500"
} ${
settings.mode === "light"
? "bg-zinc-100 text-zinc-800"
: "bg-zinc-800 text-zinc-200"
} ${isSelected ? "border-solid border-blue-500" : borderColor}
`}
select-none rounded-lg w-full ${ select-none rounded-lg w-full ${
settings.mode === "light" settings.mode === "light"
? "bg-zinc-100 text-zinc-800" ? "bg-zinc-100 text-zinc-800"
@ -100,15 +116,28 @@ export default function Table(props) {
style={{ direction: "ltr" }} style={{ direction: "ltr" }}
> >
<div <div
className="h-[10px] w-full rounded-t-md" className={`h-[10px] w-full ${
style={{ backgroundColor: tableData.color }} settings.notation !== Notation.DEFAULT
? ""
: "rounded-t-md"
}`}
style={{ backgroundColor: tableData.color, height: settings.notation !== Notation.DEFAULT ? 0 : "10px" }}
/> />
<div <div
className={`overflow-hidden font-bold h-[40px] flex justify-between items-center border-b border-gray-400 ${ className={`overflow-hidden font-bold h-[40px] flex justify-between items-center border-b border-gray-400 ${
settings.mode === "light" ? "bg-zinc-200" : "bg-zinc-900" settings.notation !== Notation.DEFAULT
? "bg-transparent"
: settings.mode === "light"
? "bg-zinc-200"
: "bg-zinc-900"
}`}
>
<div className={` px-3 overflow-hidden text-ellipsis whitespace-nowrap ${
settings.notation !== Notation.DEFAULT
? ""
: ""
}`} }`}
> >
<div className=" px-3 overflow-hidden text-ellipsis whitespace-nowrap">
{tableData.name} {tableData.name}
</div> </div>
<div className="hidden group-hover:block"> <div className="hidden group-hover:block">
@ -293,10 +322,50 @@ export default function Table(props) {
function field(fieldData, index) { function field(fieldData, index) {
return ( return (
<div <div
className={`${ className={`
index === tableData.fields.length - 1 ${(tableData.fields.length === 1 && settings.notation === Notation.DEFAULT)
? "" ? "rounded-b-md"
: "border-b border-gray-400" : ""
}${(tableData.fields.length === 1 && settings.notation !== Notation.DEFAULT)
? "border-l border-r border-gray-400"
: ""
}${
(fieldData.primary && settings.notation !== Notation.DEFAULT && primaryKeyCount === 1)
? "border-b border-gray-400"
: ""
}${
(fieldData.primary && settings.notation !== Notation.DEFAULT && index ===primaryKeyCount - 1)
? "border-b border-gray-400"
: ""
}
${
(!fieldData.primary && settings.notation !== Notation.DEFAULT )
? "border-l border-r"
: ""
} ${
settings.mode === "light"
? "bg-zinc-100 text-zinc-800"
: "bg-zinc-800 text-zinc-200"
} ${
(settings.notation !== Notation.DEFAULT && index !== tableData.fields.length - 1)
? "border-l border-r border-gray-400"
: ""
} ${
(settings.notation !== Notation.DEFAULT && index === tableData.fields.length - 1)
? "border-b border-gray-400"
: ""
} ${
(fieldData.primary && settings.notation === Notation.DEFAULT)
? "border-b border-gray-400"
: ""
}${
(settings.notation === Notation.DEFAULT && index !== tableData.fields.length - 1 && fieldData.primary === false)
? "border-b border-gray-400"
: ""
}${
(settings.notation === Notation.DEFAULT && index === tableData.fields.length - 1)
? "rounded-b-md"
: ""
} group h-[36px] px-2 py-1 flex justify-between items-center gap-1 w-full overflow-hidden`} } group h-[36px] px-2 py-1 flex justify-between items-center gap-1 w-full overflow-hidden`}
onPointerEnter={(e) => { onPointerEnter={(e) => {
if (!e.isPrimary) return; if (!e.isPrimary) return;
@ -324,7 +393,11 @@ export default function Table(props) {
} flex items-center gap-2 overflow-hidden`} } flex items-center gap-2 overflow-hidden`}
> >
<button <button
className="shrink-0 w-[10px] h-[10px] bg-[#2f68adcc] rounded-full" className={`flex-shrink-0 w-[10px] h-[10px] bg-[#2f68adcc] rounded-full ${
(fieldData.primary && settings.notation !== Notation.DEFAULT)
? "bg-[#ff2222cc]"
: "bg-[#2f68adcc]"
}`}
onPointerDown={(e) => { onPointerDown={(e) => {
if (!e.isPrimary) return; if (!e.isPrimary) return;
@ -367,6 +440,22 @@ export default function Table(props) {
/> />
) : settings.showDataTypes ? ( ) : settings.showDataTypes ? (
<div className="flex gap-1 items-center"> <div className="flex gap-1 items-center">
{settings.notation !== Notation.DEFAULT ? (
<>
<span>
{fieldData.type +
((dbToTypes[database][fieldData.type].isSized ||
dbToTypes[database][fieldData.type].hasPrecision) &&
fieldData.size &&
fieldData.size !== ""
? "(" + fieldData.size + ")"
: "")}
</span>
{!fieldData.notNull && <span>NULL</span>}
{fieldData.notNull && <span>NOT NULL</span>}
</>
) : (
<>
{fieldData.primary && <IconKeyStroked/>} {fieldData.primary && <IconKeyStroked/>}
{!fieldData.notNull && <span>?</span>} {!fieldData.notNull && <span>?</span>}
<span> <span>
@ -375,9 +464,11 @@ export default function Table(props) {
dbToTypes[database][fieldData.type].hasPrecision) && dbToTypes[database][fieldData.type].hasPrecision) &&
fieldData.size && fieldData.size &&
fieldData.size !== "" fieldData.size !== ""
? `(${fieldData.size})` ? "(" + fieldData.size + ")"
: "")} : "")}
</span> </span>
</>
)}
</div> </div>
) : null} ) : null}
</div> </div>

View File

@ -42,6 +42,7 @@ import {
SIDESHEET, SIDESHEET,
DB, DB,
IMPORT_FROM, IMPORT_FROM,
Notation,
} from "../../data/constants"; } from "../../data/constants";
import jsPDF from "jspdf"; import jsPDF from "jspdf";
import { useHotkeys } from "react-hotkeys-hook"; import { useHotkeys } from "react-hotkeys-hook";
@ -1338,6 +1339,26 @@ export default function ControlPanel({
showCardinality: !prev.showCardinality, showCardinality: !prev.showCardinality,
})), })),
}, },
notation: {
children: [
{
default_notation: () => {
setSettings((prev) => ({ ...prev, notation: Notation.DEFAULT }));
},
},
{
crows_foot_notation: () => {
setSettings((prev) => ({ ...prev, notation: Notation.CROWS_FOOT }));
},
},
{
idef1x_notation: () => {
setSettings((prev) => ({ ...prev, notation: Notation.IDEF1X }));
},
},
],
function: () => {},
},
show_relationship_labels: { show_relationship_labels: {
state: settings.showRelationshipLabels ? ( state: settings.showRelationshipLabels ? (
<i className="bi bi-toggle-on" /> <i className="bi bi-toggle-on" />

View File

@ -1,6 +1,5 @@
import { createContext, useEffect, useState } from "react"; import { createContext, useEffect, useState } from "react";
import { tableWidth } from "../data/constants"; import { tableWidth, Notation } from "../data/constants";
const defaultSettings = { const defaultSettings = {
strictMode: false, strictMode: false,
showFieldSummary: true, showFieldSummary: true,
@ -11,6 +10,7 @@ const defaultSettings = {
panning: true, panning: true,
showCardinality: true, showCardinality: true,
showRelationshipLabels: true, showRelationshipLabels: true,
notation: Notation.DEFAULT,
tableWidth: tableWidth, tableWidth: tableWidth,
showDebugCoordinates: false, showDebugCoordinates: false,
}; };

View File

@ -11,7 +11,11 @@ export const Cardinality = {
ONE_TO_MANY: "one_to_many", ONE_TO_MANY: "one_to_many",
MANY_TO_ONE: "many_to_one", MANY_TO_ONE: "many_to_one",
}; };
export const Notation = {
DEFAULT: "default",
CROWS_FOOT: "crows_foot",
IDEF1X: "idef1x",
}
export const Constraint = { export const Constraint = {
NONE: "No action", NONE: "No action",
RESTRICT: "Restrict", RESTRICT: "Restrict",

View File

@ -51,6 +51,10 @@ const en = {
show_grid: "Show grid", show_grid: "Show grid",
show_datatype: "Show datatype", show_datatype: "Show datatype",
show_cardinality: "Show cardinality", show_cardinality: "Show cardinality",
default_notation: "Default",
crows_foot_notation: "Crow's foot",
idef1x_notation: "IDEF1X",
notation: "Notation",
theme: "Theme", theme: "Theme",
light: "Light", light: "Light",
dark: "Dark", dark: "Dark",

View File

@ -50,6 +50,10 @@ const es = {
reset_view: "Restablecer vista", reset_view: "Restablecer vista",
show_grid: "Mostrar cuadrícula", show_grid: "Mostrar cuadrícula",
show_cardinality: "Mostrar cardinalidad", show_cardinality: "Mostrar cardinalidad",
notation: "Notación",
default_notation: "Notación predeterminada",
crows_foot_notation: "Notación Crow's Foot",
idef1x_notation: "Notación IDEF1X",
theme: "Tema", theme: "Tema",
light: "Claro", light: "Claro",
dark: "Oscuro", dark: "Oscuro",