From 94226de56139c54fdb1d4026a70005a99b42de4c Mon Sep 17 00:00:00 2001 From: 1ilit <1ilit@proton.me> Date: Sun, 11 May 2025 21:44:04 +0400 Subject: [PATCH] Table and field drag and drop ordering (#444) * Add dnd for tables and fields * Fix inputs * Decouple ids and indecies in the editor * Decouple ids and indecies in utils * Fix field indexes * Use nanoid instead of numberic ids for fields and tables * Fix review comments --- package-lock.json | 76 ++++++--- package.json | 4 + src/components/EditorCanvas/Canvas.jsx | 64 ++++---- src/components/EditorCanvas/Relationship.jsx | 33 ++-- src/components/EditorCanvas/Table.jsx | 19 ++- src/components/EditorHeader/ControlPanel.jsx | 152 ++++++------------ .../EditorHeader/Modal/ImportDiagram.jsx | 14 +- src/components/EditorHeader/Modal/Modal.jsx | 10 +- src/components/EditorHeader/Modal/Share.jsx | 2 +- .../EditorSidePanel/EnumsTab/EnumDetails.jsx | 8 +- .../RelationshipsTab/RelationshipInfo.jsx | 73 ++++----- .../TablesTab/FieldDetails.jsx | 63 ++++---- .../TablesTab/IndexDetails.jsx | 19 +-- .../EditorSidePanel/TablesTab/SearchBar.jsx | 2 +- .../EditorSidePanel/TablesTab/TableField.jsx | 70 ++++---- .../EditorSidePanel/TablesTab/TableInfo.jsx | 124 +++----------- .../EditorSidePanel/TablesTab/TablesTab.jsx | 68 +++++--- .../EditorSidePanel/TypesTab/TypeInfo.jsx | 8 +- src/components/SimpleCanvas.jsx | 3 +- src/components/SortableList/DragHandle.jsx | 14 ++ src/components/SortableList/SortableItem.jsx | 18 +++ src/components/SortableList/SortableList.jsx | 54 +++++++ src/components/Thumbnail.jsx | 20 --- src/components/Workspace.jsx | 6 +- src/context/DiagramContext.jsx | 104 +++++------- src/utils/calcPath.js | 21 ++- src/utils/exportAs/dbml.js | 27 ++-- src/utils/exportAs/documentation.js | 6 +- src/utils/exportAs/mermaid.js | 6 +- src/utils/exportSQL/generic.js | 114 ++++++++----- src/utils/exportSQL/mariadb.js | 24 +-- src/utils/exportSQL/mssql.js | 22 ++- src/utils/exportSQL/mysql.js | 24 +-- src/utils/exportSQL/oraclesql.js | 22 +-- src/utils/exportSQL/postgres.js | 23 ++- src/utils/exportSQL/shared.js | 8 +- src/utils/importFrom/dbml.js | 38 +++-- src/utils/importSQL/mariadb.js | 111 ++++++------- src/utils/importSQL/mssql.js | 112 ++++++------- src/utils/importSQL/mysql.js | 114 +++++++------ src/utils/importSQL/oraclesql.js | 39 ++--- src/utils/importSQL/postgres.js | 144 ++++++++--------- src/utils/importSQL/sqlite.js | 60 +++---- src/utils/issues.js | 2 +- 44 files changed, 990 insertions(+), 955 deletions(-) create mode 100644 src/components/SortableList/DragHandle.jsx create mode 100644 src/components/SortableList/SortableItem.jsx create mode 100644 src/components/SortableList/SortableList.jsx diff --git a/package-lock.json b/package-lock.json index 47fd597..8a0403b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "0.0.0", "dependencies": { "@dbml/core": "^3.9.7-alpha.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@douyinfe/semi-ui": "^2.77.1", "@lexical/react": "^0.12.5", "@monaco-editor/react": "^4.7.0", @@ -27,6 +30,7 @@ "jspdf": "^3.0.1", "jszip": "^3.10.1", "lexical": "^0.12.5", + "nanoid": "^5.1.5", "node-sql-parser": "^5.3.9", "oracle-sql-parser": "^0.1.0", "react": "^18.2.0", @@ -402,9 +406,10 @@ } }, "node_modules/@dnd-kit/accessibility": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz", - "integrity": "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -413,11 +418,12 @@ } }, "node_modules/@dnd-kit/core": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.1.0.tgz", - "integrity": "sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", "dependencies": { - "@dnd-kit/accessibility": "^3.1.0", + "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, @@ -427,15 +433,16 @@ } }, "node_modules/@dnd-kit/sortable": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz", - "integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", "dependencies": { - "@dnd-kit/utilities": "^3.2.0", + "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { - "@dnd-kit/core": "^6.0.7", + "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, @@ -443,6 +450,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -568,6 +576,20 @@ "react-dom": ">=16.0.0" } }, + "node_modules/@douyinfe/semi-ui/node_modules/@dnd-kit/sortable": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz", + "integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.0", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.0.7", + "react": ">=16.8.0" + } + }, "node_modules/@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", @@ -6214,10 +6236,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", + "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", "funding": [ { "type": "github", @@ -6226,10 +6247,10 @@ ], "license": "MIT", "bin": { - "nanoid": "bin/nanoid.cjs" + "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^18 || >=20" } }, "node_modules/natural-compare": { @@ -6571,6 +6592,25 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/package.json b/package.json index 9f1f49c..efadb31 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,9 @@ }, "dependencies": { "@dbml/core": "^3.9.7-alpha.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@douyinfe/semi-ui": "^2.77.1", "@lexical/react": "^0.12.5", "@monaco-editor/react": "^4.7.0", @@ -29,6 +32,7 @@ "jspdf": "^3.0.1", "jszip": "^3.10.1", "lexical": "^0.12.5", + "nanoid": "^5.1.5", "node-sql-parser": "^5.3.9", "oracle-sql-parser": "^0.1.0", "react": "^18.2.0", diff --git a/src/components/EditorCanvas/Canvas.jsx b/src/components/EditorCanvas/Canvas.jsx index b57263c..57e1c4c 100644 --- a/src/components/EditorCanvas/Canvas.jsx +++ b/src/components/EditorCanvas/Canvas.jsx @@ -55,7 +55,7 @@ export default function Canvas() { } = useSelect(); const [dragging, setDragging] = useState({ element: ObjectType.NONE, - id: -1, + id: null, prevX: 0, prevY: 0, initialPositions: [], @@ -73,8 +73,8 @@ export default function Canvas() { }); const [grabOffset, setGrabOffset] = useState({ x: 0, y: 0 }); const [hoveredTable, setHoveredTable] = useState({ - tableId: -1, - field: -2, + tableId: null, + fieldId: null, }); const [panning, setPanning] = useState({ isPanning: false, @@ -167,7 +167,7 @@ export default function Canvas() { const getElement = (element) => { switch (element.type) { case ObjectType.TABLE: - return tables[element.id]; + return tables.find((t) => t.id === element.id); case ObjectType.AREA: return areas[element.id]; case ObjectType.NOTE: @@ -264,7 +264,7 @@ export default function Canvas() { }); } else if ( dragging.element !== ObjectType.NONE && - dragging.id >= 0 && + dragging.id !== null && bulkSelectedElements.length ) { const currentX = pointer.spaces.diagram.x + grabOffset.x; @@ -274,9 +274,10 @@ export default function Canvas() { for (const element of bulkSelectedElements) { if (element.type === ObjectType.TABLE) { + const { x, y } = tables.find((e) => e.id === element.id); updateTable(element.id, { - x: tables[element.id].x + deltaX, - y: tables[element.id].y + deltaY, + x: x + deltaX, + y: y + deltaY, }); } if (element.type === ObjectType.AREA) { @@ -317,21 +318,21 @@ export default function Canvas() { (panning.cursorStart.y - pointer.spaces.screen.y) / transform.zoom, }, })); - } else if (dragging.element === ObjectType.TABLE && dragging.id >= 0) { + } else if (dragging.element === ObjectType.TABLE && dragging.id !== null) { updateTable(dragging.id, { x: pointer.spaces.diagram.x + grabOffset.x, y: pointer.spaces.diagram.y + grabOffset.y, }); } else if ( dragging.element === ObjectType.AREA && - dragging.id >= 0 && + dragging.id !== null && areaResize.id === -1 ) { updateArea(dragging.id, { x: pointer.spaces.diagram.x + grabOffset.x, y: pointer.spaces.diagram.y + grabOffset.y, }); - } else if (dragging.element === ObjectType.NOTE && dragging.id >= 0) { + } else if (dragging.element === ObjectType.NOTE && dragging.id !== null) { updateNote(dragging.id, { x: pointer.spaces.diagram.x + grabOffset.x, y: pointer.spaces.diagram.y + grabOffset.y, @@ -441,7 +442,10 @@ export default function Canvas() { }; const didPan = () => - !(transform.pan.x === panning.panStart.x && transform.pan.y === panning.panStart.y); + !( + transform.pan.x === panning.panStart.x && + transform.pan.y === panning.panStart.y + ); /** * @param {PointerEvent} e @@ -503,7 +507,7 @@ export default function Canvas() { } setDragging({ element: ObjectType.NONE, - id: -1, + id: null, prevX: 0, prevY: 0, initialPositions: [], @@ -585,7 +589,7 @@ export default function Canvas() { setPanning((old) => ({ ...old, isPanning: false })); setDragging({ element: ObjectType.NONE, - id: -1, + id: null, prevX: 0, prevY: 0, initialPositions: [], @@ -594,34 +598,40 @@ export default function Canvas() { }; const handleLinking = () => { - if (hoveredTable.tableId < 0) return; - if (hoveredTable.field < 0) return; - if ( - !areFieldsCompatible( - database, - tables[linkingLine.startTableId].fields[linkingLine.startFieldId], - tables[hoveredTable.tableId].fields[hoveredTable.field], - ) - ) { + if (hoveredTable.tableId === null) return; + if (hoveredTable.fieldId === null) return; + + const { fields: startTableFields, name: startTableName } = tables.find( + (t) => t.id === linkingLine.startTableId, + ); + const { type: startType, name: startFieldName } = startTableFields.find( + (f) => f.id === linkingLine.startFieldId, + ); + const { fields: endTableFields, name: endTableName } = tables.find( + (t) => t.id === hoveredTable.tableId, + ); + const { type: endType } = endTableFields.find( + (f) => f.id === hoveredTable.fieldId, + ); + + if (!areFieldsCompatible(database, startType, endType)) { Toast.info(t("cannot_connect")); return; } if ( linkingLine.startTableId === hoveredTable.tableId && - linkingLine.startFieldId === hoveredTable.field + linkingLine.startFieldId === hoveredTable.fieldId ) return; const newRelationship = { ...linkingLine, endTableId: hoveredTable.tableId, - endFieldId: hoveredTable.field, + endFieldId: hoveredTable.fieldId, cardinality: Cardinality.ONE_TO_ONE, updateConstraint: Constraint.NONE, deleteConstraint: Constraint.NONE, - name: `fk_${tables[linkingLine.startTableId].name}_${ - tables[linkingLine.startTableId].fields[linkingLine.startFieldId].name - }_${tables[hoveredTable.tableId].name}`, + name: `fk_${startTableName}_${startFieldName}_${endTableName}`, id: relationships.length, }; delete newRelationship.startX; diff --git a/src/components/EditorCanvas/Relationship.jsx b/src/components/EditorCanvas/Relationship.jsx index fd29354..cca5753 100644 --- a/src/components/EditorCanvas/Relationship.jsx +++ b/src/components/EditorCanvas/Relationship.jsx @@ -1,4 +1,4 @@ -import { useRef } from "react"; +import { useMemo, useRef } from "react"; import { Cardinality, darkBgTheme, @@ -20,6 +20,22 @@ export default function Relationship({ data }) { const { selectedElement, setSelectedElement } = useSelect(); const { t } = useTranslation(); + const pathValues = useMemo(() => { + const startTable = tables.find((t) => t.id === data.startTableId); + const endTable = tables.find((t) => t.id === data.endTableId); + + if (!startTable || !endTable) return null; + + return { + startFieldIndex: startTable.fields.findIndex( + (f) => f.id === data.startFieldId, + ), + endFieldIndex: endTable.fields.findIndex((f) => f.id === data.endFieldId), + startTable: { x: startTable.x, y: startTable.y }, + endTable: { x: endTable.x, y: endTable.y }, + }; + }, [tables, data]); + const theme = localStorage.getItem("theme"); const pathRef = useRef(); @@ -106,20 +122,7 @@ export default function Relationship({ data }) { { return ( - (selectedElement.id === tableData.id && + (selectedElement.id == tableData.id && selectedElement.element === ObjectType.TABLE) || bulkSelectedElements.some( (e) => e.type === ObjectType.TABLE && e.id === tableData.id, @@ -124,7 +125,7 @@ export default function Table(props) { onClick={openEditor} />
@@ -304,13 +305,17 @@ export default function Table(props) { setHoveredField(index); setHoveredTable({ tableId: tableData.id, - field: index, + fieldId: fieldData.id, }); }} onPointerLeave={(e) => { if (!e.isPrimary) return; - setHoveredField(-1); + setHoveredField(null); + setHoveredTable({ + tableId: null, + fieldId: null, + }); }} onPointerDown={(e) => { // Required for onPointerLeave to trigger when a touch pointer leaves @@ -328,10 +333,10 @@ export default function Table(props) { onPointerDown={(e) => { if (!e.isPrimary) return; - handleGripField(index); + handleGripField(); setLinkingLine((prev) => ({ ...prev, - startFieldId: index, + startFieldId: fieldData.id, startTableId: tableData.id, startX: tableData.x + 15, startY: diff --git a/src/components/EditorHeader/ControlPanel.jsx b/src/components/EditorHeader/ControlPanel.jsx index 6c9f8a9..e485806 100644 --- a/src/components/EditorHeader/ControlPanel.jsx +++ b/src/components/EditorHeader/ControlPanel.jsx @@ -78,6 +78,7 @@ import { IdContext } from "../Workspace"; import { socials } from "../../data/socials"; import { toDBML } from "../../utils/exportAs/dbml"; import { exportSavedData } from "../../utils/exportSavedData"; +import { nanoid } from "nanoid"; export default function ControlPanel({ diagramId, @@ -144,13 +145,12 @@ export default function ControlPanel({ } } setRedoStack((prev) => [...prev, a]); - console.log(a); return; } if (a.action === Action.ADD) { if (a.element === ObjectType.TABLE) { - deleteTable(tables[tables.length - 1].id, false); + deleteTable(a.id, false); } else if (a.element === ObjectType.AREA) { deleteArea(areas[areas.length - 1].id, false); } else if (a.element === ObjectType.NOTE) { @@ -165,10 +165,8 @@ export default function ControlPanel({ setRedoStack((prev) => [...prev, a]); } else if (a.action === Action.MOVE) { if (a.element === ObjectType.TABLE) { - setRedoStack((prev) => [ - ...prev, - { ...a, x: tables[a.id].x, y: tables[a.id].y }, - ]); + const { x, y } = tables.find((t) => t.id === a.id); + setRedoStack((prev) => [...prev, { ...a, x, y }]); updateTable(a.id, { x: a.x, y: a.y }); } else if (a.element === ObjectType.AREA) { setRedoStack((prev) => [ @@ -205,6 +203,7 @@ export default function ControlPanel({ } else if (a.element === ObjectType.NOTE) { updateNote(a.nid, a.undo); } else if (a.element === ObjectType.TABLE) { + const table = tables.find((t) => t.id === a.tid); if (a.component === "field") { updateField(a.tid, a.fid, a.undo); } else if (a.component === "field_delete") { @@ -213,65 +212,24 @@ export default function ControlPanel({ a.data.relationship.forEach((r) => { temp.splice(r.id, 0, r); }); - temp = temp.map((e, i) => { - const recoveredRel = a.data.relationship.find( - (x) => - (x.startTableId === e.startTableId && - x.startFieldId === e.startFieldId) || - (x.endTableId === e.endTableId && - x.endFieldId === a.endFieldId), - ); - if ( - e.startTableId === a.tid && - e.startFieldId >= a.data.field.id && - !recoveredRel - ) { - return { - ...e, - id: i, - startFieldId: e.startFieldId + 1, - }; - } - if ( - e.endTableId === a.tid && - e.endFieldId >= a.data.field.id && - !recoveredRel - ) { - return { - ...e, - id: i, - endFieldId: e.endFieldId + 1, - }; - } - return { ...e, id: i }; - }); return temp; }); - setTables((prev) => - prev.map((t) => { - if (t.id === a.tid) { - const temp = t.fields.slice(); - temp.splice(a.data.field.id, 0, a.data.field); - return { ...t, fields: temp.map((t, i) => ({ ...t, id: i })) }; - } - return t; - }), - ); + const updatedFields = table.fields.slice(); + updatedFields.splice(a.data.index, 0, a.data.field); + updateTable(a.tid, { fields: updatedFields }); } 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 })), + fields: table.fields.filter((e) => e.id !== a.fid), }); } else if (a.component === "index_add") { updateTable(a.tid, { - indices: tables[a.tid].indices - .filter((e) => e.id !== tables[a.tid].indices.length - 1) + indices: table.indices + .filter((e) => e.id !== table.indices.length - 1) .map((t, i) => ({ ...t, id: i })), }); } else if (a.component === "index") { updateTable(a.tid, { - indices: tables[a.tid].indices.map((index) => + indices: table.indices.map((index) => index.id === a.iid ? { ...index, @@ -281,19 +239,11 @@ export default function ControlPanel({ ), }); } 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; - }), - ); + const updatedIndices = table.indices.slice(); + updatedIndices.splice(a.data.id, 0, a.data); + updateTable(a.tid, { + indices: updatedIndices.map((t, i) => ({ ...t, id: i })), + }); } else if (a.component === "self") { updateTable(a.tid, a.undo); } @@ -390,10 +340,8 @@ export default function ControlPanel({ setUndoStack((prev) => [...prev, a]); } 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 }, - ]); + const { x, y } = tables.find((t) => t.id == a.id); + setUndoStack((prev) => [...prev, { ...a, x, y }]); updateTable(a.id, { x: a.x, y: a.y }); } else if (a.element === ObjectType.AREA) { setUndoStack((prev) => [ @@ -429,6 +377,7 @@ export default function ControlPanel({ } else if (a.element === ObjectType.NOTE) { updateNote(a.nid, a.redo); } else if (a.element === ObjectType.TABLE) { + const table = tables.find((t) => t.id === a.tid); if (a.component === "field") { updateField(a.tid, a.fid, a.redo); } else if (a.component === "field_delete") { @@ -436,7 +385,7 @@ export default function ControlPanel({ } else if (a.component === "field_add") { updateTable(a.tid, { fields: [ - ...tables[a.tid].fields, + ...table.fields, { name: "", type: "", @@ -447,32 +396,24 @@ export default function ControlPanel({ notNull: false, increment: false, comment: "", - id: tables[a.tid].fields.length, + id: nanoid(), }, ], }); } 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; - }), - ); + updateTable(a.tid, { + indices: [ + ...table.indices, + { + id: table.indices.length, + name: `index_${table.indices.length}`, + fields: [], + }, + ], + }); } else if (a.component === "index") { updateTable(a.tid, { - indices: tables[a.tid].indices.map((index) => + indices: table.indices.map((index) => index.id === a.iid ? { ...index, @@ -483,7 +424,7 @@ export default function ControlPanel({ }); } else if (a.component === "index_delete") { updateTable(a.tid, { - indices: tables[a.tid].indices + indices: table.indices .filter((e) => e.id !== a.data.id) .map((t, i) => ({ ...t, id: i })), }); @@ -662,14 +603,16 @@ export default function ControlPanel({ }; const duplicate = () => { switch (selectedElement.element) { - case ObjectType.TABLE: + case ObjectType.TABLE: { + const copiedTable = tables.find((t) => t.id === selectedElement.id); addTable({ - ...tables[selectedElement.id], - x: tables[selectedElement.id].x + 20, - y: tables[selectedElement.id].y + 20, + ...copiedTable, + x: copiedTable.x + 20, + y: copiedTable.y + 20, id: tables.length, }); break; + } case ObjectType.NOTE: addNote({ ...notes[selectedElement.id], @@ -694,7 +637,9 @@ export default function ControlPanel({ switch (selectedElement.element) { case ObjectType.TABLE: navigator.clipboard - .writeText(JSON.stringify({ ...tables[selectedElement.id] })) + .writeText( + JSON.stringify(tables.find((t) => t.id === selectedElement.id)), + ) .catch(() => Toast.error(t("oops_smth_went_wrong"))); break; case ObjectType.NOTE: @@ -1201,17 +1146,18 @@ export default function ControlPanel({ setRedoStack([]); if (!diagramId) { - console.error("Something went wrong."); + Toast.error(t("oops_smth_went_wrong")); return; } db.table("diagrams") .delete(diagramId) - .then(() => { - console.info('Deleted diagram successfully.') - }) .catch((error) => { - console.error(`Error deleting records with gistId '${diagramId}':`, error); + Toast.error(t("oops_smth_went_wrong")); + console.error( + `Error deleting records with gistId '${diagramId}':`, + error, + ); }); }, }, diff --git a/src/components/EditorHeader/Modal/ImportDiagram.jsx b/src/components/EditorHeader/Modal/ImportDiagram.jsx index b6d8387..3d744c4 100644 --- a/src/components/EditorHeader/Modal/ImportDiagram.jsx +++ b/src/components/EditorHeader/Modal/ImportDiagram.jsx @@ -83,10 +83,12 @@ export default function ImportDiagram({ let ok = true; jsonObject.relationships.forEach((rel) => { - if ( - !jsonObject.tables[rel.startTableId] || - !jsonObject.tables[rel.endTableId] - ) { + const startTable = jsonObject.tables.find( + (t) => t.id === rel.startTableId, + ); + const endTable = jsonObject.tables.find((t) => t.id === rel.endTableId); + + if (!startTable || !endTable) { setError({ type: STATUS.ERROR, message: `Relationship ${rel.name} references a table that does not exist.`, @@ -96,8 +98,8 @@ export default function ImportDiagram({ } if ( - !jsonObject.tables[rel.startTableId].fields[rel.startFieldId] || - !jsonObject.tables[rel.endTableId].fields[rel.endFieldId] + !startTable.fields.find((f) => f.id === rel.startFieldId) || + !endTable.fields.find((f) => f.id === rel.endFieldId) ) { setError({ type: STATUS.ERROR, diff --git a/src/components/EditorHeader/Modal/Modal.jsx b/src/components/EditorHeader/Modal/Modal.jsx index 812b07b..e6fc3b3 100644 --- a/src/components/EditorHeader/Modal/Modal.jsx +++ b/src/components/EditorHeader/Modal/Modal.jsx @@ -59,8 +59,7 @@ export default function Modal({ importFrom, }) { const { t, i18n } = useTranslation(); - const { tables, setTables, setRelationships, database, setDatabase } = - useDiagram(); + const { setTables, setRelationships, database, setDatabase } = useDiagram(); const { setNotes } = useNotes(); const { setAreas } = useAreas(); const { setTypes } = useTypes(); @@ -182,15 +181,10 @@ export default function Modal({ setUndoStack([]); setRedoStack([]); } else { - const initialTablesLength = tables.length; - setTables((prev) => - [...prev, ...diagramData.tables].map((t, i) => ({ ...t, id: i })), - ); + setTables((prev) => [...prev, ...diagramData.tables]); setRelationships((prev) => [...prev, ...diagramData.relationships].map((r, i) => ({ ...r, - startTableId: initialTablesLength + r.startTableId, - endTableId: initialTablesLength + r.endTableId, id: i, })), ); diff --git a/src/components/EditorHeader/Modal/Share.jsx b/src/components/EditorHeader/Modal/Share.jsx index 4ec4c78..87f843c 100644 --- a/src/components/EditorHeader/Modal/Share.jsx +++ b/src/components/EditorHeader/Modal/Share.jsx @@ -31,6 +31,7 @@ export default function Share({ title, setModal }) { const diagramToString = useCallback(() => { return JSON.stringify({ + title, tables: tables, relationships: relationships, notes: notes, @@ -38,7 +39,6 @@ export default function Share({ title, setModal }) { database: database, ...(databases[database].hasTypes && { types: types }), ...(databases[database].hasEnums && { enums: enums }), - title: title, transform: transform, }); }, [ diff --git a/src/components/EditorSidePanel/EnumsTab/EnumDetails.jsx b/src/components/EditorSidePanel/EnumsTab/EnumDetails.jsx index b77bb96..c84e18e 100644 --- a/src/components/EditorSidePanel/EnumsTab/EnumDetails.jsx +++ b/src/components/EditorSidePanel/EnumsTab/EnumDetails.jsx @@ -22,10 +22,12 @@ export default function EnumDetails({ data, i }) { validateStatus={data.name.trim() === "" ? "error" : "default"} onChange={(value) => { updateEnum(i, { name: value }); - tables.forEach((table, i) => { - table.fields.forEach((field, j) => { + tables.forEach((table) => { + table.fields.forEach((field) => { if (field.type.toLowerCase() === data.name.toLowerCase()) { - updateField(i, j, { type: value.toUpperCase() }); + updateField(table.id, field.id, { + type: value.toUpperCase(), + }); } }); }); diff --git a/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx b/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx index 3cfc451..be15be1 100644 --- a/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx +++ b/src/components/EditorSidePanel/RelationshipsTab/RelationshipInfo.jsx @@ -21,7 +21,7 @@ import { import { useDiagram, useUndoRedo } from "../../../hooks"; import i18n from "../../../i18n/i18n"; import { useTranslation } from "react-i18next"; -import { useState } from "react"; +import { useMemo, useState } from "react"; const columns = [ { @@ -36,11 +36,31 @@ const columns = [ export default function RelationshipInfo({ data }) { const { setUndoStack, setRedoStack } = useUndoRedo(); - const { tables, setRelationships, deleteRelationship, updateRelationship } = - useDiagram(); + const { tables, deleteRelationship, updateRelationship } = useDiagram(); const { t } = useTranslation(); const [editField, setEditField] = useState({}); + const relValues = useMemo(() => { + const { fields: startTableFields, name: startTableName } = tables.find( + (t) => t.id === data.startTableId, + ); + const { name: startFieldName } = startTableFields.find( + (f) => f.id === data.startFieldId, + ); + const { fields: endTableFields, name: endTableName } = tables.find( + (t) => t.id === data.endTableId, + ); + const { name: endFieldName } = endTableFields.find( + (f) => f.id === data.endFieldId, + ); + return { + startTableName, + startFieldName, + endTableName, + endFieldName, + }; + }, [tables, data]); + const swapKeys = () => { setUndoStack((prev) => [ ...prev, @@ -67,22 +87,14 @@ export default function RelationshipInfo({ data }) { }, ]); setRedoStack([]); - setRelationships((prev) => - prev.map((e, idx) => - idx === data.id - ? { - ...e, - name: `fk_${tables[e.endTableId].name}_${ - tables[e.endTableId].fields[e.endFieldId].name - }_${tables[e.startTableId].name}`, - startTableId: e.endTableId, - startFieldId: e.endFieldId, - endTableId: e.startTableId, - endFieldId: e.startFieldId, - } - : e, - ), - ); + + updateRelationship(data.id, { + name: `fk_${relValues.endTableName}_${relValues.endFieldName}_${relValues.startTableName}`, + startTableId: data.endTableId, + startFieldId: data.endFieldId, + endTableId: data.startTableId, + endFieldId: data.startFieldId, + }); }; const changeCardinality = (value) => { @@ -101,11 +113,7 @@ export default function RelationshipInfo({ data }) { }, ]); setRedoStack([]); - setRelationships((prev) => - prev.map((e, idx) => - idx === data.id ? { ...e, cardinality: value } : e, - ), - ); + updateRelationship(data.id, { cardinality: value }); }; const changeConstraint = (key, value) => { @@ -125,9 +133,7 @@ export default function RelationshipInfo({ data }) { }, ]); setRedoStack([]); - setRelationships((prev) => - prev.map((e, idx) => (idx === data.id ? { ...e, [undoKey]: value } : e)), - ); + updateRelationship(data.id, { [undoKey]: value }); }; return ( @@ -165,11 +171,11 @@ export default function RelationshipInfo({ data }) {
{t("primary")}: - {tables[data.endTableId].name} + {relValues.endTableName}
{t("foreign")}: - {tables[data.startTableId].name} + {relValues.startTableName}
tables.find((t) => t.id === tid), [tables, tid]); return (
@@ -29,7 +30,7 @@ export default function FieldDetails({ data, tid, index }) { placeholder={t("default_value")} value={data.default} disabled={dbToTypes[database][data.type].noDefault || data.increment} - onChange={(value) => updateField(tid, index, { default: value })} + onChange={(value) => updateField(tid, data.id, { default: value })} onFocus={(e) => setEditField({ default: e.target.value })} onBlur={(e) => { if (e.target.value === editField.default) return; @@ -40,11 +41,11 @@ export default function FieldDetails({ data, tid, index }) { element: ObjectType.TABLE, component: "field", tid: tid, - fid: index, + fid: data.id, undo: editField, redo: { default: e.target.value }, message: t("edit_table", { - tableName: tables[tid].name, + tableName: table.name, extra: "[field]", }), }, @@ -66,7 +67,7 @@ export default function FieldDetails({ data, tid, index }) { addOnBlur className="my-2" placeholder={t("use_for_batch_input")} - onChange={(v) => updateField(tid, index, { values: v })} + onChange={(v) => updateField(tid, data.id, { values: v })} onFocus={() => setEditField({ values: data.values })} onBlur={() => { if ( @@ -80,11 +81,11 @@ export default function FieldDetails({ data, tid, index }) { element: ObjectType.TABLE, component: "field", tid: tid, - fid: index, + fid: data.id, undo: editField, redo: { values: data.values }, message: t("edit_table", { - tableName: tables[tid].name, + tableName: table.name, extra: "[field]", }), }, @@ -101,7 +102,7 @@ export default function FieldDetails({ data, tid, index }) { className="my-2 w-full" placeholder={t("size")} value={data.size} - onChange={(value) => updateField(tid, index, { size: value })} + onChange={(value) => updateField(tid, data.id, { size: value })} onFocus={(e) => setEditField({ size: e.target.value })} onBlur={(e) => { if (e.target.value === editField.size) return; @@ -112,11 +113,11 @@ export default function FieldDetails({ data, tid, index }) { element: ObjectType.TABLE, component: "field", tid: tid, - fid: index, + fid: data.id, undo: editField, redo: { size: e.target.value }, message: t("edit_table", { - tableName: tables[tid].name, + tableName: table.name, extra: "[field]", }), }, @@ -138,7 +139,7 @@ export default function FieldDetails({ data, tid, index }) { : "error" } value={data.size} - onChange={(value) => updateField(tid, index, { size: value })} + onChange={(value) => updateField(tid, data.id, { size: value })} onFocus={(e) => setEditField({ size: e.target.value })} onBlur={(e) => { if (e.target.value === editField.size) return; @@ -149,11 +150,11 @@ export default function FieldDetails({ data, tid, index }) { element: ObjectType.TABLE, component: "field", tid: tid, - fid: index, + fid: data.id, undo: editField, redo: { size: e.target.value }, message: t("edit_table", { - tableName: tables[tid].name, + tableName: table.name, extra: "[field]", }), }, @@ -171,7 +172,7 @@ export default function FieldDetails({ data, tid, index }) { placeholder={t("check")} value={data.check} disabled={data.increment} - onChange={(value) => updateField(tid, index, { check: value })} + onChange={(value) => updateField(tid, data.id, { check: value })} onFocus={(e) => setEditField({ check: e.target.value })} onBlur={(e) => { if (e.target.value === editField.check) return; @@ -182,11 +183,11 @@ export default function FieldDetails({ data, tid, index }) { element: ObjectType.TABLE, component: "field", tid: tid, - fid: index, + fid: data.id, undo: editField, redo: { check: e.target.value }, message: t("edit_table", { - tableName: tables[tid].name, + tableName: table.name, extra: "[field]", }), }, @@ -210,7 +211,7 @@ export default function FieldDetails({ data, tid, index }) { element: ObjectType.TABLE, component: "field", tid: tid, - fid: index, + fid: data.id, undo: { [checkedValues.target.value]: !checkedValues.target.checked, }, @@ -220,7 +221,7 @@ export default function FieldDetails({ data, tid, index }) { }, ]); setRedoStack([]); - updateField(tid, index, { + updateField(tid, data.id, { [checkedValues.target.value]: checkedValues.target.checked, }); }} @@ -242,7 +243,7 @@ export default function FieldDetails({ data, tid, index }) { element: ObjectType.TABLE, component: "field", tid: tid, - fid: index, + fid: data.id, undo: { [checkedValues.target.value]: !checkedValues.target.checked, }, @@ -250,13 +251,13 @@ export default function FieldDetails({ data, tid, index }) { [checkedValues.target.value]: checkedValues.target.checked, }, message: t("edit_table", { - tableName: tables[tid].name, + tableName: table.name, extra: "[field]", }), }, ]); setRedoStack([]); - updateField(tid, index, { + updateField(tid, data.id, { increment: !data.increment, check: data.increment ? data.check : "", }); @@ -277,7 +278,7 @@ export default function FieldDetails({ data, tid, index }) { element: ObjectType.TABLE, component: "field", tid: tid, - fid: index, + fid: data.id, undo: { [checkedValues.target.value]: !checkedValues.target.checked, }, @@ -285,13 +286,13 @@ export default function FieldDetails({ data, tid, index }) { [checkedValues.target.value]: checkedValues.target.checked, }, message: t("edit_table", { - tableName: tables[tid].name, + tableName: table.name, extra: "[field]", }), }, ]); setRedoStack([]); - updateField(tid, index, { + updateField(tid, data.id, { isArray: checkedValues.target.checked, increment: data.isArray ? data.increment : false, }); @@ -314,7 +315,7 @@ export default function FieldDetails({ data, tid, index }) { element: ObjectType.TABLE, component: "field", tid: tid, - fid: index, + fid: data.id, undo: { [checkedValues.target.value]: !checkedValues.target.checked, @@ -324,13 +325,13 @@ export default function FieldDetails({ data, tid, index }) { checkedValues.target.checked, }, message: t("edit_table", { - tableName: tables[tid].name, + tableName: table.name, extra: "[field]", }), }, ]); setRedoStack([]); - updateField(tid, index, { + updateField(tid, data.id, { unsigned: checkedValues.target.checked, }); }} @@ -344,7 +345,7 @@ export default function FieldDetails({ data, tid, index }) { value={data.comment} autosize rows={2} - onChange={(value) => updateField(tid, index, { comment: value })} + onChange={(value) => updateField(tid, data.id, { comment: value })} onFocus={(e) => setEditField({ comment: e.target.value })} onBlur={(e) => { if (e.target.value === editField.comment) return; @@ -355,11 +356,11 @@ export default function FieldDetails({ data, tid, index }) { element: ObjectType.TABLE, component: "field", tid: tid, - fid: index, + fid: data.id, undo: editField, redo: { comment: e.target.value }, message: t("edit_table", { - tableName: tables[tid].name, + tableName: table.name, extra: "[field]", }), }, diff --git a/src/components/EditorSidePanel/TablesTab/IndexDetails.jsx b/src/components/EditorSidePanel/TablesTab/IndexDetails.jsx index 3bc4dfb..3774d86 100644 --- a/src/components/EditorSidePanel/TablesTab/IndexDetails.jsx +++ b/src/components/EditorSidePanel/TablesTab/IndexDetails.jsx @@ -3,13 +3,14 @@ import { Input, Button, Popover, Checkbox, Select } from "@douyinfe/semi-ui"; import { IconMore, IconDeleteStroked } from "@douyinfe/semi-icons"; import { useDiagram, useUndoRedo } from "../../../hooks"; import { useTranslation } from "react-i18next"; -import { useState } from "react"; +import { useMemo, useState } from "react"; export default function IndexDetails({ data, fields, iid, tid }) { const { t } = useTranslation(); const { tables, updateTable } = useDiagram(); const { setUndoStack, setRedoStack } = useUndoRedo(); const [editField, setEditField] = useState({}); + const table = useMemo(() => tables.find((t) => t.id === tid), [tables, tid]); return (
@@ -36,14 +37,14 @@ export default function IndexDetails({ data, fields, iid, tid }) { fields: [...value], }, message: t("edit_table", { - tableName: tables[tid].name, + tableName: table.name, extra: "[index field]", }), }, ]); setRedoStack([]); updateTable(tid, { - indices: tables[tid].indices.map((index) => + indices: table.indices.map((index) => index.id === iid ? { ...index, @@ -69,7 +70,7 @@ export default function IndexDetails({ data, fields, iid, tid }) { } onChange={(value) => updateTable(tid, { - indices: tables[tid].indices.map((index) => + indices: table.indices.map((index) => index.id === iid ? { ...index, @@ -92,7 +93,7 @@ export default function IndexDetails({ data, fields, iid, tid }) { undo: editField, redo: { name: e.target.value }, message: t("edit_table", { - tableName: tables[tid].name, + tableName: table.name, extra: "[index]", }), }, @@ -123,14 +124,14 @@ export default function IndexDetails({ data, fields, iid, tid }) { checkedValues.target.checked, }, message: t("edit_table", { - tableName: tables[tid].name, + tableName: table.name, extra: "[index field]", }), }, ]); setRedoStack([]); updateTable(tid, { - indices: tables[tid].indices.map((index) => + indices: table.indices.map((index) => index.id === iid ? { ...index, @@ -157,14 +158,14 @@ export default function IndexDetails({ data, fields, iid, tid }) { tid: tid, data: data, message: t("edit_table", { - tableName: tables[tid].name, + tableName: table.name, extra: "[delete index]", }), }, ]); setRedoStack([]); updateTable(tid, { - indices: tables[tid].indices + indices: table.indices .filter((e) => e.id !== iid) .map((e, j) => ({ ...e, diff --git a/src/components/EditorSidePanel/TablesTab/SearchBar.jsx b/src/components/EditorSidePanel/TablesTab/SearchBar.jsx index bcaec05..95e6a9b 100644 --- a/src/components/EditorSidePanel/TablesTab/SearchBar.jsx +++ b/src/components/EditorSidePanel/TablesTab/SearchBar.jsx @@ -11,7 +11,7 @@ export default function SearchBar({ tables }) { const treeData = useMemo(() => { return tables.map(({ id, name: parentName, fields }, i) => { - const children = fields.map(({ name }, j) => ({ + const children = fields?.map(({ name }, j) => ({ tableId: id, id: `${j}`, label: name, diff --git a/src/components/EditorSidePanel/TablesTab/TableField.jsx b/src/components/EditorSidePanel/TablesTab/TableField.jsx index 3dc2421..7c744f4 100644 --- a/src/components/EditorSidePanel/TablesTab/TableField.jsx +++ b/src/components/EditorSidePanel/TablesTab/TableField.jsx @@ -1,11 +1,12 @@ +import { useMemo, useState } from "react"; import { Action, ObjectType } from "../../../data/constants"; -import { Row, Col, Input, Button, Popover, Select } from "@douyinfe/semi-ui"; +import { Input, Button, Popover, Select } from "@douyinfe/semi-ui"; import { IconMore, IconKeyStroked } from "@douyinfe/semi-icons"; import { useEnums, useDiagram, useTypes, useUndoRedo } from "../../../hooks"; -import { useState } from "react"; -import FieldDetails from "./FieldDetails"; import { useTranslation } from "react-i18next"; import { dbToTypes } from "../../../data/datatypes"; +import { DragHandle } from "../../SortableList/DragHandle"; +import FieldDetails from "./FieldDetails"; export default function TableField({ data, tid, index }) { const { updateField } = useDiagram(); @@ -15,16 +16,18 @@ export default function TableField({ data, tid, index }) { const { t } = useTranslation(); const { setUndoStack, setRedoStack } = useUndoRedo(); const [editField, setEditField] = useState({}); + const table = useMemo(() => tables.find((t) => t.id === tid), [tables, tid]); return ( - - +
+ +
updateField(tid, index, { name: value })} + onChange={(value) => updateField(tid, data.id, { name: value })} onFocus={(e) => setEditField({ name: e.target.value })} onBlur={(e) => { if (e.target.value === editField.name) return; @@ -35,11 +38,11 @@ export default function TableField({ data, tid, index }) { element: ObjectType.TABLE, component: "field", tid: tid, - fid: index, + fid: data.id, undo: editField, redo: { name: e.target.value }, message: t("edit_table", { - tableName: tables[tid].name, + tableName: table.name, extra: "[field]", }), }, @@ -47,8 +50,8 @@ export default function TableField({ data, tid, index }) { setRedoStack([]); }} /> - - +
+