diff --git a/src/components/EditorCanvas/Relationship.jsx b/src/components/EditorCanvas/Relationship.jsx index fd293543..cb772420 100644 --- a/src/components/EditorCanvas/Relationship.jsx +++ b/src/components/EditorCanvas/Relationship.jsx @@ -2,6 +2,7 @@ import { useRef } from "react"; import { Cardinality, darkBgTheme, + Notation, ObjectType, Tab, } from "../../data/constants"; @@ -10,6 +11,8 @@ import { useDiagram, useSettings, useLayout, useSelect } from "../../hooks"; import { useTranslation } from "react-i18next"; import { SideSheet } from "@douyinfe/semi-ui"; import RelationshipInfo from "../EditorSidePanel/RelationshipsTab/RelationshipInfo"; +import { CrowOM, CrowOO, IDEFZM, DefaultNotation } from "./RelationshipFormat"; + const labelFontSize = 16; @@ -24,28 +27,70 @@ export default function Relationship({ data }) { const pathRef = useRef(); const labelRef = useRef(); - + const type = settings.notation === 'default' ? 0 : 10; + const relationshipType=(5,type); + let direction = 1; let cardinalityStart = "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) { // the translated values are to ensure backwards compatibility case t(Cardinality.MANY_TO_ONE): case Cardinality.MANY_TO_ONE: - cardinalityStart = "n"; - cardinalityEnd = "1"; + if (settings.notation === Notation.DEFAULT) { + cardinalityStart = "n"; + cardinalityEnd = "1"; + } else { + cardinalityStart = "(1,*)"; + cardinalityEnd = "(1,1)"; + } + format = formats.notation[settings.notation].many_to_one; break; case t(Cardinality.ONE_TO_MANY): case Cardinality.ONE_TO_MANY: - cardinalityStart = "1"; - cardinalityEnd = "n"; + if (settings.notation === Notation.DEFAULT) { + cardinalityStart = "1"; + cardinalityEnd = "n"; + } else { + cardinalityStart = "(1,1)"; + cardinalityEnd = "(1,*)"; + } + format = formats.notation[settings.notation].one_to_many; break; case t(Cardinality.ONE_TO_ONE): case Cardinality.ONE_TO_ONE: - cardinalityStart = "1"; - cardinalityEnd = "1"; + if (settings.notation === Notation.DEFAULT) { + cardinalityStart = "1"; + cardinalityEnd = "1"; + } else { + cardinalityStart = "(1,1)"; + cardinalityEnd = "(1,1)"; + } + format = formats.notation[settings.notation].one_to_one; break; default: + format = formats.default.one_to_one; break; } @@ -61,8 +106,9 @@ export default function Relationship({ data }) { const cardinalityOffset = 28; + if (pathRef.current) { - const pathLength = pathRef.current.getTotalLength(); + const pathLength = pathRef.current.getTotalLength() - cardinalityOffset; const labelPoint = pathRef.current.getPointAtLength(pathLength / 2); labelX = labelPoint.x - (labelWidth ?? 0) / 2; @@ -71,8 +117,9 @@ export default function Relationship({ data }) { const point1 = pathRef.current.getPointAtLength(cardinalityOffset); cardinalityStartX = point1.x; cardinalityStartY = point1.y; + const point2 = pathRef.current.getPointAtLength( - pathLength - cardinalityOffset, + pathLength, ); cardinalityEndX = point2.x; 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 ( <> <g className="select-none group" onDoubleClick={edit}> @@ -123,9 +174,24 @@ export default function Relationship({ data }) { stroke="gray" className="group-hover:stroke-sky-700" fill="none" + strokeDasharray={relationshipType} strokeWidth={2} cursor="pointer" /> + {settings.showCardinality && ( + <> + {format( + pathRef, + cardinalityEndX, + cardinalityEndY, + cardinalityStartX, + cardinalityStartY, + direction, + cardinalityStart, + cardinalityEnd, + )} + </> + )} {settings.showRelationshipLabels && ( <> <rect @@ -148,45 +214,8 @@ export default function Relationship({ data }) { </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> + <SideSheet title={t("edit")} size="small" diff --git a/src/components/EditorCanvas/RelationshipFormat.jsx b/src/components/EditorCanvas/RelationshipFormat.jsx new file mode 100644 index 00000000..0b0a1402 --- /dev/null +++ b/src/components/EditorCanvas/RelationshipFormat.jsx @@ -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> + </> + ) + ) +} diff --git a/src/components/EditorCanvas/Table.jsx b/src/components/EditorCanvas/Table.jsx index 3a33652a..df25b63b 100644 --- a/src/components/EditorCanvas/Table.jsx +++ b/src/components/EditorCanvas/Table.jsx @@ -5,6 +5,7 @@ import { tableFieldHeight, tableHeaderHeight, tableColorStripHeight, + Notation, } from "../../data/constants"; import { IconEdit, @@ -16,7 +17,7 @@ import { import { Popover, Tag, Button, SideSheet } from "@douyinfe/semi-ui"; import { useLayout, useSettings, useDiagram, useSelect } from "../../hooks"; import TableInfo from "../EditorSidePanel/TablesTab/TableInfo"; -import { useTranslation } from "react-i18next"; +import { useTranslation} from "react-i18next"; import { dbToTypes } from "../../data/datatypes"; import { isRtl } from "../../i18n/utils/rtl"; import i18n from "../../i18n/i18n"; @@ -77,7 +78,7 @@ export default function Table(props) { .scrollIntoView({ behavior: "smooth" }); } }; - + const primaryKeyCount = tableData.fields.filter(field => field.primary).length; return ( <> <foreignObject @@ -86,12 +87,27 @@ export default function Table(props) { y={tableData.y} width={settings.tableWidth} height={height} - className="group drop-shadow-lg rounded-md cursor-move" + className="group drop-shadow-lg cursor-move" onPointerDown={onPointerDown} > <div onDoubleClick={openEditor} 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 ${ settings.mode === "light" ? "bg-zinc-100 text-zinc-800" @@ -100,15 +116,28 @@ export default function Table(props) { style={{ direction: "ltr" }} > <div - className="h-[10px] w-full rounded-t-md" - style={{ backgroundColor: tableData.color }} + className={`h-[10px] w-full ${ + settings.notation !== Notation.DEFAULT + ? "" + : "rounded-t-md" + }`} + style={{ backgroundColor: tableData.color, height: settings.notation !== Notation.DEFAULT ? 0 : "10px" }} /> <div 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"> + <div className={` px-3 overflow-hidden text-ellipsis whitespace-nowrap ${ + settings.notation !== Notation.DEFAULT + ? "" + : "" + }`} + > {tableData.name} </div> <div className="hidden group-hover:block"> @@ -293,10 +322,50 @@ export default function Table(props) { function field(fieldData, index) { return ( <div - className={`${ - index === tableData.fields.length - 1 - ? "" - : "border-b border-gray-400" + className={` + ${(tableData.fields.length === 1 && settings.notation === Notation.DEFAULT) + ? "rounded-b-md" + : "" + }${(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`} onPointerEnter={(e) => { if (!e.isPrimary) return; @@ -324,7 +393,11 @@ export default function Table(props) { } flex items-center gap-2 overflow-hidden`} > <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) => { if (!e.isPrimary) return; @@ -367,17 +440,35 @@ export default function Table(props) { /> ) : settings.showDataTypes ? ( <div className="flex gap-1 items-center"> - {fieldData.primary && <IconKeyStroked />} - {!fieldData.notNull && <span>?</span>} - <span> - {fieldData.type + - ((dbToTypes[database][fieldData.type].isSized || - dbToTypes[database][fieldData.type].hasPrecision) && - fieldData.size && - fieldData.size !== "" - ? `(${fieldData.size})` - : "")} - </span> + {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.notNull && <span>?</span>} + <span> + {fieldData.type + + ((dbToTypes[database][fieldData.type].isSized || + dbToTypes[database][fieldData.type].hasPrecision) && + fieldData.size && + fieldData.size !== "" + ? "(" + fieldData.size + ")" + : "")} + </span> + </> + )} </div> ) : null} </div> diff --git a/src/components/EditorHeader/ControlPanel.jsx b/src/components/EditorHeader/ControlPanel.jsx index feef7a5b..caa3a66a 100644 --- a/src/components/EditorHeader/ControlPanel.jsx +++ b/src/components/EditorHeader/ControlPanel.jsx @@ -42,6 +42,7 @@ import { SIDESHEET, DB, IMPORT_FROM, + Notation, } from "../../data/constants"; import jsPDF from "jspdf"; import { useHotkeys } from "react-hotkeys-hook"; @@ -1323,6 +1324,26 @@ export default function ControlPanel({ 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: { state: settings.showRelationshipLabels ? ( <i className="bi bi-toggle-on" /> diff --git a/src/context/SettingsContext.jsx b/src/context/SettingsContext.jsx index 19cd69ec..3da6fedd 100644 --- a/src/context/SettingsContext.jsx +++ b/src/context/SettingsContext.jsx @@ -1,6 +1,5 @@ import { createContext, useEffect, useState } from "react"; -import { tableWidth } from "../data/constants"; - +import { tableWidth, Notation } from "../data/constants"; const defaultSettings = { strictMode: false, showFieldSummary: true, @@ -11,6 +10,7 @@ const defaultSettings = { panning: true, showCardinality: true, showRelationshipLabels: true, + notation: Notation.DEFAULT, tableWidth: tableWidth, showDebugCoordinates: false, }; diff --git a/src/data/constants.js b/src/data/constants.js index 80020c1e..6e9b7a48 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -11,7 +11,11 @@ export const Cardinality = { ONE_TO_MANY: "one_to_many", MANY_TO_ONE: "many_to_one", }; - +export const Notation = { + DEFAULT: "default", + CROWS_FOOT: "crows_foot", + IDEF1X: "idef1x", +} export const Constraint = { NONE: "No action", RESTRICT: "Restrict", diff --git a/src/i18n/locales/en.js b/src/i18n/locales/en.js index 472d86ab..a78eaf82 100644 --- a/src/i18n/locales/en.js +++ b/src/i18n/locales/en.js @@ -51,6 +51,10 @@ const en = { show_grid: "Show grid", show_datatype: "Show datatype", show_cardinality: "Show cardinality", + default_notation: "Default", + crows_foot_notation: "Crow's foot", + idef1x_notation: "IDEF1X", + notation: "Notation", theme: "Theme", light: "Light", dark: "Dark", diff --git a/src/i18n/locales/es.js b/src/i18n/locales/es.js index 38eaf3a3..ce576394 100644 --- a/src/i18n/locales/es.js +++ b/src/i18n/locales/es.js @@ -50,6 +50,10 @@ const es = { reset_view: "Restablecer vista", show_grid: "Mostrar cuadrícula", 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", light: "Claro", dark: "Oscuro",