From d74853380cb7b8a815490023169765af93b6bdbb Mon Sep 17 00:00:00 2001 From: lethalSopaper <2bluisugartechea@gmail.com> Date: Tue, 23 Sep 2025 20:10:26 -0600 Subject: [PATCH 1/6] feat: add custom cardinality input with validation for custom cardinalities - Add 'Custom...' option to ONE_TO_MANY cardinality dropdown - Support all three notation systems (Default, Crow's Foot, IDEF1X) - Add input validation: first coordinate (0 or 1), second coordinate (>1 or *) - Prevent negative cardinalities and enforce proper format (x,y) --- .../EditorCanvas/RelationshipFormat.jsx | 253 ++++++++++-------- .../RelationshipsTab/RelationshipInfo.jsx | 176 ++++++++++-- src/data/constants.js | 19 +- 3 files changed, 298 insertions(+), 150 deletions(-) diff --git a/src/components/EditorCanvas/RelationshipFormat.jsx b/src/components/EditorCanvas/RelationshipFormat.jsx index 4e2f2fb82..ac7787a64 100644 --- a/src/components/EditorCanvas/RelationshipFormat.jsx +++ b/src/components/EditorCanvas/RelationshipFormat.jsx @@ -3,7 +3,7 @@ export function CrowParentLines( cardinalityStartY, direction, isVertical = false, - cardinalityText = "" + cardinalityText = "", ) { const offsetDistance = 6; const lineOffset = 10; @@ -13,18 +13,18 @@ export function CrowParentLines( <> 0) { dx = vectorInfo.dx / magnitude; dy = vectorInfo.dy / magnitude; @@ -190,27 +195,27 @@ export function CrowsFootChild( // For vertical relationships, position cardinalities to the side return { x: isStart ? x - 25 : x + 35, // Child side (end) further away from table for clarity - y: y // Same vertical level as the notation + y: y, // Same vertical level as the notation }; } else { return { x: isStart ? x - 8 : x + 15, - y: y - 20 // Above the line for horizontal relationships + y: y - 20, // Above the line for horizontal relationships }; } }; // Base distances from table edge - CORRECTED ORDER // Note: cardinalityEndX/Y are already offset by 35px from table edge in Relationship.jsx - const crowsFootOffset = 6; // Crow's foot closer to table (reduced due to existing 35px offset) - const elementOffset = 12; // Bars/circles further from table (reduced due to existing 35px offset) - const lineLength = 8; // Half length of bars + const crowsFootOffset = 6; // Crow's foot closer to table (reduced due to existing 35px offset) + const elementOffset = 12; // Bars/circles further from table (reduced due to existing 35px offset) + const lineLength = 8; // Half length of bars // Calculate positions along the line direction - const crowsFootX = cardinalityEndX - (dx * crowsFootOffset); - const crowsFootY = cardinalityEndY - (dy * crowsFootOffset); - const elementX = cardinalityEndX - (dx * elementOffset); - const elementY = cardinalityEndY - (dy * elementOffset); + const crowsFootX = cardinalityEndX - dx * crowsFootOffset; + const crowsFootY = cardinalityEndY - dy * crowsFootOffset; + const elementX = cardinalityEndX - dx * elementOffset; + const elementY = cardinalityEndY - dy * elementOffset; return ( pathRef && ( @@ -233,10 +238,10 @@ export function CrowsFootChild( {isMandatory && isMany && ( <> {(() => { - const startPos = getCardinalityPosition(cardinalityStartX, cardinalityStartY, true); - const endPos = getCardinalityPosition(cardinalityEndX, cardinalityEndY, false); + const startPos = getCardinalityPosition( + cardinalityStartX, + cardinalityStartY, + true, + ); + const endPos = getCardinalityPosition( + cardinalityEndX, + cardinalityEndY, + false, + ); return ( <> - - - {cardinalityStart} - - - - {cardinalityEnd} - - - ) - ) + return ( + pathRef && ( + + + + {cardinalityStart} + + + + {cardinalityEnd} + + + ) + ); } export function IDEFZM( @@ -423,23 +436,29 @@ export function IDEFZM( cardinalityEnd, showCardinality = true, isVertical = false, - vectorInfo = null + vectorInfo = null, ) { let letter = null; - switch (cardinalityEnd) { - case "(1,*)": - letter = "P"; - break; - case "(1,1)": - letter = "L"; - break; - case "(0,1)": - letter = "Z"; - break; + + if (cardinalityEnd.startsWith("(1") && cardinalityEnd.endsWith("1)")) { + letter = "L"; + } else if ( + cardinalityEnd.startsWith("(1") && + (cardinalityEnd.endsWith("*)") || + (cardinalityEnd.includes(",") && !cardinalityEnd.endsWith("1)"))) + ) { + // (1,*) or (1,X) where X is not 1 - one to many (like (1,10), (1,5)) + letter = "P"; + } else if (cardinalityEnd.startsWith("(0") && cardinalityEnd.endsWith("1)")) { + // (0,1) - zero or one + letter = "Z"; } - let dx = 1, dy = 0; + let dx = 1, + dy = 0; if (vectorInfo) { - const magnitude = Math.sqrt(vectorInfo.dx * vectorInfo.dx + vectorInfo.dy * vectorInfo.dy); + const magnitude = Math.sqrt( + vectorInfo.dx * vectorInfo.dx + vectorInfo.dy * vectorInfo.dy, + ); if (magnitude > 0) { dx = vectorInfo.dx / magnitude; dy = vectorInfo.dy / magnitude; @@ -448,13 +467,13 @@ export function IDEFZM( // Position circle along the line direction, away from table const circleOffset = 8; - const circleX = cardinalityEndX - (dx * circleOffset); - const circleY = cardinalityEndY - (dy * circleOffset); + const circleX = cardinalityEndX - dx * circleOffset; + const circleY = cardinalityEndY - dy * circleOffset; // Position letter further away from table const letterOffset = 15; - const letterX = cardinalityEndX - (dx * letterOffset); - const letterY = cardinalityEndY - (dy * letterOffset) + 4; // Small adjustment for text baseline + const letterX = cardinalityEndX - dx * letterOffset; + const letterY = cardinalityEndY - dy * letterOffset + 4; // Small adjustment for text baseline return ( pathRef && ( diff --git a/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx b/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx index e5aed5f90..0e157a1f8 100644 --- a/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx +++ b/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx @@ -51,6 +51,7 @@ export default function RelationshipInfo({ data }) { } = useDiagram(); const { t } = useTranslation(); const [editField, setEditField] = useState({}); + const [customCardinality, setCustomCardinality] = useState(""); // Helper function to get the effective end table ID and field ID const getEffectiveEndTable = () => { if (data.endTableId !== undefined) { @@ -219,6 +220,11 @@ export default function RelationshipInfo({ data }) { }; const changeCardinality = (value) => { + if (value === "Custom...") { + setCustomCardinality(data.cardinality || ""); + return; + } + pushUndo({ action: Action.EDIT, element: ObjectType.RELATIONSHIP, @@ -237,6 +243,90 @@ export default function RelationshipInfo({ data }) { ); }; + const handleCustomCardinalityChange = (value) => { + setCustomCardinality(value); + }; + + const validateCustomCardinality = (cardinality) => { + // Check if it matches the pattern (x,y) where x and y are numbers + const match = cardinality.match(/^\(\s*(\d+)\s*,\s*(\d+|\*)\s*\)$/); + if (!match) { + return { + valid: false, + message: t( + "cardinality_format_error", + "Invalid format. Use (x,y) where x is 0 or 1, and y is greater than 1 or *", + ), + }; + } + + const first = parseInt(match[1]); + const second = match[2]; + + // First coordinate must be 0 or 1 + if (first !== 0 && first !== 1) { + return { + valid: false, + message: t( + "cardinality_first_error", + "First coordinate must be 0 or 1", + ), + }; + } + + // Second coordinate must be * or a number greater than 1 + if (second !== "*") { + const secondNum = parseInt(second); + if (isNaN(secondNum) || secondNum < 2) { + return { + valid: false, + message: t( + "cardinality_second_error", + "Second coordinate must be greater than 1 or *", + ), + }; + } + } + + return { valid: true }; + }; + + const applyCustomCardinality = () => { + const trimmedCardinality = customCardinality.trim(); + if (trimmedCardinality === "") { + return; + } + + // Validate the custom cardinality + const validation = validateCustomCardinality(trimmedCardinality); + if (!validation.valid) { + Toast.error(validation.message); + return; + } + + pushUndo({ + action: Action.EDIT, + element: ObjectType.RELATIONSHIP, + rid: data.id, + undo: { cardinality: data.cardinality }, + redo: { cardinality: trimmedCardinality }, + message: t("edit_relationship", { + refName: data.name, + extra: "[cardinality]", + }), + }); + setRelationships((prev) => + prev.map((e, idx) => + idx === data.id ? { ...e, cardinality: trimmedCardinality } : e, + ), + ); + setCustomCardinality(""); + }; + + const cancelCustomCardinality = () => { + setCustomCardinality(""); + }; + const changeSubtypeRestriction = (value) => { pushUndo({ action: Action.EDIT, @@ -504,31 +594,69 @@ export default function RelationshipInfo({ data }) { data.relationshipType !== RelationshipType.SUBTYPE && ( <>
{t("cardinality")}:
-
- 1 or *). E.g., (0,5), (1,*)" + } + className="w-full" /> - )} -
+
+ Format: (x,y) where x is 0 or 1, and y is greater than 1 or * +
+
+ + +
+ + ) : ( + // Normal cardinality selection +
+ 1 or *). E.g., (0,5), (1,*)" @@ -609,6 +631,8 @@ export default function RelationshipInfo({ data }) { />
Format: (x,y) where x is 0 or 1, and y is greater than 1 or * +
+ Press Enter to apply, Escape to cancel
+ +
+ + + setChangeCardinalityModal((prev) => ({ + ...prev, + newCardinality: value, + })) + } + placeholder={"e.g., (1,n)"} + onEnterPress={handleChangeCardinalityConfirm} + /> + + {/* ADD THIS SECTION FOR THE LEGEND */} +
+

