diff --git a/src/components/EditorCanvas/RelationshipFormat.jsx b/src/components/EditorCanvas/RelationshipFormat.jsx
new file mode 100644
index 000000000..64c99a921
--- /dev/null
+++ b/src/components/EditorCanvas/RelationshipFormat.jsx
@@ -0,0 +1,262 @@
+export function CrowParentLines(cardinalityStartX, cardinalityStartY, direction) {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export function CrowParentDiamond(cardinalityStartX, cardinalityStartY, direction) {
+ return (
+
+ );
+}
+
+export function CrowsFootChild(
+ pathRef,
+ cardinalityEndX,
+ cardinalityEndY,
+ cardinalityStartX,
+ cardinalityStartY,
+ direction,
+ cardinalityStart,
+ cardinalityEnd,
+ showCardinality
+) {
+ const isMandatory = cardinalityEnd.startsWith("(1");
+ const isOptional = cardinalityEnd.startsWith("(0");
+ const isMany = cardinalityEnd.endsWith("*)");
+ const isOne = cardinalityEnd.endsWith("1)");
+ return (
+ pathRef && (
+ <>
+ {isMany && (
+ <>
+
+
+
+
+ >
+ )}
+ {isOne && (
+
+ )}
+ {isOptional && (
+
+ )}
+ {isMandatory && (
+
+ )}
+ {showCardinality && (
+ <>
+
{cardinalityStart}
+
{cardinalityEnd}
+ >
+ )}
+ >
+ )
+ );
+}
+
+export function DefaultNotation(
+ pathRef,
+ cardinalityEndX,
+ cardinalityEndY,
+ cardinalityStartX,
+ cardinalityStartY,
+ direction,
+ cardinalityStart,
+ cardinalityEnd
+) {
+ return (
+ pathRef && (
+ <>
+
+
{cardinalityStart}
+
+
{cardinalityEnd}
+ >
+ )
+ );
+}
+
+export function IDEFZM(
+ pathRef,
+ cardinalityEndX,
+ cardinalityEndY,
+ cardinalityStartX,
+ cardinalityStartY,
+ direction,
+ cardinalityStart,
+ cardinalityEnd,
+ showCardinality
+) {
+ let letter = null;
+ switch (cardinalityEnd) {
+ case "(1,*)":
+ letter = "P";
+ break;
+ case "(1,1)":
+ letter = "L";
+ break;
+ case "(0,1)":
+ letter = "Z";
+ break;
+ }
+
+ return (
+ pathRef && (
+ <>
+
+ {letter && (
+
{letter}
+ )}
+ {showCardinality && (
+ <>
+
{cardinalityStart}
+
{cardinalityEnd}
+ >
+ )}
+ >
+ )
+ );
+}
+
+export function subDT(point, angle, notation, subtypevar, direction, cardinalityStart, cardinalityEnd, onConnectSubtypePoint, relationshipId) {
+ return (
+ point && subtypevar === "1" && (
+
+
+ D
+
+
+ onConnectSubtypePoint?.(e, point.x, point.y + 20, relationshipId)}
+ />
+
+ )
+ );
+}
+
+export function subDP(point, angle, notation, subtypevar, direction, cardinalityStart, cardinalityEnd, onConnectSubtypePoint, relationshipId) {
+ return (
+ point && subtypevar === "2" && (
+
+
+ D
+
+ onConnectSubtypePoint?.(e, point.x, point.y + 20, relationshipId)}
+ />
+
+ )
+ );
+}
+
+export function subOT(point, angle, notation, subtypevar, direction, cardinalityStart, cardinalityEnd, onConnectSubtypePoint, relationshipId) {
+ return (
+ point && subtypevar === "3" && (
+
+
+ O
+
+
+ onConnectSubtypePoint?.(e, point.x, point.y + 20, relationshipId)}
+ />
+
+ )
+ );
+}
+
+export function subOP(point, angle, notation, subtypevar, direction, cardinalityStart, cardinalityEnd, onConnectSubtypePoint, relationshipId) {
+ return (
+ point && subtypevar === "4" && (
+
+
+ O
+
+ onConnectSubtypePoint?.(e, point.x, point.y + 20, relationshipId)}
+ />
+
+ )
+ );
+}
\ No newline at end of file
diff --git a/src/components/EditorCanvas/Table.jsx b/src/components/EditorCanvas/Table.jsx
index 4509de91d..20132c294 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,
@@ -15,7 +16,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";
@@ -61,6 +62,37 @@ export default function Table(props) {
.scrollIntoView({ behavior: "smooth" });
}
};
+ const primaryKeyCount = tableData.fields.filter(field => field.primary).length;
+
+ const sortedFields = [...tableData.fields].sort((a, b) => {
+ const aIsPK = a.primary;
+ const bIsPK = b.primary;
+ const aIsFK = a.foreignK === true;
+ const bIsFK = b.foreignK === true;
+
+ let groupA;
+ if (aIsPK) {
+ groupA = 1;
+ } else if (!aIsFK) {
+ groupA = 2;
+ } else {
+ groupA = 3;
+ }
+
+ let groupB;
+ if (bIsPK) {
+ groupB = 1;
+ } else if (!bIsFK) {
+ groupB = 2;
+ } else {
+ groupB = 3;
+ }
+
+ if (groupA !== groupB) {
+ return groupA - groupB;
+ }
+ return 0;
+ });
return (
<>
@@ -70,36 +102,49 @@ 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}
>
-
+
{tableData.name}
@@ -189,7 +234,7 @@ export default function Table(props) {
- {tableData.fields.map((e, i) => {
+ {sortedFields.map((e, i) => {
return settings.showFieldSummary ? (
{
if (!e.isPrimary) return;
@@ -315,29 +403,28 @@ export default function Table(props) {
} flex items-center gap-2 overflow-hidden`}
>
{
if (!e.isPrimary) return;
- handleGripField(fieldData);
+ handleGripField(fieldData,tableData.id);
+
+ const effectiveColorStripHeight = settings.notation === Notation.DEFAULT ? tableColorStripHeight : 0;
+ const gripYOffset = tableHeaderHeight + effectiveColorStripHeight + (index * tableFieldHeight) + (tableFieldHeight / 2);
+ const gripXOffset = settings.tableWidth / 2; // Or a fixed small offset from table edge
+
setLinkingLine((prev) => ({
...prev,
- startFieldId: fieldData.id,
- startTableId: tableData.id,
- startX: tableData.x + 15,
- startY:
- tableData.y +
- index * tableFieldHeight +
- tableHeaderHeight +
- tableColorStripHeight +
- 12,
- endX: tableData.x + 15,
- endY:
- tableData.y +
- index * tableFieldHeight +
- tableHeaderHeight +
- tableColorStripHeight +
- 12,
+ // startTableId and startFieldId will be set by handleGripField in Canvas.jsx
+ // This setLinkingLine is primarily for the visual startX/startY of the temporary line.
+ startX: tableData.x + gripXOffset,
+ startY: tableData.y + gripYOffset,
+ endX: tableData.x + gripXOffset, // Initialize end to start
+ endY: tableData.y + gripYOffset,
}));
}}
/>
@@ -358,50 +445,68 @@ export default function Table(props) {
/>
) : (
- {fieldData.primary &&
-
-
}
- {fieldData.foreignK &&
-
-
}
- {!fieldData.notNull &&
? }
-
- {fieldData.type +
- ((dbToTypes[database][fieldData.type].isSized ||
- dbToTypes[database][fieldData.type].hasPrecision) &&
- fieldData.size &&
- fieldData.size !== ""
- ? "(" + fieldData.size + ")"
- : "")}
-
+ {settings.notation !== Notation.DEFAULT ? (
+ <>
+
+ {fieldData.type +
+ ((dbToTypes[database][fieldData.type].isSized ||
+ dbToTypes[database][fieldData.type].hasPrecision) &&
+ fieldData.size &&
+ fieldData.size !== ""
+ ? "(" + fieldData.size + ")"
+ : "")}
+
+ {!fieldData.notNull &&
NULL }
+ {fieldData.notNull &&
NOT NULL }
+ >
+ ) : (
+ <>
+ {!fieldData.notNull &&
? }
+
+ {fieldData.type +
+ ((dbToTypes[database][fieldData.type].isSized ||
+ dbToTypes[database][fieldData.type].hasPrecision) &&
+ fieldData.size &&
+ fieldData.size !== ""
+ ? "(" + fieldData.size + ")"
+ : "")}
+
+ >
+ )}
)}
diff --git a/src/components/EditorHeader/ControlPanel.jsx b/src/components/EditorHeader/ControlPanel.jsx
index 8a90fdce1..bd966f352 100644
--- a/src/components/EditorHeader/ControlPanel.jsx
+++ b/src/components/EditorHeader/ControlPanel.jsx
@@ -24,7 +24,6 @@ import {
Popconfirm,
} from "@douyinfe/semi-ui";
import { toPng, toJpeg, toSvg } from "html-to-image";
-import { saveAs } from "file-saver";
import {
jsonToMySQL,
jsonToPostgreSQL,
@@ -41,6 +40,8 @@ import {
MODAL,
SIDESHEET,
DB,
+ IMPORT_FROM,
+ Notation,
} from "../../data/constants";
import jsPDF from "jspdf";
import { useHotkeys } from "react-hotkeys-hook";
@@ -74,6 +75,8 @@ import { jsonToMermaid } from "../../utils/exportAs/mermaid";
import { isRtl } from "../../i18n/utils/rtl";
import { jsonToDocumentation } from "../../utils/exportAs/documentation";
import { IdContext } from "../Workspace";
+import { socials } from "../../data/socials";
+import { toDBML } from "../../utils/exportAs/dbml";
export default function ControlPanel({
diagramId,
@@ -91,6 +94,7 @@ export default function ControlPanel({
filename: `${title}_${new Date().toISOString()}`,
extension: "",
});
+ const [importFrom, setImportFrom] = useState(IMPORT_FROM.JSON);
const { saveState, setSaveState } = useSaveState();
const { layout, setLayout } = useLayout();
const { settings, setSettings } = useSettings();
@@ -106,6 +110,7 @@ export default function ControlPanel({
setRelationships,
addRelationship,
deleteRelationship,
+ restoreFieldsToTable,
updateRelationship,
database,
} = useDiagram();
@@ -127,383 +132,534 @@ export default function ControlPanel({
if (undoStack.length === 0) return;
const a = undoStack[undoStack.length - 1];
setUndoStack((prev) => prev.filter((_, i) => i !== prev.length - 1));
+
+ let actionForRedoStack = { ...a }; // Cloning the action for redo stack
+
if (a.action === Action.ADD) {
if (a.element === ObjectType.TABLE) {
- deleteTable(tables[tables.length - 1].id, false);
+ const tableIdToDelete = a.data && typeof a.data.id !== 'undefined' ? a.data.id : (tables.length > 0 ? tables[tables.length - 1].id : null);
+ if (tableIdToDelete !== null) {
+ deleteTable(tableIdToDelete, false);
+ }
} else if (a.element === ObjectType.AREA) {
- deleteArea(areas[areas.length - 1].id, false);
+ const areaIdToDelete = a.data && typeof a.data.id !== 'undefined' ? a.data.id : (areas.length > 0 ? areas[areas.length - 1].id : null);
+ if (areaIdToDelete !== null) {
+ deleteArea(areaIdToDelete, false);
+ }
} else if (a.element === ObjectType.NOTE) {
- deleteNote(notes[notes.length - 1].id, false);
+ const noteIdToDelete = a.data && typeof a.data.id !== 'undefined' ? a.data.id : (notes.length > 0 ? notes[notes.length - 1].id : null);
+ if (noteIdToDelete !== null) {
+ deleteNote(noteIdToDelete, false);
+ }
} else if (a.element === ObjectType.RELATIONSHIP) {
- deleteRelationship(a.data.id, false);
+ const { relationship: relToDelete, autoGeneratedFkFields, childTableIdWithGeneratedFks } = a.data;
+ if (relToDelete && typeof relToDelete.id !== 'undefined') {
+ deleteRelationship(relToDelete.id, false);
+ if (autoGeneratedFkFields && autoGeneratedFkFields.length > 0 && childTableIdWithGeneratedFks !== undefined) {
+ const childTable = tables.find(t => t.id === childTableIdWithGeneratedFks);
+ if (childTable) {
+ const newFields = childTable.fields.filter(
+ cf => !autoGeneratedFkFields.some(afk => afk.id === cf.id && afk.name === cf.name)
+ ).map((f,i) => ({...f, id:i}));
+ updateTable(childTableIdWithGeneratedFks, { fields: newFields });
+ }
+ }
+ }
} else if (a.element === ObjectType.TYPE) {
- deleteType(types.length - 1, false);
+ const typeIdToDelete = a.data && typeof a.data.id !== 'undefined' ? a.data.id : (types.length > 0 ? types.length - 1 : null);
+ if (typeIdToDelete !== null) {
+ deleteType(typeIdToDelete, false);
+ }
} else if (a.element === ObjectType.ENUM) {
- deleteEnum(enums.length - 1, false);
+ const enumIdToDelete = a.data && typeof a.data.id !== 'undefined' ? a.data.id : (enums.length > 0 ? enums.length - 1 : null);
+ if (enumIdToDelete !== null) {
+ deleteEnum(enumIdToDelete, false);
+ }
}
- setRedoStack((prev) => [...prev, a]);
- } else if (a.action === Action.MOVE) {
- // Movimientos múltiples
- if (Array.isArray(a.id)) {
- setRedoStack((prev) => [
- ...prev,
- {
- ...a,
- finalPositions: a.id.reduce((acc, id) => {
- if (a.element === ObjectType.TABLE) {
- acc[id] = { x: tables[id].x, y: tables[id].y };
- } else if (a.element === ObjectType.AREA) {
- acc[id] = { x: areas[id].x, y: areas[id].y };
- } else if (a.element === ObjectType.NOTE) {
- acc[id] = { x: notes[id].x, y: notes[id].y };
- }
- return acc;
- }, {}),
- },
- ]);
- // Revertir cada objeto a su posición inicial
- a.id.forEach((id) => {
- if (a.element === ObjectType.TABLE) {
- updateTable(id, a.initialPositions[id]);
- } else if (a.element === ObjectType.AREA) {
- updateArea(id, a.initialPositions[id]);
- } else if (a.element === ObjectType.NOTE) {
- updateNote(id, a.initialPositions[id]);
- }
- });
- } else {
- // Caso individual: se utiliza el campo "from"
- setRedoStack((prev) => [
- ...prev,
- { ...a, to: { x: tables[a.id].x, y: tables[a.id].y } },
- ]);
- if (a.element === ObjectType.TABLE) {
- updateTable(a.id, a.from);
- } else if (a.element === ObjectType.AREA) {
- updateArea(a.id, a.from);
- } else if (a.element === ObjectType.NOTE) {
- updateNote(a.id, a.from);
+ actionForRedoStack = { ...a }; // For ADD, the redo is the same action.
+ setRedoStack((prev) => [...prev, actionForRedoStack]);
+ } else if (a.action === Action.MOVE) {
+ let originalPositions = {};
+ if (Array.isArray(a.id)) { // Multiple moves
+ originalPositions = a.id.reduce((acc, id) => {
+ let elementArr;
+ if (a.element === ObjectType.TABLE) elementArr = tables;
+ else if (a.element === ObjectType.AREA) elementArr = areas;
+ else if (a.element === ObjectType.NOTE) elementArr = notes;
+ else return acc;
+
+ const item = elementArr.find(el => el.id === id);
+ if (item) {
+ acc[id] = { x: item.x, y: item.y }; // Save current position for redo
}
+ return acc;
+ }, {});
+ // Undo the movement by restoring original positions
+ a.originalPositions.forEach(op => {
+ if (a.element === ObjectType.TABLE) updateTable(op.id, { x: op.x, y: op.y });
+ else if (a.element === ObjectType.AREA) updateArea(op.id, { x: op.x, y: op.y });
+ else if (a.element === ObjectType.NOTE) updateNote(op.id, { x: op.x, y: op.y });
+ });
+ actionForRedoStack = { ...a, newPositions: originalPositions }; // For redo, we need the positions to which it was moved
+ } else { // Individual move
+ let currentItem;
+ if (a.element === ObjectType.TABLE) currentItem = tables.find(t => t.id === a.id);
+ else if (a.element === ObjectType.AREA) currentItem = areas.find(ar => ar.id === a.id);
+ else if (a.element === ObjectType.NOTE) currentItem = notes.find(n => n.id === a.id);
+
+ if (currentItem) {
+ actionForRedoStack = { ...a, to: { x: currentItem.x, y: currentItem.y } }; // Save current position for redo
}
- } else if (a.action === Action.DELETE) {
+ if (a.element === ObjectType.TABLE) updateTable(a.id, { x: a.from.x, y: a.from.y });
+ else if (a.element === ObjectType.AREA) updateArea(a.id, { x: a.from.x, y: a.from.y });
+ else if (a.element === ObjectType.NOTE) updateNote(a.id, { x: a.from.x, y: a.from.y });
+ }
+ setRedoStack((prev) => [...prev, actionForRedoStack]);
+ } else if (a.action === Action.DELETE) {
if (a.element === ObjectType.TABLE) {
- a.data.relationship.forEach((x) => addRelationship(x, false));
- addTable(a.data.table, false);
+ if (a.data && a.data.table) {
+ addTable(a.data.table, false);
+ }
+ if (a.data && a.data.relationship && Array.isArray(a.data.relationship)) {
+ a.data.relationship.forEach((x) => addRelationship(x, null, null, false));
+ }
} else if (a.element === ObjectType.RELATIONSHIP) {
- addRelationship(a.data, false);
+ // --- Undo the deletion of a relationship ---
+ const {
+ relationship: relationshipToRestore,
+ childTableFieldsBeforeFkDeletion, // Use the full state of the fields
+ childTableIdWithPotentiallyModifiedFields
+ } = a.data;
+
+ if (relationshipToRestore) {
+ // 1. Restore the full state of the child table's fields.
+ if (childTableFieldsBeforeFkDeletion && typeof childTableIdWithPotentiallyModifiedFields !== 'undefined') {
+ // Directly update the table with its previous fields.
+ updateTable(childTableIdWithPotentiallyModifiedFields, { fields: JSON.parse(JSON.stringify(childTableFieldsBeforeFkDeletion)) });
+ }
+ // 2. Re-add the relationship object to the relationships array.
+ addRelationship(relationshipToRestore, null, null, false);
+ }
} else if (a.element === ObjectType.NOTE) {
- addNote(a.data, false);
+ if (a.data) {
+ addNote(a.data, false);
+ }
} else if (a.element === ObjectType.AREA) {
- addArea(a.data, false);
+ if (a.data) {
+ addArea(a.data, false);
+ }
} else if (a.element === ObjectType.TYPE) {
- addType({ id: a.id, ...a.data }, false);
+ if (a.data) {
+ addType({ id: a.id, ...a.data }, false);
+ }
} else if (a.element === ObjectType.ENUM) {
- addEnum({ id: a.id, ...a.data }, false);
+ if (a.data) {
+ addEnum({ id: a.id, ...a.data }, false);
+ }
}
- setRedoStack((prev) => [...prev, a]);
+ actionForRedoStack = { ...a };
+ setRedoStack((prev) => [...prev, actionForRedoStack]);
} else if (a.action === Action.EDIT) {
+ let redoStateProperties = {};
+
if (a.element === ObjectType.AREA) {
+ const currentArea = areas.find(ar => ar.id === a.aid);
+ if (currentArea) redoStateProperties = { redo: { ...currentArea } };
updateArea(a.aid, a.undo);
} else if (a.element === ObjectType.NOTE) {
+ const currentNote = notes.find(n => n.id === a.nid);
+ if (currentNote) redoStateProperties = { redo: { ...currentNote } };
updateNote(a.nid, a.undo);
} else if (a.element === ObjectType.TABLE) {
- if (a.component === "field") {
- updateField(a.tid, a.fid, a.undo);
- } else if (a.component === "field_delete") {
- // Restores relationships
- setRelationships((prev) => {
- let temp = [...prev];
- a.data.relationship.forEach((r) => {
- temp.splice(r.id, 0, r);
- });
- temp = temp.map((e, i) => ({ ...e, id: i }));
- return temp;
- });
- // Restores the fields of the parent table
- setTables((prev) =>
- prev.map((t) =>
- t.id === a.tid ? { ...t, fields: a.data.previousFields } : t
- )
- );
- // Restores the affected child tables according to the snapshot
- if (a.data.childFieldsSnapshot) {
- setTables((prev) =>
- prev.map((t) => {
- if (a.data.childFieldsSnapshot[t.id]) {
- return { ...t, fields: a.data.childFieldsSnapshot[t.id] };
- }
- return t;
- })
- );
- }
- }else if (a.component === "field_add") {
- updateTable(a.tid, {
- fields: tables[a.tid].fields
- .filter((e) => e.id !== tables[a.tid].fields.length - 1)
- .map((t, i) => ({ ...t, id: i })),
- });
+ const currentTable = tables.find(t => t.id === a.tid);
+ if (a.component === "field_update") {
+ if (currentTable && a.data && a.data.previousFields) {
+ actionForRedoStack.data = {
+ ...a.data,
+ fieldsBeforeUndoWasApplied: JSON.parse(JSON.stringify(currentTable.fields)),
+ };
+ setTables(prevTables =>
+ prevTables.map(table => {
+ if (table.id === a.tid) {
+ return { ...table, fields: a.data.previousFields };
+ }
+ return table;
+ })
+ );
+ }
+ } else if (a.component === "field") {
+ let currentFieldStateForRedo = a.redo;
+ if (currentTable) {
+ const currentField = currentTable.fields.find(f => f.id === a.fid);
+ if (currentField) currentFieldStateForRedo = { ...currentField };
+ }
+ redoStateProperties = { redo: currentFieldStateForRedo };
+ if (typeof a.undo !== 'undefined') {
+ updateField(a.tid, a.fid, a.undo, false);
+ }
+ } else if (a.component === "field_delete") {
+ // a.data content:
+ // - field: the field that was deleted.
+ // - previousFields: the array of fields in the table BEFORE the deletion of the 'field'.
+ // - deletedRelationships: array of relationships that were deleted.
+ // - modifiedRelationshipsOriginalState: array of relationships (original state) that were modified (not deleted).
+ // - childFieldsSnapshot: object { tableId: arrayOfFieldsBeforeFkDeletion }
+ // - tid: the id of the table.
+ if (a.data && a.data.previousFields && a.data.field) {
+ actionForRedoStack.data = JSON.parse(JSON.stringify(a.data));
+ // Restore the fields of the main table to their previous state.
+ setTables(prevTables =>
+ prevTables.map(table => {
+ if (table.id === a.tid) {
+ return { ...table, fields: JSON.parse(JSON.stringify(a.data.previousFields)) };
+ }
+ return table;
+ })
+ );
+ // Restore the fields in the child tables (if snapshot was saved and restoreFieldsToTable is available).
+ if (a.data.childFieldsSnapshot && typeof restoreFieldsToTable === 'function') {
+ Object.entries(a.data.childFieldsSnapshot).forEach(([childTableId, fieldsSnapshot]) => {
+ const numericChildTableId = parseInt(childTableId, 10);
+ if (!isNaN(numericChildTableId)) {
+ restoreFieldsToTable(numericChildTableId, JSON.parse(JSON.stringify(fieldsSnapshot)));
+ }
+ });
+ }
+ // Restore the relationships that were deleted.
+ if (a.data.deletedRelationships && Array.isArray(a.data.deletedRelationships)) {
+ a.data.deletedRelationships.forEach(rel => {
+ addRelationship(JSON.parse(JSON.stringify(rel)), null, null, false);
+ });
+ }
+ // Restore the relationships that were modified (their fieldId) to their original state.
+ if (a.data.modifiedRelationshipsOriginalState && Array.isArray(a.data.modifiedRelationshipsOriginalState)) {
+ a.data.modifiedRelationshipsOriginalState.forEach(originalRel => {
+ updateRelationship(originalRel.id, JSON.parse(JSON.stringify(originalRel)), false);
+ });
+ }
+ }
+ } else if (a.component === "field_add") {
+ if (currentTable) {
+ const fieldAdded = a.data && a.data.fieldIdAdded;
+ const fieldsBeforeAdd = a.data && a.data.fieldsBeforeAdd;
+
+ if (fieldsBeforeAdd) {
+ actionForRedoStack.data = {
+ ...a.data,
+ fieldThatWasAdded: currentTable.fields.find(f => f.id === fieldAdded)
+ };
+ updateTable(a.tid, { fields: JSON.parse(JSON.stringify(fieldsBeforeAdd)) });
+ } else if (currentTable.fields.length > 0) {
+ actionForRedoStack.data = {
+ ...a.data,
+ fieldToAdd: JSON.parse(JSON.stringify(currentTable.fields[currentTable.fields.length - 1]))
+ };
+ updateTable(a.tid, { fields: currentTable.fields.slice(0, -1).map((f,i)=> ({...f, id:i})) });
+ }
+ }
} else if (a.component === "index_add") {
- updateTable(a.tid, {
- indices: tables[a.tid].indices
- .filter((e) => e.id !== tables[a.tid].indices.length - 1)
- .map((t, i) => ({ ...t, id: i })),
- });
+ if (currentTable && currentTable.indices.length > 0) {
+ actionForRedoStack.data = { ...a.data, indexToAdd: JSON.parse(JSON.stringify(currentTable.indices[currentTable.indices.length - 1])) };
+ updateTable(a.tid, { indices: currentTable.indices.slice(0, -1).map((idx,i)=> ({...idx, id:i})) });
+ }
} else if (a.component === "index") {
- updateTable(a.tid, {
- indices: tables[a.tid].indices.map((index) =>
- index.id === a.iid
- ? {
- ...index,
- ...a.undo,
- }
- : index,
- ),
- });
+ if (currentTable) {
+ const currentIndex = currentTable.indices.find(idx => idx.id === a.iid);
+ if (currentIndex) redoStateProperties = { redo: { ...currentIndex } };
+ updateTable(a.tid, { indices: currentTable.indices.map(idx => idx.id === a.iid ? {...idx, ...a.undo} : idx ) });
+ }
} else if (a.component === "index_delete") {
- setTables((prev) =>
- prev.map((table) => {
- if (table.id === a.tid) {
- const temp = table.indices.slice();
- temp.splice(a.data.id, 0, a.data);
- return {
- ...table,
- indices: temp.map((t, i) => ({ ...t, id: i })),
- };
- }
- return table;
- }),
- );
+ if (a.data && currentTable) {
+ actionForRedoStack.data = { ...a.data, indexIdToDelete: a.data.id, tid: a.tid };
+ const newIndices = [...currentTable.indices];
+ newIndices.splice(a.data.id, 0, JSON.parse(JSON.stringify(a.data)));
+ updateTable(a.tid, { indices: newIndices.map((idx,i)=> ({...idx, id:i})) });
+ }
} else if (a.component === "self") {
+ if (currentTable) redoStateProperties = { redo: { ...currentTable, ...a.redo } };
updateTable(a.tid, a.undo);
}
} else if (a.element === ObjectType.RELATIONSHIP) {
+ const currentRel = relationships.find(r => r.id === a.rid);
+ if (currentRel) redoStateProperties = { redo: { ...currentRel, ...a.redo } };
updateRelationship(a.rid, a.undo);
} else if (a.element === ObjectType.TYPE) {
+ const currentType = types.find(ty => ty.id === a.tid);
if (a.component === "field_add") {
- updateType(a.tid, {
- fields: types[a.tid].fields.filter(
- (_, i) => i !== types[a.tid].fields.length - 1,
- ),
- });
- }
- if (a.component === "field") {
- updateType(a.tid, {
- fields: types[a.tid].fields.map((e, i) =>
- i === a.fid ? { ...e, ...a.undo } : e,
- ),
- });
+ if (currentType) {
+ actionForRedoStack.data = { ...a.data, fieldToAdd: JSON.parse(JSON.stringify(currentType.fields[currentType.fields.length - 1])) };
+ updateType(a.tid, {
+ fields: currentType.fields.slice(0, -1).map((f,i)=> ({...f, id:i})),
+ });
+ }
+ } else if (a.component === "field") {
+ if (currentType) {
+ const currentField = currentType.fields.find(f => f.id === a.fid);
+ if (currentField) redoStateProperties = { redo: { ...currentField } };
+ updateType(a.tid, {
+ fields: currentType.fields.map(f =>
+ f.id === a.fid ? { ...f, ...a.undo } : f,
+ ),
+ });
+ }
} else if (a.component === "field_delete") {
- setTypes((prev) =>
- prev.map((t, i) => {
- if (i === a.tid) {
- const temp = t.fields.slice();
- temp.splice(a.fid, 0, a.data);
- return { ...t, fields: temp };
- }
- return t;
- }),
- );
+ if (a.data && currentType) {
+ actionForRedoStack.data = { ...a.data, fieldIdToDelete: a.data.id, typeId: a.tid };
+ const newFields = [...currentType.fields];
+ // Find the index of the field to delete
+ const originalIndex = currentType.fields.findIndex(f => f.id === a.data.id);
+ newFields.splice(originalIndex !== -1 ? originalIndex : newFields.length, 0, JSON.parse(JSON.stringify(a.data)));
+ updateType(a.tid, { fields: newFields.map((f,i)=> ({...f, id:i})) });
+ }
} else if (a.component === "self") {
+ if (currentType) redoStateProperties = { redo: { ...currentType, ...a.redo } };
updateType(a.tid, a.undo);
- if (a.updatedFields) {
- if (a.undo.name) {
- a.updatedFields.forEach((x) =>
- updateField(x.tid, x.fid, { type: a.undo.name.toUpperCase() }),
- );
- }
+ if (a.updatedFields && a.undo.name) {
+ a.updatedFields.forEach((x) =>
+ updateField(x.tid, x.fid, { type: x.originalType }),
+ );
}
}
} else if (a.element === ObjectType.ENUM) {
+ const currentEnum = enums.find(en => en.id === a.id);
+ if (currentEnum) redoStateProperties = { redo: { ...currentEnum, ...a.redo } };
updateEnum(a.id, a.undo);
- if (a.updatedFields) {
- if (a.undo.name) {
- a.updatedFields.forEach((x) =>
- updateField(x.tid, x.fid, { type: a.undo.name.toUpperCase() }),
+ if (a.updatedFields && a.undo.name) {
+ a.updatedFields.forEach((x) =>
+ updateField(x.tid, x.fid, { type: x.originalType }),
);
- }
}
}
- setRedoStack((prev) => [...prev, a]);
+ if (Object.keys(redoStateProperties).length > 0) {
+ actionForRedoStack = { ...actionForRedoStack, ...redoStateProperties };
+ }
+ setRedoStack((prev) => [...prev, actionForRedoStack]);
} else if (a.action === Action.PAN) {
- setTransform((prev) => ({
- ...prev,
+ actionForRedoStack = { ...a, redo: { ...transform.pan } };
+ setTransform((prevTransform) => ({
+ ...prevTransform,
pan: a.undo,
}));
- setRedoStack((prev) => [...prev, a]);
+ setRedoStack((prev) => [...prev, actionForRedoStack]);
}
};
const redo = () => {
if (redoStack.length === 0) return;
const a = redoStack[redoStack.length - 1];
- setRedoStack((prev) => prev.filter((e, i) => i !== prev.length - 1));
+ setRedoStack((prev) => prev.filter((_, i) => i !== prev.length - 1));
+
+ let actionForUndoStack = { ...a };
+
if (a.action === Action.ADD) {
if (a.element === ObjectType.TABLE) {
- addTable(null, false);
+ addTable(a.data ? a.data.table || a.data : null, false);
} else if (a.element === ObjectType.AREA) {
- addArea(null, false);
+ addArea(a.data || null, false);
} else if (a.element === ObjectType.NOTE) {
- addNote(null, false);
+ addNote(a.data || null, false);
} else if (a.element === ObjectType.RELATIONSHIP) {
- addRelationship(a.data, false);
+ const { relationship, autoGeneratedFkFields, childTableIdWithGeneratedFks } = a.data;
+ addRelationship(relationship, autoGeneratedFkFields, childTableIdWithGeneratedFks, false);
} else if (a.element === ObjectType.TYPE) {
- addType(null, false);
+ addType(a.data ? {id: a.id, ...a.data} : null, false);
} else if (a.element === ObjectType.ENUM) {
- addEnum(null, false);
+ addEnum(a.data ? {id: a.id, ...a.data} : null, false);
}
- setUndoStack((prev) => [...prev, a]);
+ setUndoStack((prev) => [...prev, actionForUndoStack]);
} else if (a.action === Action.MOVE) {
- if (a.element === ObjectType.TABLE) {
- setUndoStack((prev) => [
- ...prev,
- { ...a, x: tables[a.id].x, y: tables[a.id].y },
- ]);
- updateTable(a.id, { x: a.x, y: a.y });
- } else if (a.element === ObjectType.AREA) {
- setUndoStack((prev) => [
- ...prev,
- { ...a, x: areas[a.id].x, y: areas[a.id].y },
- ]);
- updateArea(a.id, { x: a.x, y: a.y });
- } else if (a.element === ObjectType.NOTE) {
- setUndoStack((prev) => [
- ...prev,
- { ...a, x: notes[a.id].x, y: notes[a.id].y },
- ]);
- updateNote(a.id, { x: a.x, y: a.y });
+ if (!Array.isArray(a.id)) {
+ let itemBeforeMove;
+ if (a.element === ObjectType.TABLE) itemBeforeMove = tables.find(t => t.id === a.id);
+ else if (a.element === ObjectType.AREA) itemBeforeMove = areas.find(ar => ar.id === a.id);
+ else if (a.element === ObjectType.NOTE) itemBeforeMove = notes.find(n => n.id === a.id);
+
+ if (itemBeforeMove) {
+ actionForUndoStack.from = { x: itemBeforeMove.x, y: itemBeforeMove.y }; // Current pos becomes 'from' for next undo
+ }
+ // Apply the redo move (a.to contains the target position)
+ if (a.element === ObjectType.TABLE) updateTable(a.id, { x: a.to.x, y: a.to.y });
+ else if (a.element === ObjectType.AREA) updateArea(a.id, { x: a.to.x, y: a.to.y });
+ else if (a.element === ObjectType.NOTE) updateNote(a.id, { x: a.to.x, y: a.to.y });
+ } else { // Multiple moves
+ const currentPositions = a.id.reduce((acc, id) => {
+ let elementArr;
+ if (a.element === ObjectType.TABLE) elementArr = tables;
+ else if (a.element === ObjectType.AREA) elementArr = areas;
+ else if (a.element === ObjectType.NOTE) elementArr = notes;
+ else return acc;
+ const item = elementArr.find(el => el.id === id);
+ if (item) acc[id] = { x: item.x, y: item.y };
+ return acc;
+ }, {});
+ actionForUndoStack.originalPositions = Object.values(currentPositions).map((pos, index) => ({ id: a.id[index], ...pos }));
+
+ a.newPositions.forEach(np => { // a.newPositions has target positions for redo
+ if (a.element === ObjectType.TABLE) updateTable(np.id, { x: np.x, y: np.y });
+ else if (a.element === ObjectType.AREA) updateArea(np.id, { x: np.x, y: np.y });
+ else if (a.element === ObjectType.NOTE) updateNote(np.id, { x: np.x, y: np.y });
+ });
}
+ setUndoStack((prev) => [...prev, actionForUndoStack]);
} else if (a.action === Action.DELETE) {
if (a.element === ObjectType.TABLE) {
- deleteTable(a.data.table.id, false);
+ // a.data.table should be the table object that was deleted
+ if (a.data && a.data.table) deleteTable(a.data.table.id, false);
} else if (a.element === ObjectType.RELATIONSHIP) {
- deleteRelationship(a.data.id, false);
+ // a.data.relationship is the relationship object that was deleted
+ if (a.data && a.data.relationship) deleteRelationship(a.data.relationship.id, false);
} else if (a.element === ObjectType.NOTE) {
- deleteNote(a.data.id, false);
+ if (a.data) deleteNote(a.data.id, false);
} else if (a.element === ObjectType.AREA) {
- deleteArea(a.data.id, false);
+ if (a.data) deleteArea(a.data.id, false);
} else if (a.element === ObjectType.TYPE) {
- deleteType(a.id, false);
+ if (a.data) deleteType(a.id, false);
} else if (a.element === ObjectType.ENUM) {
- deleteEnum(a.id, false);
+ if (a.data) deleteEnum(a.id, false);
}
- setUndoStack((prev) => [...prev, a]);
+ setUndoStack((prev) => [...prev, actionForUndoStack]);
} else if (a.action === Action.EDIT) {
+ let undoStateProperties = {};
+
if (a.element === ObjectType.AREA) {
+ const areaBeforeRedo = areas.find(ar => ar.id === a.aid);
+ if (areaBeforeRedo) undoStateProperties = { undo: { ...areaBeforeRedo } };
+ else if (a.undo) undoStateProperties = { undo: a.undo };
updateArea(a.aid, a.redo);
} else if (a.element === ObjectType.NOTE) {
+ const noteBeforeRedo = notes.find(n => n.id === a.nid);
+ if (noteBeforeRedo) undoStateProperties = { undo: { ...noteBeforeRedo } };
+ else if (a.undo) undoStateProperties = { undo: a.undo };
updateNote(a.nid, a.redo);
} else if (a.element === ObjectType.TABLE) {
- if (a.component === "field") {
- updateField(a.tid, a.fid, a.redo);
+ const tableBeforeRedo = tables.find(t => t.id === a.tid);
+ if (a.component === "field_update") {
+ if (tableBeforeRedo && a.data && typeof a.data.updatedFieldId !== 'undefined' && a.data.appliedValues) {
+ actionForUndoStack.data = {
+ ...a.data,
+ previousFields: JSON.parse(JSON.stringify(tableBeforeRedo.fields)),
+ };
+ updateField(a.tid, a.data.updatedFieldId, a.data.appliedValues, false);
+ }
+ } else if (a.component === "field") {
+ let previousFieldStateForUndo = a.undo;
+ if (tableBeforeRedo) {
+ const fieldBeforeRedo = tableBeforeRedo.fields.find(f => f.id === a.fid);
+ if (fieldBeforeRedo) previousFieldStateForUndo = { ...fieldBeforeRedo };
+ }
+ undoStateProperties = { undo: previousFieldStateForUndo };
+ if (typeof a.redo !== 'undefined') {
+ updateField(a.tid, a.fid, a.redo, false);
+ }
} else if (a.component === "field_delete") {
- deleteField(a.data.field, a.tid, false);
+ // Redoing a field_delete means calling deleteField again.
+ // a.data should contain { field, deletedRelationships, modifiedRelationshipsOriginalState, previousFields, childFieldsSnapshot }
+ // The 'previousFields' in a.data is the state *before* the original deletion, which is what the *next* undo needs.
+ if (a.data && a.data.field && typeof a.data.tid !== 'undefined') {
+ actionForUndoStack.data = JSON.parse(JSON.stringify(a.data));
+
+ deleteField(a.data.field, a.data.tid, false);
+ }
} else if (a.component === "field_add") {
- updateTable(a.tid, {
- fields: [
- ...tables[a.tid].fields,
- {
- name: "",
- type: "",
- default: "",
- check: "",
- primary: false,
- unique: false,
- notNull: false,
- increment: false,
- comment: "",
- id: tables[a.tid].fields.length,
- },
- ],
- });
+ if (tableBeforeRedo && a.data && a.data.fieldThatWasAdded) {
+ actionForUndoStack.data = {
+ ...a.data,
+ fieldsBeforeAdd: JSON.parse(JSON.stringify(tableBeforeRedo.fields))
+ };
+ const newFields = [...tableBeforeRedo.fields, JSON.parse(JSON.stringify(a.data.fieldThatWasAdded))];
+ updateTable(a.tid, { fields: newFields.map((f, i) => ({ ...f, id: i })) }, false);
+ }
} else if (a.component === "index_add") {
- setTables((prev) =>
- prev.map((table) => {
- if (table.id === a.tid) {
- return {
- ...table,
- indices: [
- ...table.indices,
- {
- id: table.indices.length,
- name: `index_${table.indices.length}`,
- fields: [],
- },
- ],
- };
- }
- return table;
- }),
- );
+ if (tableBeforeRedo && a.data && a.data.indexToAdd) {
+ actionForUndoStack.data = {
+ ...a.data,
+ indicesBeforeAdd: JSON.parse(JSON.stringify(tableBeforeRedo.indices || []))
+ };
+ const newIndices = [...(tableBeforeRedo.indices || []), JSON.parse(JSON.stringify(a.data.indexToAdd))];
+ updateTable(a.tid, { indices: newIndices.map((idx, i) => ({ ...idx, id: i })) }, false);
+ }
} else if (a.component === "index") {
- updateTable(a.tid, {
- indices: tables[a.tid].indices.map((index) =>
- index.id === a.iid
- ? {
- ...index,
- ...a.redo,
- }
- : index,
- ),
- });
+ if (tableBeforeRedo) {
+ const indexBeforeRedo = tableBeforeRedo.indices.find(idx => idx.id === a.iid);
+ if (indexBeforeRedo) undoStateProperties = { undo: { ...indexBeforeRedo } };
+ else if (a.undo) undoStateProperties = { undo: a.undo };
+ updateTable(a.tid, { indices: tableBeforeRedo.indices.map(idx => idx.id === a.iid ? {...idx, ...a.redo} : idx ) }, false);
+ }
} else if (a.component === "index_delete") {
- updateTable(a.tid, {
- indices: tables[a.tid].indices
- .filter((e) => e.id !== a.data.id)
- .map((t, i) => ({ ...t, id: i })),
- });
+ if (tableBeforeRedo && a.data && typeof a.data.id !== 'undefined') {
+ actionForUndoStack.data = JSON.parse(JSON.stringify(a.data));
+ const newIndices = tableBeforeRedo.indices.filter(idx => idx.id !== a.data.id).map((idx, i) => ({...idx, id: i}));
+ updateTable(a.tid, { indices: newIndices }, false);
+ }
} else if (a.component === "self") {
+ if (tableBeforeRedo) undoStateProperties = { undo: { ...tableBeforeRedo, ...a.undo } };
updateTable(a.tid, a.redo, false);
}
} else if (a.element === ObjectType.RELATIONSHIP) {
- updateRelationship(a.rid, a.redo);
+ const relBeforeRedo = relationships.find(r => r.id === a.rid);
+ if (relBeforeRedo) undoStateProperties = { undo: { ...relBeforeRedo } };
+ else if (a.undo) undoStateProperties = { undo: a.undo };
+ updateRelationship(a.rid, a.redo, false);
} else if (a.element === ObjectType.TYPE) {
+ const typeBeforeRedo = types.find(ty => ty.id === a.tid);
if (a.component === "field_add") {
- updateType(a.tid, {
- fields: [
- ...types[a.tid].fields,
- {
- name: "",
- type: "",
- },
- ],
- });
+ // a.data.fieldToAdd was prepared by undo
+ if (typeBeforeRedo && a.data && a.data.fieldToAdd) {
+ actionForUndoStack.data = {
+ ...a.data,
+ fieldsBeforeAdd: JSON.parse(JSON.stringify(typeBeforeRedo.fields || []))
+ };
+ const newFields = [...(typeBeforeRedo.fields || []), JSON.parse(JSON.stringify(a.data.fieldToAdd))];
+ updateType(a.tid, { fields: newFields.map((f, i) => ({ ...f, id: i })) }, false);
+ }
} else if (a.component === "field") {
- updateType(a.tid, {
- fields: types[a.tid].fields.map((e, i) =>
- i === a.fid ? { ...e, ...a.redo } : e,
- ),
- });
+ if (typeBeforeRedo) {
+ const fieldBeforeRedo = typeBeforeRedo.fields.find(f => f.id === a.fid);
+ if (fieldBeforeRedo) undoStateProperties = { undo: { ...fieldBeforeRedo } };
+ else if (a.undo) undoStateProperties = { undo: a.undo };
+ // Apply redo
+ updateType(a.tid, {
+ fields: typeBeforeRedo.fields.map(f =>
+ f.id === a.fid ? { ...f, ...a.redo } : f,
+ ),
+ }, false);
+ }
} else if (a.component === "field_delete") {
- updateType(a.tid, {
- fields: types[a.tid].fields.filter((field, i) => i !== a.fid),
- });
- } else if (a.component === "self") {
- updateType(a.tid, a.redo);
- if (a.updatedFields) {
- if (a.redo.name) {
- a.updatedFields.forEach((x) =>
- updateField(x.tid, x.fid, { type: a.redo.name.toUpperCase() }),
- );
- }
+ // Redoing a field_delete for a type field
+ // a.data is the field object that was deleted.
+ if (typeBeforeRedo && a.data && typeof a.data.id !== 'undefined') {
+ actionForUndoStack.data = JSON.parse(JSON.stringify(a.data)); // Save deleted field for next undo
+ const newFields = typeBeforeRedo.fields.filter(f => f.id !== a.data.id).map((f,i) => ({...f, id:i}));
+ updateType(a.tid, { fields: newFields }, false);
}
+ } else if (a.component === "self") {
+ if (typeBeforeRedo) undoStateProperties = { undo: { ...typeBeforeRedo, ...a.undo } };
+ updateType(a.tid, a.redo, false);
}
} else if (a.element === ObjectType.ENUM) {
- updateEnum(a.id, a.redo);
- if (a.updatedFields) {
- if (a.redo.name) {
- a.updatedFields.forEach((x) =>
- updateField(x.tid, x.fid, { type: a.redo.name.toUpperCase() }),
- );
- }
- }
+ const enumBeforeRedo = enums.find(en => en.id === a.id);
+ if (enumBeforeRedo) undoStateProperties = { undo: { ...enumBeforeRedo } };
+ else if (a.undo) undoStateProperties = { undo: a.undo };
+ updateEnum(a.id, a.redo, false);
+ // Similar to TYPE self, the updatedFields logic might be for undo
}
- setUndoStack((prev) => [...prev, a]);
+
+ // Merge general undo properties if they were set and not handled by direct data manipulation
+ const componentHandledDataDirectly =
+ (a.element === ObjectType.TABLE && (a.component === "field_update" || a.component === "field_delete" || a.component === "field_add" || a.component === "index_add" || a.component === "index_delete")) ||
+ (a.element === ObjectType.TYPE && (a.component === "field_add" || a.component === "field_delete"));
+
+ if (Object.keys(undoStateProperties).length > 0 && !componentHandledDataDirectly) {
+ actionForUndoStack = { ...actionForUndoStack, ...undoStateProperties };
+ }
+ setUndoStack((prev) => [...prev, actionForUndoStack]);
} else if (a.action === Action.PAN) {
- setTransform((prev) => ({
- ...prev,
+ actionForUndoStack = { ...a, undo: { ...transform.pan } };
+ setTransform((prevTransform) => ({
+ ...prevTransform,
pan: a.redo,
}));
- setUndoStack((prev) => [...prev, a]);
+ setUndoStack((prev) => [...prev, actionForUndoStack]);
}
};
@@ -788,9 +944,18 @@ export default function ControlPanel({
.catch(() => Toast.error(t("oops_smth_went_wrong")));
},
},
- import_diagram: {
- function: fileImport,
- shortcut: "Ctrl+I",
+ import_from: {
+ children: [
+ {
+ JSON: fileImport,
+ },
+ {
+ DBML: () => {
+ setModal(MODAL.IMPORT);
+ setImportFrom(IMPORT_FROM.DBML);
+ },
+ },
+ ],
},
import_from_source: {
...(database === DB.GENERIC && {
@@ -985,6 +1150,21 @@ export default function ControlPanel({
setModal(MODAL.IMG);
},
},
+ {
+ SVG: () => {
+ const filter = (node) => node.tagName !== "i";
+ toSvg(document.getElementById("canvas"), { filter: filter }).then(
+ function (dataUrl) {
+ setExportData((prev) => ({
+ ...prev,
+ data: dataUrl,
+ extension: "svg",
+ }));
+ },
+ );
+ setModal(MODAL.IMG);
+ },
+ },
{
JSON: () => {
setModal(MODAL.CODE);
@@ -1010,18 +1190,18 @@ export default function ControlPanel({
},
},
{
- SVG: () => {
- const filter = (node) => node.tagName !== "i";
- toSvg(document.getElementById("canvas"), { filter: filter }).then(
- function (dataUrl) {
- setExportData((prev) => ({
- ...prev,
- data: dataUrl,
- extension: "svg",
- }));
- },
- );
- setModal(MODAL.IMG);
+ DBML: () => {
+ setModal(MODAL.CODE);
+ const result = toDBML({
+ tables,
+ relationships,
+ enums,
+ });
+ setExportData((prev) => ({
+ ...prev,
+ data: result,
+ extension: "dbml",
+ }));
},
},
{
@@ -1044,30 +1224,6 @@ export default function ControlPanel({
});
},
},
- {
- DRAWDB: () => {
- const result = JSON.stringify(
- {
- author: "Unnamed",
- title: title,
- date: new Date().toISOString(),
- tables: tables,
- relationships: relationships,
- notes: notes,
- subjectAreas: areas,
- database: database,
- ...(databases[database].hasTypes && { types: types }),
- ...(databases[database].hasEnums && { enums: enums }),
- },
- null,
- 2,
- );
- const blob = new Blob([result], {
- type: "text/plain;charset=utf-8",
- });
- saveAs(blob, `${exportData.filename}.ddb`);
- },
- },
{
MERMAID: () => {
setModal(MODAL.CODE);
@@ -1252,6 +1408,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 ? (
@@ -1364,12 +1540,15 @@ export default function ControlPanel({
},
},
help: {
- shortcuts: {
- function: () => window.open("/shortcuts", "_blank"),
+ docs: {
+ function: () => window.open(`${socials.docs}`, "_blank"),
shortcut: "Ctrl+H",
},
+ shortcuts: {
+ function: () => window.open(`${socials.docs}/shortcuts`, "_blank"),
+ },
ask_on_discord: {
- function: () => window.open("https://discord.gg/BrjZgNrmR6", "_blank"),
+ function: () => window.open(socials.discord, "_blank"),
},
report_bug: {
function: () => window.open("/bug-report", "_blank"),
@@ -1397,18 +1576,18 @@ export default function ControlPanel({
useHotkeys("ctrl+shift+m, meta+shift+m", viewStrictMode, {
preventDefault: true,
});
- useHotkeys("ctrl+shift+f, meta+shift+f", viewFieldSummary, {
+ useHotkeys("mod+shift+f", viewFieldSummary, {
preventDefault: true,
});
- useHotkeys("ctrl+shift+s, meta+shift+s", saveDiagramAs, {
+ useHotkeys("mod+shift+s", saveDiagramAs, {
preventDefault: true,
});
- useHotkeys("ctrl+alt+c, meta+alt+c", copyAsImage, { preventDefault: true });
- useHotkeys("ctrl+r, meta+r", resetView, { preventDefault: true });
- useHotkeys("ctrl+h, meta+h", () => window.open("/shortcuts", "_blank"), {
+ useHotkeys("mod+alt+c", copyAsImage, { preventDefault: true });
+ useHotkeys("mod+r", resetView, { preventDefault: true });
+ useHotkeys("mod+h", () => window.open(socials.docs, "_blank"), {
preventDefault: true,
});
- useHotkeys("ctrl+alt+w, meta+alt+w", fitWindow, { preventDefault: true });
+ useHotkeys("mod+alt+w", fitWindow, { preventDefault: true });
return (
<>
@@ -1442,6 +1621,7 @@ export default function ControlPanel({
setTitle={setTitle}
setDiagramId={setDiagramId}
setModal={setModal}
+ importFrom={importFrom}
importDb={importDb}
/>
- {t(category)}
+ {t(category)}
))}
diff --git a/src/components/EditorHeader/Modal/ImportDiagram.jsx b/src/components/EditorHeader/Modal/ImportDiagram.jsx
index e84af2a30..b6d83872a 100644
--- a/src/components/EditorHeader/Modal/ImportDiagram.jsx
+++ b/src/components/EditorHeader/Modal/ImportDiagram.jsx
@@ -3,7 +3,7 @@ import {
jsonDiagramIsValid,
} from "../../../utils/validateSchema";
import { Upload, Banner } from "@douyinfe/semi-ui";
-import { DB, STATUS } from "../../../data/constants";
+import { DB, IMPORT_FROM, STATUS } from "../../../data/constants";
import {
useAreas,
useEnums,
@@ -12,8 +12,14 @@ import {
useTypes,
} from "../../../hooks";
import { useTranslation } from "react-i18next";
+import { fromDBML } from "../../../utils/importFrom/dbml";
-export default function ImportDiagram({ setImportData, error, setError }) {
+export default function ImportDiagram({
+ setImportData,
+ error,
+ setError,
+ importFrom,
+}) {
const { areas } = useAreas();
const { notes } = useNotes();
const { tables, relationships, database } = useDiagram();
@@ -32,6 +38,125 @@ export default function ImportDiagram({ setImportData, error, setError }) {
);
};
+ const loadJsonData = (file, e) => {
+ let jsonObject = null;
+ try {
+ jsonObject = JSON.parse(e.target.result);
+ } catch (error) {
+ setError({
+ type: STATUS.ERROR,
+ message: "The file contains an error.",
+ });
+ return;
+ }
+
+ if (file.type === "application/json") {
+ if (!jsonDiagramIsValid(jsonObject)) {
+ setError({
+ type: STATUS.ERROR,
+ message: "The file is missing necessary properties for a diagram.",
+ });
+ return;
+ }
+ } else if (file.name.split(".").pop() === "ddb") {
+ if (!ddbDiagramIsValid(jsonObject)) {
+ setError({
+ type: STATUS.ERROR,
+ message: "The file is missing necessary properties for a diagram.",
+ });
+ return;
+ }
+ }
+
+ if (!jsonObject.database) {
+ jsonObject.database = DB.GENERIC;
+ }
+
+ if (jsonObject.database !== database) {
+ setError({
+ type: STATUS.ERROR,
+ message:
+ "The imported diagram and the open diagram don't use matching databases.",
+ });
+ return;
+ }
+
+ let ok = true;
+ jsonObject.relationships.forEach((rel) => {
+ if (
+ !jsonObject.tables[rel.startTableId] ||
+ !jsonObject.tables[rel.endTableId]
+ ) {
+ setError({
+ type: STATUS.ERROR,
+ message: `Relationship ${rel.name} references a table that does not exist.`,
+ });
+ ok = false;
+ return;
+ }
+
+ if (
+ !jsonObject.tables[rel.startTableId].fields[rel.startFieldId] ||
+ !jsonObject.tables[rel.endTableId].fields[rel.endFieldId]
+ ) {
+ setError({
+ type: STATUS.ERROR,
+ message: `Relationship ${rel.name} references a field that does not exist.`,
+ });
+ ok = false;
+ return;
+ }
+ });
+
+ if (!ok) return;
+
+ setImportData(jsonObject);
+ if (diagramIsEmpty()) {
+ setError({
+ type: STATUS.OK,
+ message: "Everything looks good. You can now import.",
+ });
+ } else {
+ setError({
+ type: STATUS.WARNING,
+ message:
+ "The current diagram is not empty. Importing a new diagram will overwrite the current changes.",
+ });
+ }
+ };
+
+ const loadDBMLData = (e) => {
+ try {
+ setImportData(fromDBML(e.target.result));
+ } catch (error) {
+ const message = `${error.diags[0].name} [Ln ${error.diags[0].location.start.line}, Col ${error.diags[0].location.start.column}]: ${error.diags[0].message}`;
+
+ setError({ type: STATUS.ERROR, message });
+ }
+ };
+
+ const getAcceptableFileTypes = () => {
+ switch (importFrom) {
+ case IMPORT_FROM.JSON:
+ return "application/json,.ddb";
+ case IMPORT_FROM.DBML:
+ return ".dbml";
+ default:
+ return "";
+ }
+ };
+
+ const getDragSubText = () => {
+ switch (importFrom) {
+ case IMPORT_FROM.JSON:
+ return `${t("supported_types")} JSON, DDB`;
+ case IMPORT_FROM.DBML:
+ return `${t("supported_types")} DBML`;
+ default:
+ return "";
+ }
+ };
+
return (
{
- let jsonObject = null;
- try {
- jsonObject = JSON.parse(e.target.result);
- } catch (error) {
- setError({
- type: STATUS.ERROR,
- message: "The file contains an error.",
- });
- return;
- }
- if (f.type === "application/json") {
- if (!jsonDiagramIsValid(jsonObject)) {
- setError({
- type: STATUS.ERROR,
- message:
- "The file is missing necessary properties for a diagram.",
- });
- return;
- }
- } else if (f.name.split(".").pop() === "ddb") {
- if (!ddbDiagramIsValid(jsonObject)) {
- setError({
- type: STATUS.ERROR,
- message:
- "The file is missing necessary properties for a diagram.",
- });
- return;
- }
- }
-
- if (!jsonObject.database) {
- jsonObject.database = DB.GENERIC;
- }
-
- if (jsonObject.database !== database) {
- setError({
- type: STATUS.ERROR,
- message:
- "The imported diagram and the open diagram don't use matching databases.",
- });
- return;
- }
-
- let ok = true;
- jsonObject.relationships.forEach((rel) => {
- if (
- !jsonObject.tables[rel.startTableId] ||
- !jsonObject.tables[rel.endTableId]
- ) {
- setError({
- type: STATUS.ERROR,
- message: `Relationship ${rel.name} references a table that does not exist.`,
- });
- ok = false;
- return;
- }
-
- if (
- !jsonObject.tables[rel.startTableId].fields[rel.startFieldId] ||
- !jsonObject.tables[rel.endTableId].fields[rel.endFieldId]
- ) {
- setError({
- type: STATUS.ERROR,
- message: `Relationship ${rel.name} references a field that does not exist.`,
- });
- ok = false;
- return;
- }
- });
-
- if (!ok) return;
-
- setImportData(jsonObject);
- if (diagramIsEmpty()) {
- setError({
- type: STATUS.OK,
- message: "Everything looks good. You can now import.",
- });
- } else {
- setError({
- type: STATUS.WARNING,
- message:
- "The current diagram is not empty. Importing a new diagram will overwrite the current changes.",
- });
- }
+ if (importFrom == IMPORT_FROM.JSON) loadJsonData(f, e);
+ if (importFrom == IMPORT_FROM.DBML) loadDBMLData(e);
};
reader.readAsText(f);
@@ -140,8 +182,8 @@ export default function ImportDiagram({ setImportData, error, setError }) {
}}
draggable={true}
dragMainText={t("drag_and_drop_files")}
- dragSubText={t("support_json_and_ddb")}
- accept="application/json,.ddb"
+ dragSubText={getDragSubText()}
+ accept={getAcceptableFileTypes()}
onRemove={() =>
setError({
type: STATUS.NONE,
diff --git a/src/components/EditorHeader/Modal/Modal.jsx b/src/components/EditorHeader/Modal/Modal.jsx
index 8db36c466..270172e67 100644
--- a/src/components/EditorHeader/Modal/Modal.jsx
+++ b/src/components/EditorHeader/Modal/Modal.jsx
@@ -48,6 +48,7 @@ export default function Modal({
exportData,
setExportData,
importDb,
+ importFrom,
}) {
const { t, i18n } = useTranslation();
const { setTables, setRelationships, database, setDatabase } = useDiagram();
@@ -75,8 +76,8 @@ export default function Modal({
const overwriteDiagram = () => {
setTables(importData.tables);
setRelationships(importData.relationships);
- setAreas(importData.subjectAreas);
- setNotes(importData.notes);
+ setAreas(importData.subjectAreas ?? []);
+ setNotes(importData.notes ?? []);
if (importData.title) {
setTitle(importData.title);
}
@@ -247,6 +248,7 @@ export default function Modal({
setImportData={setImportData}
error={error}
setError={setError}
+ importFrom={importFrom}
/>
);
case MODAL.IMPORT_SRC:
diff --git a/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx b/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx
index 3cfc4516b..4faeb1804 100644
--- a/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx
+++ b/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx
@@ -6,6 +6,7 @@ import {
Popover,
Table,
Input,
+ Checkbox,
} from "@douyinfe/semi-ui";
import {
IconDeleteStroked,
@@ -13,16 +14,19 @@ import {
IconMore,
} from "@douyinfe/semi-icons";
import {
- Cardinality,
+ RelationshipType,
+ RelationshipCardinalities,
Constraint,
+ SubtypeRestriction,
Action,
ObjectType,
+ Notation,
} from "../../../data/constants";
-import { useDiagram, useUndoRedo } from "../../../hooks";
+import { useDiagram, useUndoRedo} from "../../../hooks";
import i18n from "../../../i18n/i18n";
import { useTranslation } from "react-i18next";
import { useState } from "react";
-
+import { useSettings } from "../../../hooks";
const columns = [
{
title: i18n.t("primary"),
@@ -36,10 +40,11 @@ const columns = [
export default function RelationshipInfo({ data }) {
const { setUndoStack, setRedoStack } = useUndoRedo();
- const { tables, setRelationships, deleteRelationship, updateRelationship } =
+ const { tables, setTables, setRelationships, deleteRelationship, updateRelationship } =
useDiagram();
const { t } = useTranslation();
const [editField, setEditField] = useState({});
+ const { settings } = useSettings();
const swapKeys = () => {
setUndoStack((prev) => [
@@ -85,6 +90,42 @@ export default function RelationshipInfo({ data }) {
);
};
+ const changeRelationshipType = (value) => {
+ const defaultCardinality =
+ RelationshipCardinalities[value] && RelationshipCardinalities[value][0]
+ ? RelationshipCardinalities[value][0].label
+ : "";
+
+ setUndoStack((prev) => [
+ ...prev,
+ {
+ action: Action.EDIT,
+ element: ObjectType.RELATIONSHIP,
+ rid: data.id,
+ undo: {
+ relationshipType: data.relationshipType,
+ cardinality: data.cardinality,
+ },
+ redo: {
+ relationshipType: value,
+ cardinality: defaultCardinality,
+ },
+ message: t("edit_relationship", {
+ refName: data.name,
+ extra: "[relationship type]",
+ }),
+ },
+ ]);
+ setRedoStack([]);
+ setRelationships((prev) =>
+ prev.map((e, idx) =>
+ idx === data.id
+ ? { ...e, relationshipType: value, cardinality: defaultCardinality }
+ : e,
+ ),
+ );
+ };
+
const changeCardinality = (value) => {
setUndoStack((prev) => [
...prev,
@@ -108,6 +149,54 @@ export default function RelationshipInfo({ data }) {
);
};
+ const changeSubtypeRestriction = (value) => {
+ setUndoStack((prev) => [
+ ...prev,
+ {
+ action: Action.EDIT,
+ element: ObjectType.RELATIONSHIP,
+ rid: data.id,
+ undo: { subtype_restriction: data.subtype_restriction },
+ redo: { subtype_restriction: value },
+ message: t("edit_relationship", {
+ refName: data.name,
+ extra: "[subtype_restriction]",
+ }),
+ },
+ ]);
+ setRedoStack([]);
+ setRelationships((prev) =>
+ prev.map((e, idx) =>
+ idx === data.id ? { ...e, subtype_restriction: value } : e,
+ ),
+ );
+ }
+
+ const toggleSubtype = () => {
+ const prevVal = data.subtype;
+ setUndoStack((prev) => [
+ ...prev,
+ {
+ action: Action.EDIT,
+ element: ObjectType.RELATIONSHIP,
+ rid: data.id,
+ undo: { subtype: data.subtype },
+ redo: { subtype: !data.subtype },
+ message: t("edit_relationship", {
+ refName: data.name,
+ extra: "[subtype]",
+ }),
+ },
+ ]);
+
+ setRedoStack([]);
+ setRelationships((prev) =>
+ prev.map((e, idx) =>
+ idx === data.id ? { ...e, subtype: !prevVal } : e,
+ ),
+ );
+ };
+
const changeConstraint = (key, value) => {
const undoKey = `${key}Constraint`;
setUndoStack((prev) => [
@@ -130,6 +219,57 @@ export default function RelationshipInfo({ data }) {
);
};
+ const toggleParentCardinality = () => {
+ const startTable = tables.find((t) => t.id === data.endTableId);
+ if (!startTable) return;
+
+ const fkFieldIds = Array.isArray(data.endFieldId)
+ ? data.endFieldId
+ : [data.endFieldId];
+
+ const fkFields = startTable.fields.filter((f) => fkFieldIds.includes(f.id));
+ if (fkFields.length === 0) return;
+
+ const isCurrentlyNullable = fkFields.every((field) => !field.notNull);
+ const newNotNullValue = isCurrentlyNullable;
+
+ const undoFields = fkFields.map(field => ({ id: field.id, notNull: field.notNull }));
+ const redoFields = fkFields.map(field => ({ id: field.id, notNull: newNotNullValue }));
+
+ setUndoStack((prev) => [
+ ...prev,
+ {
+ action: Action.EDIT,
+ element: ObjectType.TABLE,
+ tid: startTable.id,
+ undo: { fields: undoFields },
+ redo: { fields: redoFields },
+ message: t("edit_relationship", {
+ refName: data.name,
+ extra: "[parent cardinality]",
+ }),
+ },
+ ]);
+ setRedoStack([]);
+
+ setTables((prevTables) =>
+ prevTables.map((table) => {
+ if (table.id === startTable.id) {
+ return {
+ ...table,
+ fields: table.fields.map((field) => {
+ if (fkFieldIds.includes(field.id)) {
+ return { ...field, notNull: newNotNullValue };
+ }
+ return field;
+ }),
+ };
+ }
+ return table;
+ }),
+ );
+ };
+
return (
<>
@@ -212,16 +352,73 @@ export default function RelationshipInfo({ data }) {
- {t("cardinality")}:
+ {t("relationship_type")}:
({
+ optionList={Object.values(RelationshipType).map((v) => ({
label: t(v),
value: v,
}))}
- value={data.cardinality}
+ value={data.relationshipType}
className="w-full"
- onChange={changeCardinality}
+ onChange={changeRelationshipType}
/>
+ {settings.notation !== "default" && (
+ <>
+ {t("cardinality")}:
+
+ ({
+ label: c.label,
+ value: c.label,
+ }))
+ }
+ value={data.cardinality}
+ className="w-full"
+ onChange={changeCardinality}
+ disabled={!data.relationshipType}
+ placeholder={t("select_cardinality")}
+ />
+ {(settings.notation === Notation.CROWS_FOOT ||
+ settings.notation === Notation.IDEF1X) && (
+ }
+ type="tertiary"
+ onClick={toggleParentCardinality}
+ aria-label="Toggle Parent Cardinality"
+ />
+ )}
+
+
+ {/* 👇 Aquí ya es otra fila separada */}
+
+
+ {t("subtype")}:
+
+
+
+
+
+
+ {data.subtype && (
+
+
+ {t("subtype_restriction")}:
+
+ ({
+ label: t(v),
+ value: v,
+ }))}
+ value={data.subtype_restriction}
+ className="w-full"
+ onChange={changeSubtypeRestriction}
+ />
+
+ )}
+ >
+ )}
{t("on_update")}:
@@ -248,6 +445,7 @@ export default function RelationshipInfo({ data }) {
/>
+
}
block
diff --git a/src/components/EditorSidePanel/TablesTab/TableInfo.jsx b/src/components/EditorSidePanel/TablesTab/TableInfo.jsx
index f2477b781..723d08f5e 100644
--- a/src/components/EditorSidePanel/TablesTab/TableInfo.jsx
+++ b/src/components/EditorSidePanel/TablesTab/TableInfo.jsx
@@ -343,7 +343,7 @@ export default function TableInfo({ data }) {
notNull: false,
increment: false,
comment: "",
- foreignK: false,
+ foreignK: false,
id: data.fields.length,
},
],
diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx
index 6aae11ffe..8ef775d07 100644
--- a/src/components/Navbar.jsx
+++ b/src/components/Navbar.jsx
@@ -3,6 +3,7 @@ import { Link } from "react-router-dom";
import logo from "../assets/logo_light_160.png";
import { SideSheet } from "@douyinfe/semi-ui";
import { IconMenu } from "@douyinfe/semi-icons";
+import { socials } from "../data/socials";
export default function Navbar() {
const [openMenu, setOpenMenu] = useState(false);
@@ -37,12 +38,18 @@ export default function Navbar() {
>
Templates
+
+ Docs
+
Speed up development with keyboard shortuts. See all available shortcuts
-
+
here
.
diff --git a/src/pages/NotFound.jsx b/src/pages/NotFound.jsx
index d6e6c4de2..53f199ee3 100644
--- a/src/pages/NotFound.jsx
+++ b/src/pages/NotFound.jsx
@@ -1,3 +1,5 @@
+import { socials } from "../data/socials";
+
export default function NotFound() {
return (
@@ -5,14 +7,16 @@ export default function NotFound() {
looking for something you couldn't find?
+ check out the{" "}
+
+ docs
+
+ ,{" "}
shoot us an email
{" "}
or{" "}
-
+
a message on discord
@@ -21,6 +25,12 @@ export default function NotFound() {
* to create a relationship hold the blue dot of a field and drag it
towards the field you want to connect it to
+
+ see here
+
);
}
diff --git a/src/pages/Shortcuts.jsx b/src/pages/Shortcuts.jsx
deleted file mode 100644
index 7dd93a7a4..000000000
--- a/src/pages/Shortcuts.jsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import { useEffect, useState } from "react";
-import logo_light from "../assets/logo_light_160.png";
-import logo_dark from "../assets/logo_dark_160.png";
-import { AutoComplete, Button } from "@douyinfe/semi-ui";
-import { IconSearch, IconSun, IconMoon } from "@douyinfe/semi-icons";
-import { Link } from "react-router-dom";
-import { shortcuts } from "../data/shortcuts";
-
-export default function Shortcuts() {
- const [theme, setTheme] = useState("");
- const [value, setValue] = useState("");
- const [filteredResult, setFilteredResult] = useState(
- shortcuts.map((t) => {
- return t.shortcut;
- })
- );
-
- const handleStringSearch = (value) => {
- setFilteredResult(
- shortcuts
- .filter(
- (i) =>
- i.shortcut.toLowerCase().includes(value.toLowerCase()) ||
- i.title.toLowerCase().includes(value.toLowerCase())
- )
- .map((i) => i.shortcut)
- );
- };
-
- useEffect(() => {
- setTheme(localStorage.getItem("theme"));
- document.title = "Shortcuts | drawDB";
- document.body.setAttribute("class", "theme");
- }, [setTheme]);
-
- const changeTheme = () => {
- const body = document.body;
- const t = body.getAttribute("theme-mode");
- if (t === "dark") {
- if (body.hasAttribute("theme-mode")) {
- body.setAttribute("theme-mode", "light");
- setTheme("light");
- }
- } else {
- if (body.hasAttribute("theme-mode")) {
- body.setAttribute("theme-mode", "dark");
- setTheme("dark");
- }
- }
- };
-
- return (
- <>
-
-
-
-
-
-
- Keyboard shortcuts
-
-
-
-
- ) : (
-
- )
- }
- theme="borderless"
- onClick={changeTheme}
- />
-
-
}
- placeholder="Search..."
- data={filteredResult}
- value={value}
- onSearch={(v) => handleStringSearch(v)}
- emptyContent={
-
No shortcuts found
- }
- onChange={(v) => setValue(v)}
- onSelect={() => {}}
- >
-
-
-
-
-
-
}
- placeholder="Search..."
- className="w-[80%]"
- data={filteredResult}
- value={value}
- onSearch={(v) => handleStringSearch(v)}
- emptyContent={
-
No shortcuts found
- }
- onChange={(v) => setValue(v)}
- onSelect={() => {}}
- >
-
-
- {shortcuts.map((s, i) => (
-
-
-
{s.shortcut}
-
{s.title}
-
- {s.description && (
- <>
-
-
{s.description}
- >
- )}
-
- ))}
-
-
-
- © 2024 drawDB - All right reserved.
-
- >
- );
-}
diff --git a/src/utils/arrangeTables.js b/src/utils/arrangeTables.js
new file mode 100644
index 000000000..a509907d9
--- /dev/null
+++ b/src/utils/arrangeTables.js
@@ -0,0 +1,27 @@
+import {
+ tableColorStripHeight,
+ tableFieldHeight,
+ tableHeaderHeight,
+} from "../data/constants";
+
+export function arrangeTables(diagram) {
+ let maxHeight = -1;
+ const tableWidth = 200;
+ const gapX = 54;
+ const gapY = 40;
+ diagram.tables.forEach((table, i) => {
+ if (i < diagram.tables.length / 2) {
+ table.x = i * tableWidth + (i + 1) * gapX;
+ table.y = gapY;
+ const height =
+ table.fields.length * tableFieldHeight +
+ tableHeaderHeight +
+ tableColorStripHeight;
+ maxHeight = Math.max(height, maxHeight);
+ } else {
+ const index = diagram.tables.length - i - 1;
+ table.x = index * tableWidth + (index + 1) * gapX;
+ table.y = maxHeight + 2 * gapY;
+ }
+ });
+}
diff --git a/src/utils/calcPath.js b/src/utils/calcPath.js
index 18ac61583..0a7ccaa35 100644
--- a/src/utils/calcPath.js
+++ b/src/utils/calcPath.js
@@ -1,96 +1,164 @@
-import { tableFieldHeight, tableHeaderHeight } from "../data/constants";
-
export function calcPath(r, tableWidth = 200, zoom = 1) {
const width = tableWidth * zoom;
- let x1 = r.startTable.x;
- let y1 =
- r.startTable.y +
- r.startFieldId * tableFieldHeight +
- tableHeaderHeight +
- tableFieldHeight / 2;
- let x2 = r.endTable.x;
- let y2 =
- r.endTable.y +
- r.endFieldId * tableFieldHeight +
- tableHeaderHeight +
- tableFieldHeight / 2;
-
- let radius = 10 * zoom;
- const midX = (x2 + x1 + width) / 2;
- const endX = x2 + width < x1 ? x2 + width : x2;
+ const offset = 20 * zoom; // 🔥 Segmento recto inicial y final
+
+ const x1 = r.startTable.x;
+ const y1 = r.startTable.y;
+ const x2 = r.endTable.x;
+ const y2 = r.endTable.y;
+
+ const radius = 10 * zoom;
+ const isRecursiveLike = Math.abs(x1 - x2) < 1;
+
+ const useCustomBreakpoints = Array.isArray(r.breakpoints) && r.breakpoints.length === 2;
+ const breakpoints = useCustomBreakpoints ? r.breakpoints : [];
+
+ // 🧠 Determinar de qué lado sale y entra la línea
+ const startIsLeft = x1 + width <= x2;
+ const startIsRight = !startIsLeft;
+
+ const endIsLeft = x2 + width <= x1;
+ const endIsRight = !endIsLeft;
+ // 🔸 Punto de salida desde la tabla
+ const startX = startIsLeft ? x1 + width : x1;
+ const startY = y1;
+ const startOutX = startIsLeft ? startX + offset : startX - offset;
+ const startOutY = startY;
+
+ // 🔸 Punto de entrada hacia la tabla destino
+ const endX = endIsLeft ? x2 + width : x2;
+ const endY = y2;
+ const endInX = endIsLeft ? endX + offset : endX - offset;
+ const endInY = endY;
+
+ // 🔄 🔥 CASO RECURSIVO (misma tabla)
+ if (isRecursiveLike) {
+ const loopOutwardDistance = Math.max(40 * zoom, width / 4);
+ const verticalMidpoint = (y1 + y2) / 2;
+
+ const bp = useCustomBreakpoints ? breakpoints : [
+ { x: x1 + width + loopOutwardDistance, y: y1 },
+ { x: x1 + width + loopOutwardDistance, y: verticalMidpoint }
+ ];
+ if (!useCustomBreakpoints) breakpoints.push(...bp);
+
+ const path = `M ${startX} ${startY} ` +
+ `L ${startOutX} ${startOutY} ` + // Recto inicial
+ `L ${bp[0].x} ${bp[0].y} ` +
+ `L ${bp[1].x} ${bp[1].y} ` +
+ `L ${endInX} ${endInY} ` + // Recto final
+ `L ${endX} ${endY}`;
+ return { path, breakpoints,
+ startAttach: { x: startOutX, y: startOutY },
+ endAttach: { x: endInX, y: endInY }
+ };
+ }
+
+ // ➕ 🔥 Si están muy cerca en Y (horizontalmente alineadas), línea simple
if (Math.abs(y1 - y2) <= 36 * zoom) {
- radius = Math.abs(y2 - y1) / 3;
- if (radius <= 2) {
- if (x1 + width <= x2) return `M ${x1 + width} ${y1} L ${x2} ${y2 + 0.1}`;
- else if (x2 + width < x1)
- return `M ${x1} ${y1} L ${x2 + width} ${y2 + 0.1}`;
+ const rTemp = Math.abs(y2 - y1) / 3;
+ const rFinal = rTemp > 2 ? radius : rTemp;
+
+ if (rFinal <= 2) {
+ if (startIsLeft) {
+ const path = `M ${startX} ${startY} L ${startOutX} ${startOutY} L ${endInX} ${endInY} L ${endX} ${endY}`;
+ return { path, breakpoints,
+ startAttach: { x: startOutX, y: startOutY },
+ endAttach: { x: endInX, y: endInY }
+ };
+ } else if (endIsLeft) {
+ const path = `M ${startX} ${startY} L ${startOutX} ${startOutY} L ${endInX} ${endInY} L ${endX} ${endY}`;
+ return { path, breakpoints,
+ startAttach: { x: startOutX, y: startOutY },
+ endAttach: { x: endInX, y: endInY }
+ };
+ }
}
}
+ const midX = (startOutX + endInX) / 2;
+
+ // 🔽 Si y1 <= y2 (de arriba hacia abajo)
if (y1 <= y2) {
- if (x1 + width <= x2) {
- return `M ${x1 + width} ${y1} L ${
- midX - radius
- } ${y1} A ${radius} ${radius} 0 0 1 ${midX} ${y1 + radius} L ${midX} ${
- y2 - radius
- } A ${radius} ${radius} 0 0 0 ${midX + radius} ${y2} L ${endX} ${y2}`;
- } else if (x2 <= x1 + width && x1 <= x2) {
- return `M ${x1 + width} ${y1} L ${
- x2 + width
- } ${y1} A ${radius} ${radius} 0 0 1 ${x2 + width + radius} ${
- y1 + radius
- } L ${x2 + width + radius} ${y2 - radius} A ${radius} ${radius} 0 0 1 ${
- x2 + width
- } ${y2} L ${x2 + width} ${y2}`;
- } else if (x2 + width >= x1 && x2 + width <= x1 + width) {
- return `M ${x1} ${y1} L ${
- x2 - radius
- } ${y1} A ${radius} ${radius} 0 0 0 ${x2 - radius - radius} ${
- y1 + radius
- } L ${x2 - radius - radius} ${y2 - radius} A ${radius} ${radius} 0 0 0 ${
- x2 - radius
- } ${y2} L ${x2} ${y2}`;
- } else {
- return `M ${x1} ${y1} L ${
- midX + radius
- } ${y1} A ${radius} ${radius} 0 0 0 ${midX} ${y1 + radius} L ${midX} ${
- y2 - radius
- } A ${radius} ${radius} 0 0 1 ${midX - radius} ${y2} L ${endX} ${y2}`;
+ let bp = useCustomBreakpoints ? breakpoints : [];
+
+ if (!useCustomBreakpoints) {
+ if (startIsLeft) {
+ bp = [
+ { x: midX, y: y1 + radius },
+ { x: midX, y: y2 - radius }
+ ];
+ } else if (x2 <= x1 + width && x1 <= x2) {
+ bp = [
+ { x: x1 + width + radius * 2, y: y1 + radius },
+ { x: x1 + width + radius * 2, y: y2 - radius }
+ ];
+ } else if (x2 + width >= x1 && x2 + width <= x1 + width) {
+ bp = [
+ { x: x1 - radius * 2, y: y1 + radius },
+ { x: x1 - radius * 2, y: y2 - radius }
+ ];
+ } else {
+ bp = [
+ { x: midX, y: y1 + radius },
+ { x: midX, y: y2 - radius }
+ ];
+ }
+ breakpoints.push(...bp);
}
- } else {
- if (x1 + width <= x2) {
- return `M ${x1 + width} ${y1} L ${
- midX - radius
- } ${y1} A ${radius} ${radius} 0 0 0 ${midX} ${y1 - radius} L ${midX} ${
- y2 + radius
- } A ${radius} ${radius} 0 0 1 ${midX + radius} ${y2} L ${endX} ${y2}`;
- } else if (x1 + width >= x2 && x1 + width <= x2 + width) {
- return `M ${x1} ${y1} L ${
- x1 - radius - radius
- } ${y1} A ${radius} ${radius} 0 0 1 ${x1 - radius - radius - radius} ${
- y1 - radius
- } L ${x1 - radius - radius - radius} ${
- y2 + radius
- } A ${radius} ${radius} 0 0 1 ${
- x1 - radius - radius
- } ${y2} L ${endX} ${y2}`;
- } else if (x1 >= x2 && x1 <= x2 + width) {
- return `M ${x1 + width} ${y1} L ${
- x1 + width + radius
- } ${y1} A ${radius} ${radius} 0 0 0 ${x1 + width + radius + radius} ${
- y1 - radius
- } L ${x1 + width + radius + radius} ${
- y2 + radius
- } A ${radius} ${radius} 0 0 0 ${x1 + width + radius} ${y2} L ${
- x2 + width
- } ${y2}`;
- } else {
- return `M ${x1} ${y1} L ${
- midX + radius
- } ${y1} A ${radius} ${radius} 0 0 1 ${midX} ${y1 - radius} L ${midX} ${
- y2 + radius
- } A ${radius} ${radius} 0 0 0 ${midX - radius} ${y2} L ${endX} ${y2}`;
+
+ const path = `M ${startX} ${startY} ` +
+ `L ${startOutX} ${startOutY} ` +
+ `L ${bp[0].x} ${bp[0].y} ` +
+ `L ${bp[1].x} ${bp[1].y} ` +
+ `L ${endInX} ${endInY} ` +
+ `L ${endX} ${endY}`;
+ return { path, breakpoints,
+ startAttach: { x: startOutX, y: startOutY },
+ endAttach: { x: endInX, y: endInY }
+ };
+ }
+
+ // 🔼 Si y1 > y2 (de abajo hacia arriba)
+ else {
+ let bp = useCustomBreakpoints ? breakpoints : [];
+
+ if (!useCustomBreakpoints) {
+ if (startIsLeft) {
+ bp = [
+ { x: midX, y: y1 - radius },
+ { x: midX, y: y2 + radius }
+ ];
+ } else if (x1 + width >= x2 && x1 + width <= x2 + width) {
+ bp = [
+ { x: x1 - radius * 3, y: y1 - radius },
+ { x: x1 - radius * 3, y: y2 + radius }
+ ];
+ } else if (x1 >= x2 && x1 <= x2 + width) {
+ bp = [
+ { x: x1 + width + radius * 2, y: y1 - radius },
+ { x: x1 + width + radius * 2, y: y2 + radius }
+ ];
+ } else {
+ bp = [
+ { x: midX, y: y1 - radius },
+ { x: midX, y: y2 + radius }
+ ];
+ }
+ breakpoints.push(...bp);
}
+
+ const path = `M ${startX} ${startY} ` +
+ `L ${startOutX} ${startOutY} ` +
+ `L ${bp[0].x} ${bp[0].y} ` +
+ `L ${bp[1].x} ${bp[1].y} ` +
+ `L ${endInX} ${endInY} ` +
+ `L ${endX} ${endY}`;
+ return { path, breakpoints,
+ startAttach: { x: startOutX, y: startOutY },
+ endAttach: { x: endInX, y: endInY }
+ };
}
}
+
diff --git a/src/utils/exportAs/dbml.js b/src/utils/exportAs/dbml.js
new file mode 100644
index 000000000..0add6c614
--- /dev/null
+++ b/src/utils/exportAs/dbml.js
@@ -0,0 +1,101 @@
+import { RelationshipType } from "../../data/constants";
+import { parseDefault } from "../exportSQL/shared";
+
+function hasColumnSettings(field) {
+ return (
+ field.primary ||
+ field.notNull ||
+ field.increment ||
+ field.unique ||
+ (field.comment && field.comment.trim() != "") ||
+ (field.default && field.default.trim() != "")
+ );
+}
+
+function columnDefault(field, database) {
+ if (!field.default || field.default.trim() === "") {
+ return "";
+ }
+
+ return `default: ${parseDefault(field, database)}`;
+}
+
+function columnComment(field) {
+ if (!field.comment || field.comment.trim() === "") {
+ return "";
+ }
+
+ return `note: '${field.comment}'`;
+}
+
+function columnSettings(field, database) {
+ if (!hasColumnSettings(field)) {
+ return "";
+ }
+
+ return ` [ ${field.primary ? "pk " : ""}${
+ field.increment ? "increment " : ""
+ }${field.notNull ? "not null " : ""}${
+ field.unique ? "unique " : ""
+ }${columnDefault(field, database)}${columnComment(field, database)}]`;
+}
+
+function relationshipCardinality(rel) {
+ if (rel.relationshipType === RelationshipType.ONE_TO_ONE) {
+ return "-";
+ }
+ if (rel.relationshipType === RelationshipType.ONE_TO_MANY) {
+ return "<";
+ }
+ return "-";
+}
+
+export function toDBML(diagram) {
+ return `${diagram.enums
+ .map(
+ (en) =>
+ `enum ${en.name} {\n${en.values.map((v) => `\t${v}`).join("\n")}\n}\n\n`,
+ )
+ .join("\n\n")}${diagram.tables
+ .map(
+ (table) =>
+ `Table ${table.name} {\n${table.fields
+ .map(
+ (field) =>
+ `\t${field.name} ${field.type.toLowerCase()}${columnSettings(
+ field,
+ diagram.database,
+ )}`,
+ )
+ .join("\n")}${
+ table.indices.length > 0
+ ? "\n\n\tindexes {\n" +
+ table.indices
+ .map(
+ (index) =>
+ `\t\t(${index.fields.join(", ")}) [ name: '${
+ index.name
+ }'${index.unique ? " unique" : ""} ]`,
+ )
+ .join("\n") +
+ "\n\t}"
+ : ""
+ }${
+ table.comment && table.comment.trim() !== ""
+ ? `\n\n\tNote: '${table.comment}'`
+ : ""
+ }\n}`,
+ )
+ .join("\n\n")}\n\n${diagram.relationships
+ .map(
+ (rel) =>
+ `Ref ${rel.name} {\n\t${
+ diagram.tables[rel.startTableId].name
+ }.${diagram.tables[rel.startTableId].fields[rel.startFieldId].name} ${relationshipCardinality(
+ rel,
+ )} ${diagram.tables[rel.endTableId].name}.${
+ diagram.tables[rel.endTableId].fields[rel.endFieldId].name
+ } [ delete: ${rel.deleteConstraint.toLowerCase()}, on update: ${rel.updateConstraint.toLowerCase()} ]\n}`,
+ )
+ .join("\n\n")}`;
+}
diff --git a/src/utils/exportAs/mermaid.js b/src/utils/exportAs/mermaid.js
index f820c728c..b63427dc5 100644
--- a/src/utils/exportAs/mermaid.js
+++ b/src/utils/exportAs/mermaid.js
@@ -1,22 +1,15 @@
-import { Cardinality } from "../../data/constants";
+import { RelationshipType } from "../../data/constants";
import { dbToTypes } from "../../data/datatypes";
-import i18n from "../../i18n/i18n";
export function jsonToMermaid(obj) {
- function getMermaidRelationship(relationship) {
- switch (relationship) {
- case i18n.t(Cardinality.ONE_TO_ONE):
- case Cardinality.ONE_TO_ONE:
- return "||--||";
- case i18n.t(Cardinality.MANY_TO_ONE_TO_ONE):
- case Cardinality.MANY_TO_ONE:
- return "}o--||";
- case i18n.t(Cardinality.ONE_TO_MANY):
- case Cardinality.ONE_TO_MANY:
- return "||--o{";
- default:
- return "--";
+ function getMermaidRelationship(rel) {
+ if (rel.relationshipType === RelationshipType.ONE_TO_ONE) {
+ return "||--||";
}
+ if (rel.relationshipType === RelationshipType.ONE_TO_MANY) {
+ return "||--o{";
+ }
+ return "--";
}
const mermaidEntities = obj.tables
@@ -43,7 +36,7 @@ export function jsonToMermaid(obj) {
.map((r) => {
const startTable = obj.tables[r.startTableId].name;
const endTable = obj.tables[r.endTableId].name;
- return `\t${startTable} ${getMermaidRelationship(r.cardinality)} ${endTable} : references`;
+ return `\t${startTable} ${getMermaidRelationship(r)} ${endTable} : references`;
})
.join("\n")
: "";
diff --git a/src/utils/importFrom/dbml.js b/src/utils/importFrom/dbml.js
new file mode 100644
index 000000000..141585a7e
--- /dev/null
+++ b/src/utils/importFrom/dbml.js
@@ -0,0 +1,127 @@
+import { Parser } from "@dbml/core";
+import { arrangeTables } from "../arrangeTables";
+import { RelationshipType, RelationshipCardinalities, Constraint } from "../../data/constants";
+
+const parser = new Parser();
+
+export function fromDBML(src) {
+ const ast = parser.parse(src, "dbml");
+
+ const tables = [];
+ const enums = [];
+ const relationships = [];
+
+ for (const schema of ast.schemas) {
+ for (const table of schema.tables) {
+ let parsedTable = {};
+ parsedTable.id = tables.length;
+ parsedTable.name = table.name;
+ parsedTable.comment = table.note ?? "";
+ parsedTable.color = "#175e7a";
+ parsedTable.fields = [];
+ parsedTable.indices = [];
+
+ for (const column of table.fields) {
+ const field = {};
+
+ field.id = parsedTable.fields.length;
+ field.name = column.name;
+ field.type = column.type.type_name.toUpperCase();
+ field.default = column.dbdefault ?? "";
+ field.check = "";
+ field.primary = !!column.pk;
+ field.unique = !!column.pk;
+ field.notNull = !!column.not_null;
+ field.increment = !!column.increment;
+ field.comment = column.note ?? "";
+
+ parsedTable.fields.push(field);
+ }
+
+ for (const idx of table.indexes) {
+ const parsedIndex = {};
+
+ parsedIndex.id = idx.id - 1;
+ parsedIndex.fields = idx.columns.map((x) => x.value);
+ parsedIndex.name =
+ idx.name ?? `${parsedTable.name}_index_${parsedIndex.id}`;
+ parsedIndex.unique = !!idx.unique;
+
+ parsedTable.indices.push(parsedIndex);
+ }
+
+ tables.push(parsedTable);
+ }
+
+ for (const ref of schema.refs) {
+ const startTable = ref.endpoints[0].tableName;
+ const endTable = ref.endpoints[1].tableName;
+ const startField = ref.endpoints[0].fieldNames[0];
+ const endField = ref.endpoints[1].fieldNames[0];
+
+ const startTableId = tables.findIndex((t) => t.name === startTable);
+ if (startTableId === -1) continue;
+
+ const endTableId = tables.findIndex((t) => t.name === endTable);
+ if (endTableId === -1) continue;
+
+ const endFieldId = tables[endTableId].fields.findIndex(
+ (f) => f.name === endField,
+ );
+ if (endFieldId === -1) continue;
+
+ const startFieldId = tables[startTableId].fields.findIndex(
+ (f) => f.name === startField,
+ );
+ if (startFieldId === -1) continue;
+
+ const relationship = {};
+
+ relationship.name =
+ "fk_" + startTable + "_" + startField + "_" + endTable;
+ relationship.startTableId = startTableId;
+ relationship.endTableId = endTableId;
+ relationship.endFieldId = endFieldId;
+ relationship.startFieldId = startFieldId;
+ relationship.id = relationships.length;
+
+ relationship.updateConstraint = ref.onDelete
+ ? ref.onDelete[0].toUpperCase() + ref.onDelete.substring(1)
+ : Constraint.NONE;
+ relationship.deleteConstraint = ref.onUpdate
+ ? ref.onUpdate[0].toUpperCase() + ref.onUpdate.substring(1)
+ : Constraint.NONE;
+
+ const startRelation = ref.endpoints[0].relation;
+ const endRelation = ref.endpoints[1].relation;
+
+ relationship.relationshipType = RelationshipType.ONE_TO_ONE;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_ONE][0].label;
+
+ if ((startRelation === "1" && endRelation === "*") || (startRelation === "*" && endRelation === "1")) {
+ relationship.relationshipType = RelationshipType.ONE_TO_MANY;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_MANY][0].label;
+ }
+ if (startRelation === "1" && endRelation === "1") {
+ relationship.relationshipType = RelationshipType.ONE_TO_ONE;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_ONE][0].label;
+ }
+ relationships.push(relationship);
+ }
+
+ for (const schemaEnum of schema.enums) {
+ const parsedEnum = {};
+
+ parsedEnum.name = schemaEnum.name;
+ parsedEnum.values = schemaEnum.values.map((x) => x.name);
+
+ enums.push(parsedEnum);
+ }
+ }
+
+ const diagram = { tables, enums, relationships };
+
+ arrangeTables(diagram);
+
+ return diagram;
+}
diff --git a/src/utils/importSQL/index.js b/src/utils/importSQL/index.js
index 30974ec6e..fb849e430 100644
--- a/src/utils/importSQL/index.js
+++ b/src/utils/importSQL/index.js
@@ -1,9 +1,5 @@
-import {
- DB,
- tableColorStripHeight,
- tableFieldHeight,
- tableHeaderHeight,
-} from "../../data/constants";
+import { DB } from "../../data/constants";
+import { arrangeTables } from "../arrangeTables";
import { fromMariaDB } from "./mariadb";
import { fromMSSQL } from "./mssql";
import { fromMySQL } from "./mysql";
@@ -33,25 +29,7 @@ export function importSQL(ast, toDb = DB.MYSQL, diagramDb = DB.GENERIC) {
break;
}
- let maxHeight = -1;
- const tableWidth = 200;
- const gapX = 54;
- const gapY = 40;
- diagram.tables.forEach((table, i) => {
- if (i < diagram.tables.length / 2) {
- table.x = i * tableWidth + (i + 1) * gapX;
- table.y = gapY;
- const height =
- table.fields.length * tableFieldHeight +
- tableHeaderHeight +
- tableColorStripHeight;
- maxHeight = Math.max(height, maxHeight);
- } else {
- const index = diagram.tables.length - i - 1;
- table.x = index * tableWidth + (index + 1) * gapX;
- table.y = maxHeight + 2 * gapY;
- }
- });
+ arrangeTables(diagram);
return diagram;
}
diff --git a/src/utils/importSQL/mariadb.js b/src/utils/importSQL/mariadb.js
index bdc28362a..89073948b 100644
--- a/src/utils/importSQL/mariadb.js
+++ b/src/utils/importSQL/mariadb.js
@@ -1,4 +1,4 @@
-import { Cardinality, DB } from "../../data/constants";
+import { RelationshipType, RelationshipCardinalities, DB } from "../../data/constants";
import { dbToTypes } from "../../data/datatypes";
import { buildSQLFromAST } from "./shared";
@@ -152,9 +152,11 @@ export function fromMariaDB(ast, diagramDb = DB.GENERIC) {
relationship.deleteConstraint = deleteConstraint;
if (table.fields[startFieldId].unique) {
- relationship.cardinality = Cardinality.ONE_TO_ONE;
+ relationship.relationshipType = RelationshipType.ONE_TO_ONE;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_ONE][0].label;
} else {
- relationship.cardinality = Cardinality.MANY_TO_ONE;
+ relationship.relationshipType = RelationshipType.ONE_TO_MANY;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_MANY][0].label;
}
relationships.push(relationship);
@@ -242,9 +244,11 @@ export function fromMariaDB(ast, diagramDb = DB.GENERIC) {
relationship.deleteConstraint = deleteConstraint;
if (tables[startTableId].fields[startFieldId].unique) {
- relationship.cardinality = Cardinality.ONE_TO_ONE;
+ relationship.relationshipType = RelationshipType.ONE_TO_ONE;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_ONE][0].label;
} else {
- relationship.cardinality = Cardinality.MANY_TO_ONE;
+ relationship.relationshipType = RelationshipType.ONE_TO_MANY;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_MANY][0].label;
}
relationships.push(relationship);
diff --git a/src/utils/importSQL/mssql.js b/src/utils/importSQL/mssql.js
index 5f61e0fd1..5c8e844c5 100644
--- a/src/utils/importSQL/mssql.js
+++ b/src/utils/importSQL/mssql.js
@@ -1,4 +1,4 @@
-import { Cardinality, DB } from "../../data/constants";
+import { RelationshipType, RelationshipCardinalities, DB } from "../../data/constants";
import { dbToTypes } from "../../data/datatypes";
import { buildSQLFromAST } from "./shared";
@@ -164,9 +164,11 @@ export function fromMSSQL(ast, diagramDb = DB.GENERIC) {
relationship.deleteConstraint = deleteConstraint;
if (table.fields[startFieldId].unique) {
- relationship.cardinality = Cardinality.ONE_TO_ONE;
+ relationship.relationshipType = RelationshipType.ONE_TO_ONE;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_ONE][0].label;
} else {
- relationship.cardinality = Cardinality.MANY_TO_ONE;
+ relationship.relationshipType = RelationshipType.ONE_TO_MANY;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_MANY][0].label;
}
relationships.push(relationship);
@@ -254,9 +256,11 @@ export function fromMSSQL(ast, diagramDb = DB.GENERIC) {
relationship.deleteConstraint = deleteConstraint;
if (tables[startTableId].fields[startFieldId].unique) {
- relationship.cardinality = Cardinality.ONE_TO_ONE;
+ relationship.relationshipType = RelationshipType.ONE_TO_ONE;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_ONE][0].label;
} else {
- relationship.cardinality = Cardinality.MANY_TO_ONE;
+ relationship.relationshipType = RelationshipType.ONE_TO_MANY;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_MANY][0].label;
}
relationships.push(relationship);
diff --git a/src/utils/importSQL/mysql.js b/src/utils/importSQL/mysql.js
index aac8fa0a5..e1d4414e3 100644
--- a/src/utils/importSQL/mysql.js
+++ b/src/utils/importSQL/mysql.js
@@ -1,4 +1,4 @@
-import { Cardinality, DB } from "../../data/constants";
+import { RelationshipType, RelationshipCardinalities, DB } from "../../data/constants";
import { dbToTypes } from "../../data/datatypes";
import { buildSQLFromAST } from "./shared";
@@ -152,9 +152,11 @@ export function fromMySQL(ast, diagramDb = DB.GENERIC) {
relationship.deleteConstraint = deleteConstraint;
if (table.fields[startFieldId].unique) {
- relationship.cardinality = Cardinality.ONE_TO_ONE;
+ relationship.relationshipType = RelationshipType.ONE_TO_ONE;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_ONE][0].label;
} else {
- relationship.cardinality = Cardinality.MANY_TO_ONE;
+ relationship.relationshipType = RelationshipType.ONE_TO_MANY;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_MANY][0].label;
}
relationships.push(relationship);
@@ -242,9 +244,11 @@ export function fromMySQL(ast, diagramDb = DB.GENERIC) {
relationship.deleteConstraint = deleteConstraint;
if (tables[startTableId].fields[startFieldId].unique) {
- relationship.cardinality = Cardinality.ONE_TO_ONE;
+ relationship.relationshipType = RelationshipType.ONE_TO_ONE;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_ONE][0].label;
} else {
- relationship.cardinality = Cardinality.MANY_TO_ONE;
+ relationship.relationshipType = RelationshipType.ONE_TO_MANY;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_MANY][0].label;
}
relationships.push(relationship);
diff --git a/src/utils/importSQL/oracle.js b/src/utils/importSQL/oracle.js
index 8c5c72a65..29df9e70a 100644
--- a/src/utils/importSQL/oracle.js
+++ b/src/utils/importSQL/oracle.js
@@ -1,6 +1,4 @@
-// Import SQL files from Oracle database
-
-import { Cardinality, DB } from "../../data/constants";
+import { RelationshipType, RelationshipCardinalities, DB } from "../../data/constants";
import { dbToTypes } from "../../data/datatypes";
import { buildSQLFromAST } from "./shared";
@@ -153,9 +151,11 @@ export function fromOracle(ast, diagramDb = DB.GENERIC) {
relationship.deleteConstraint = deleteConstraint;
if (table.fields[startFieldId].unique) {
- relationship.cardinality = Cardinality.ONE_TO_ONE;
+ relationship.relationshipType = RelationshipType.ONE_TO_ONE;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_ONE][0].label;
} else {
- relationship.cardinality = Cardinality.MANY_TO_ONE;
+ relationship.relationshipType = RelationshipType.ONE_TO_MANY;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_MANY][0].label;
}
relationships.push(relationship);
@@ -241,9 +241,11 @@ export function fromOracle(ast, diagramDb = DB.GENERIC) {
relationship.deleteConstraint = deleteConstraint;
if (tables[startTableId].fields[startFieldId].unique) {
- relationship.cardinality = Cardinality.ONE_TO_ONE;
+ relationship.relationshipType = RelationshipType.ONE_TO_ONE;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_ONE][0].label;
} else {
- relationship.cardinality = Cardinality.MANY_TO_ONE;
+ relationship.relationshipType = RelationshipType.ONE_TO_MANY;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_MANY][0].label;
}
relationships.push(relationship);
diff --git a/src/utils/importSQL/postgres.js b/src/utils/importSQL/postgres.js
index 7ac90526a..8fa3a40e7 100644
--- a/src/utils/importSQL/postgres.js
+++ b/src/utils/importSQL/postgres.js
@@ -1,4 +1,4 @@
-import { Cardinality, DB } from "../../data/constants";
+import { RelationshipType, RelationshipCardinalities, DB } from "../../data/constants";
import { dbToTypes } from "../../data/datatypes";
import { buildSQLFromAST } from "./shared";
@@ -153,11 +153,13 @@ export function fromPostgres(ast, diagramDb = DB.GENERIC) {
relationship.updateConstraint = updateConstraint;
relationship.deleteConstraint = deleteConstraint;
- if (table.fields[startFieldId].unique) {
- relationship.cardinality = Cardinality.ONE_TO_ONE;
- } else {
- relationship.cardinality = Cardinality.MANY_TO_ONE;
- }
+ relationship.relationshipType = tables[startTableId].fields[startFieldId].unique
+ ? RelationshipType.ONE_TO_ONE
+ : RelationshipType.ONE_TO_MANY;
+
+ relationship.cardinality = tables[startTableId].fields[startFieldId].unique
+ ? RelationshipCardinalities[RelationshipType.ONE_TO_ONE][0].label // "(0,1)" o "(1,1)"
+ : RelationshipCardinalities[RelationshipType.ONE_TO_MANY][0].label; // "(1,*)"
relationships.push(relationship);
}
}
@@ -208,12 +210,13 @@ export function fromPostgres(ast, diagramDb = DB.GENERIC) {
relationship.endFieldId = endFieldId;
relationship.updateConstraint = updateConstraint;
relationship.deleteConstraint = deleteConstraint;
+ relationship.relationshipType = tables[startTableId].fields[startFieldId].unique
+ ? RelationshipType.ONE_TO_ONE
+ : RelationshipType.ONE_TO_MANY;
- if (table.fields[startFieldId].unique) {
- relationship.cardinality = Cardinality.ONE_TO_ONE;
- } else {
- relationship.cardinality = Cardinality.MANY_TO_ONE;
- }
+ relationship.cardinality = tables[startTableId].fields[startFieldId].unique
+ ? RelationshipCardinalities[RelationshipType.ONE_TO_ONE][0].label
+ : RelationshipCardinalities[RelationshipType.ONE_TO_MANY][0].label;
relationships.push(relationship);
@@ -336,13 +339,13 @@ export function fromPostgres(ast, diagramDb = DB.GENERIC) {
relationship.endFieldId = endFieldId;
relationship.updateConstraint = updateConstraint;
relationship.deleteConstraint = deleteConstraint;
- relationship.cardinality = Cardinality.ONE_TO_ONE;
+ relationship.relationshipType = tables[startTableId].fields[startFieldId].unique
+ ? RelationshipType.ONE_TO_ONE
+ : RelationshipType.ONE_TO_MANY;
- if (tables[startTableId].fields[startFieldId].unique) {
- relationship.cardinality = Cardinality.ONE_TO_ONE;
- } else {
- relationship.cardinality = Cardinality.MANY_TO_ONE;
- }
+ relationship.cardinality = tables[startTableId].fields[startFieldId].unique
+ ? RelationshipCardinalities[RelationshipType.ONE_TO_ONE][0].label
+ : RelationshipCardinalities[RelationshipType.ONE_TO_MANY][0].label;
relationships.push(relationship);
diff --git a/src/utils/importSQL/sqlite.js b/src/utils/importSQL/sqlite.js
index 00a0e4e41..38767ffd1 100644
--- a/src/utils/importSQL/sqlite.js
+++ b/src/utils/importSQL/sqlite.js
@@ -1,4 +1,4 @@
-import { Cardinality, DB } from "../../data/constants";
+import { RelationshipType, RelationshipCardinalities, DB } from "../../data/constants";
import { dbToTypes } from "../../data/datatypes";
import { buildSQLFromAST } from "./shared";
@@ -86,9 +86,11 @@ export function fromSQLite(ast, diagramDb = DB.GENERIC) {
relationship.deleteConstraint = deleteConstraint;
if (startTable.fields[startFieldId].unique) {
- relationship.cardinality = Cardinality.ONE_TO_ONE;
+ relationship.relationshipType = RelationshipType.ONE_TO_ONE;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_ONE][0].label;
} else {
- relationship.cardinality = Cardinality.MANY_TO_ONE;
+ relationship.relationshipType = RelationshipType.ONE_TO_MANY;
+ relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_MANY][0].label;
}
relationships.push(relationship);
};
diff --git a/src/utils/utils.js b/src/utils/utils.js
index cf66fcde8..c541c1fe4 100644
--- a/src/utils/utils.js
+++ b/src/utils/utils.js
@@ -38,14 +38,17 @@ export function isFunction(str) {
export function areFieldsCompatible( parentfields, tarjetTable) {
for (let i = 0; i < parentfields.length; i++) {
- const field1 = parentfields[i];
+ const parentField = parentfields[i];
- if (!field1 || !field1.type) return false;
-
- const field2 = tarjetTable.fields.find((f) => f.name === field1.name);
-
- if (!field2) return true;
+ if(!parentField || !parentField.type) return false;
+ const existingFieldInTarjet = tarjetTable.fields.find(
+ (field) =>
+ field.name.toUpperCase() === parentField.name.toUpperCase()
+ );
+ if (existingFieldInTarjet) {
+ return false;
+ }
}
- return false;
+ return true;
}