Format: (x,y) where x is 0 or 1, and y is n, *, or a number.

+
+
+
+ Date: Mon, 6 Oct 2025 08:01:26 -0600 Subject: [PATCH 5/6] Fix: Prevent leading zeros in custom cardinality validation --- src/components/EditorCanvas/Canvas.jsx | 38 ++++++++++++++++--- .../RelationshipsTab/RelationshipInfo.jsx | 9 +++++ src/data/constants.js | 14 +++---- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/components/EditorCanvas/Canvas.jsx b/src/components/EditorCanvas/Canvas.jsx index e8e7cd444..69f5c27de 100644 --- a/src/components/EditorCanvas/Canvas.jsx +++ b/src/components/EditorCanvas/Canvas.jsx @@ -1025,16 +1025,44 @@ export default function Canvas() { }; const validateCustomCardinality = (cardinality) => { - const regex = /^\((0|1),(n|\*|\d+)\)$/; - if (regex.test(cardinality)) { - return { valid: true }; - } else { + const match = cardinality.match(/^\(\s*(\d+)\s*,\s*(\d+|\*)\s*\)$/); + if (!match) { + return { + valid: false, + message: "Invalid format. Use (x,y) where x is 0 or 1, and y is greater than 1 or *", + }; + } + + const first = parseInt(match[1]); + const second = match[2]; + + // First coordinate must be 0 or 1 + if (first !== 0 && first !== 1) { return { valid: false, - message: "Invalid format. Use (0,n), (1,*), or (0,10).", + message: "First coordinate must be 0 or 1", }; } + + // Second coordinate must be * or a number greater than 1 + if (second !== "*") { + if (/^0\d+$/.test(second)) { + return { + valid: false, + message: "Second coordinate must not have leading zeros.", + }; + } + const secondNum = parseInt(second); + if (isNaN(secondNum) || secondNum < 2) { + return { + valid: false, + message: "Second coordinate must be greater than 1 or *", + }; + } + } + + return { valid: true }; }; const handleDeleteRelationship = () => { diff --git a/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx b/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx index 7f512d2be..4faf7a251 100644 --- a/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx +++ b/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx @@ -296,6 +296,15 @@ export default function RelationshipInfo({ data }) { // Second coordinate must be * or a number greater than 1 if (second !== "*") { + if (/^0\d+$/.test(second)) { + return { + valid: false, + message: t( + "cardinality_second_leading_zero_error", + "Second coordinate must not have leading zeros.", + ), + }; + } const secondNum = parseInt(second); if (isNaN(secondNum) || secondNum < 2) { return { diff --git a/src/data/constants.js b/src/data/constants.js index d45a1dc9f..d9b9812b6 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -37,19 +37,19 @@ export const RelationshipType = { export const RelationshipCardinalities = { [RelationshipType.ONE_TO_ONE]: [ - { value: "0,1", label: "(0,1)" }, - { value: "1,1", label: "(1,1)" }, + {value: "0,1", label: "(0,1)"}, + {value: "1,1", label: "(1,1)"}, ], [RelationshipType.ONE_TO_MANY]: [ - { value: "0,*", label: "(0,*)" }, - { value: "1,*", label: "(1,*)" }, - { value: "custom", label: "Custom..." }, + {value: "0,*", label: "(0,*)"}, + {value: "1,*", label: "(1,*)"}, + {value: "custom", label: "Custom..."}, ], }; export const ParentCardinality = { - DEFAULT: { value: "1,1", label: "(1,1)" }, - NULLEABLE: { value: "0,1", label: "(0,1)" }, + DEFAULT: {value: "1,1", label: "(1,1)"}, + NULLEABLE: {value: "0,1", label: "(0,1)"}, }; export const Notation = { From 2440f80d0ca84a6c4400f644fcd4c9f9a9ddac61 Mon Sep 17 00:00:00 2001 From: lethalSopaper <2bluisugartechea@gmail.com> Date: Wed, 8 Oct 2025 07:32:03 -0600 Subject: [PATCH 6/6] Fix: allowing leading zeroes but normalizing when entered --- src/components/EditorCanvas/Canvas.jsx | 20 +++++++++------- .../RelationshipsTab/RelationshipInfo.jsx | 24 ++++++++++--------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/components/EditorCanvas/Canvas.jsx b/src/components/EditorCanvas/Canvas.jsx index 69f5c27de..ac66fc56c 100644 --- a/src/components/EditorCanvas/Canvas.jsx +++ b/src/components/EditorCanvas/Canvas.jsx @@ -516,7 +516,8 @@ export default function Canvas() { return; } - updateRelationship(relationshipId, { cardinality: trimmedCardinality }); + const normalizedCardinality = normalizeCardinality(trimmedCardinality); + updateRelationship(relationshipId, { cardinality: normalizedCardinality }); setChangeCardinalityModal({ visible: false, @@ -1025,7 +1026,6 @@ export default function Canvas() { }; const validateCustomCardinality = (cardinality) => { - const match = cardinality.match(/^\(\s*(\d+)\s*,\s*(\d+|\*)\s*\)$/); if (!match) { return { @@ -1047,12 +1047,6 @@ export default function Canvas() { // Second coordinate must be * or a number greater than 1 if (second !== "*") { - if (/^0\d+$/.test(second)) { - return { - valid: false, - message: "Second coordinate must not have leading zeros.", - }; - } const secondNum = parseInt(second); if (isNaN(secondNum) || secondNum < 2) { return { @@ -1065,6 +1059,16 @@ export default function Canvas() { return { valid: true }; }; + const normalizeCardinality = (cardinality) => { + const match = cardinality.match(/^\(\s*(\d+)\s*,\s*(\d+|\*)\s*\)$/); + if (!match) return cardinality; + + const first = parseInt(match[1]); + const second = match[2] === "*" ? "*" : parseInt(match[2]).toString(); + + return `(${first},${second})`; + }; + const handleDeleteRelationship = () => { if (relationshipContextMenu.relationshipId !== null) { deleteRelationship(relationshipContextMenu.relationshipId); diff --git a/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx b/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx index 4faf7a251..b35e03a6e 100644 --- a/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx +++ b/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx @@ -296,15 +296,6 @@ export default function RelationshipInfo({ data }) { // Second coordinate must be * or a number greater than 1 if (second !== "*") { - if (/^0\d+$/.test(second)) { - return { - valid: false, - message: t( - "cardinality_second_leading_zero_error", - "Second coordinate must not have leading zeros.", - ), - }; - } const secondNum = parseInt(second); if (isNaN(secondNum) || secondNum < 2) { return { @@ -320,6 +311,16 @@ export default function RelationshipInfo({ data }) { return { valid: true }; }; + const normalizeCardinality = (cardinality) => { + const match = cardinality.match(/^\(\s*(\d+)\s*,\s*(\d+|\*)\s*\)$/); + if (!match) return cardinality; + + const first = parseInt(match[1]); + const second = match[2] === "*" ? "*" : parseInt(match[2]).toString(); + + return `(${first},${second})`; + }; + const applyCustomCardinality = () => { const trimmedCardinality = customCardinality.trim(); if (trimmedCardinality === "") { @@ -333,13 +334,14 @@ export default function RelationshipInfo({ data }) { return; } + const normalizedCardinality = normalizeCardinality(trimmedCardinality); const timestamp = Date.now(); pushUndo({ action: Action.EDIT, element: ObjectType.RELATIONSHIP, rid: data.id, undo: { cardinality: data.cardinality }, - redo: { cardinality: trimmedCardinality }, + redo: { cardinality: normalizedCardinality }, message: t("edit_relationship", { refName: data.name, extra: `[cardinality-${timestamp}]`, @@ -347,7 +349,7 @@ export default function RelationshipInfo({ data }) { }); setRelationships((prev) => prev.map((e, idx) => - idx === data.id ? { ...e, cardinality: trimmedCardinality } : e, + idx === data.id ? { ...e, cardinality: normalizedCardinality } : e, ), ); setCustomCardinality("");