diff --git a/src/components/EditorCanvas/Canvas.jsx b/src/components/EditorCanvas/Canvas.jsx index d4ceecb..f2b71a5 100644 --- a/src/components/EditorCanvas/Canvas.jsx +++ b/src/components/EditorCanvas/Canvas.jsx @@ -1,864 +1,1759 @@ -import { useRef, useState } from "react"; -import { - Action, - Cardinality, - Constraint, - darkBgTheme, - ObjectType, - gridSize, - gridCircleRadius, - minAreaSize, -} from "../../data/constants"; -import { Toast } from "@douyinfe/semi-ui"; -import Table from "./Table"; -import Area from "./Area"; -import Relationship from "./Relationship"; -import Note from "./Note"; -import { - useCanvas, - useSettings, - useTransform, - useDiagram, - useUndoRedo, - useSelect, - useAreas, - useNotes, - useLayout, - useSaveState, -} from "../../hooks"; -import { useTranslation } from "react-i18next"; -import { useEventListener } from "usehooks-ts"; -import { areFieldsCompatible, getTableHeight } from "../../utils/utils"; -import { getRectFromEndpoints, isInsideRect } from "../../utils/rect"; -import { State, noteWidth } from "../../data/constants"; -import { nanoid } from "nanoid"; - -export default function Canvas() { - const { t } = useTranslation(); - - const canvasRef = useRef(null); - const canvasContextValue = useCanvas(); - const { - canvas: { viewBox }, - pointer, - } = canvasContextValue; - - const { tables, updateTable, relationships, addRelationship, database } = - useDiagram(); - const { setSaveState } = useSaveState(); - const { areas, updateArea } = useAreas(); - const { notes, updateNote } = useNotes(); - const { layout } = useLayout(); - const { settings } = useSettings(); - const { setUndoStack, setRedoStack } = useUndoRedo(); - const { transform, setTransform } = useTransform(); - const { - selectedElement, - setSelectedElement, - bulkSelectedElements, - setBulkSelectedElements, - } = useSelect(); - const notDragging = { - id: -1, - type: ObjectType.NONE, - grabOffset: { x: 0, y: 0 }, - }; - const [dragging, setDragging] = useState(notDragging); - const [linking, setLinking] = useState(false); - const [linkingLine, setLinkingLine] = useState({ - startTableId: -1, - startFieldId: -1, - endTableId: -1, - endFieldId: -1, - startX: 0, - startY: 0, - endX: 0, - endY: 0, - }); - const [hoveredTable, setHoveredTable] = useState({ - tableId: null, - fieldId: null, - }); - const [panning, setPanning] = useState({ - isPanning: false, - panStart: { x: 0, y: 0 }, - cursorStart: { x: 0, y: 0 }, - }); - const [areaResize, setAreaResize] = useState({ id: -1, dir: "none" }); - const [areaInitDimensions, setAreaInitDimensions] = useState({ - x: 0, - y: 0, - width: 0, - height: 0, - }); - const [bulkSelectRect, setBulkSelectRect] = useState({ - x1: 0, - y1: 0, - x2: 0, - y2: 0, - show: false, - ctrlKey: false, - metaKey: false, - }); - // this is used to store the element that is clicked on - // at the moment, and shouldn't be a part of the state - let elementPointerDown = null; - - const isSameElement = (el1, el2) => { - return el1.id === el2.id && el1.type === el2.type; - }; - - const collectSelectedElements = () => { - const rect = getRectFromEndpoints(bulkSelectRect); - const elements = []; - const shouldAddElement = (elementRect, element) => { - // if ctrl key is pressed, only add the elements that are not already selected - // can theoretically be optimized later if the selected elements is - // a map from id to element (after the ids are made unique) - return ( - isInsideRect(elementRect, rect) && - ((!bulkSelectRect.ctrlKey && !bulkSelectRect.metaKey) || - !bulkSelectedElements.some((el) => isSameElement(el, element))) - ); - }; - - tables.forEach((table) => { - if (table.locked) return; - - const element = { - id: table.id, - type: ObjectType.TABLE, - currentCoords: { x: table.x, y: table.y }, - initialCoords: { x: table.x, y: table.y }, - }; - const tableRect = { - x: table.x, - y: table.y, - width: settings.tableWidth, - height: getTableHeight(table), - }; - if (shouldAddElement(tableRect, element)) { - elements.push(element); - } - }); - - areas.forEach((area) => { - if (area.locked) return; - - const element = { - id: area.id, - type: ObjectType.AREA, - currentCoords: { x: area.x, y: area.y }, - initialCoords: { x: area.x, y: area.y }, - }; - const areaRect = { - x: area.x, - y: area.y, - width: area.width, - height: area.height, - }; - if (shouldAddElement(areaRect, element)) { - elements.push(element); - } - }); - - notes.forEach((note) => { - if (note.locked) return; - - const element = { - id: note.id, - type: ObjectType.NOTE, - currentCoords: { x: note.x, y: note.y }, - initialCoords: { x: note.x, y: note.y }, - }; - const noteRect = { - x: note.x, - y: note.y, - width: noteWidth, - height: note.height, - }; - if (shouldAddElement(noteRect, element)) { - elements.push(element); - } - }); - - if (bulkSelectRect.ctrlKey || bulkSelectRect.metaKey) { - setBulkSelectedElements([...bulkSelectedElements, ...elements]); - } else { - setBulkSelectedElements(elements); - } - }; - - const handlePointerDownOnElement = (e, { element, type }) => { - if (selectedElement.open && !layout.sidebar) return; - - if (!e.isPrimary) return; - - if (!element.locked || !(e.ctrlKey || e.metaKey)) { - setSelectedElement((prev) => ({ - ...prev, - element: type, - id: element.id, - open: false, - })); - } - - if (element.locked) { - if (!(e.ctrlKey || e.metaKey)) { - setBulkSelectedElements([]); - } - return; - } - - setBulkSelectRect((prev) => ({ - ...prev, - show: false, - })); - - // this is the object that will be added to the bulk selected elements - // if necessary - const elementInBulk = { - id: element.id, - type, - currentCoords: { x: element.x, y: element.y }, - initialCoords: { x: element.x, y: element.y }, - }; - - const isSelected = bulkSelectedElements.some((el) => - isSameElement(el, elementInBulk), - ); - - if (e.ctrlKey || e.metaKey) { - if (isSelected) { - if (bulkSelectedElements.length > 1) { - setBulkSelectedElements( - bulkSelectedElements.filter( - (el) => !isSameElement(el, elementInBulk), - ), - ); - setSelectedElement({ - ...selectedElement, - element: ObjectType.NONE, - id: -1, - open: false, - }); - } - } else { - setBulkSelectedElements([...bulkSelectedElements, elementInBulk]); - } - setDragging(notDragging); - return; - } - - if (!isSelected) { - setBulkSelectedElements([elementInBulk]); - } - setDragging({ - id: element.id, - type, - grabOffset: { - x: pointer.spaces.diagram.x - element.x, - y: pointer.spaces.diagram.y - element.y, - }, - }); - }; - - const coordinatesAfterSnappingToGrid = ({ x, y }) => { - if (settings.snapToGrid) { - return { - x: Math.round(x / gridSize) * gridSize, - y: Math.round(y / gridSize) * gridSize, - }; - } - return { x, y }; - }; - - /** - * @param {PointerEvent} e - */ - const handlePointerMove = (e) => { - if (selectedElement.open && !layout.sidebar) return; - - if (!e.isPrimary) return; - - if (panning.isPanning) { - setTransform((prev) => ({ - ...prev, - pan: { - x: - panning.panStart.x + - (panning.cursorStart.x - pointer.spaces.screen.x) / transform.zoom, - y: - panning.panStart.y + - (panning.cursorStart.y - pointer.spaces.screen.y) / transform.zoom, - }, - })); - return; - } - - if (layout.readOnly) return; - - if (linking) { - setLinkingLine({ - ...linkingLine, - endX: pointer.spaces.diagram.x, - endY: pointer.spaces.diagram.y, - }); - return; - } - - if (isDragging()) { - const { x: mainElementFinalX, y: mainElementFinalY } = - coordinatesAfterSnappingToGrid({ - x: pointer.spaces.diagram.x - dragging.grabOffset.x, - y: pointer.spaces.diagram.y - dragging.grabOffset.y, - }); - - const { currentCoords } = bulkSelectedElements.find((el) => - isSameElement(el, dragging), - ); - - const deltaX = mainElementFinalX - currentCoords.x; - const deltaY = mainElementFinalY - currentCoords.y; - - const newBulkSelectedElements = []; - bulkSelectedElements.forEach((el) => { - const elementFinalCoords = { - x: el.currentCoords.x + deltaX, - y: el.currentCoords.y + deltaY, - }; - if (el.type === ObjectType.TABLE) { - updateTable(el.id, { ...elementFinalCoords }); - } - if (el.type === ObjectType.AREA) { - updateArea(el.id, { ...elementFinalCoords }); - } - if (el.type === ObjectType.NOTE) { - updateNote(el.id, { ...elementFinalCoords }); - } - newBulkSelectedElements.push({ - ...el, - currentCoords: elementFinalCoords, - }); - }); - - setBulkSelectedElements(newBulkSelectedElements); - return; - } - - if (areaResize.id !== -1) { - if (areaResize.dir === "none") return; - let newDims = { ...areaInitDimensions }; - setPanning((old) => ({ ...old, isPanning: false })); - const { x, y } = coordinatesAfterSnappingToGrid(pointer.spaces.diagram); - - switch (areaResize.dir) { - case "br": - newDims.width = x - areaInitDimensions.x; - newDims.height = y - areaInitDimensions.y; - break; - case "tl": - newDims.x = x; - newDims.y = y; - newDims.width = areaInitDimensions.width - (x - areaInitDimensions.x); - newDims.height = - areaInitDimensions.height - (y - areaInitDimensions.y); - break; - case "tr": - newDims.y = y; - newDims.width = x - areaInitDimensions.x; - newDims.height = - areaInitDimensions.height - (y - areaInitDimensions.y); - break; - case "bl": - newDims.x = x; - newDims.width = areaInitDimensions.width - (x - areaInitDimensions.x); - newDims.height = y - areaInitDimensions.y; - break; - } - - if (newDims.width <= minAreaSize) { - newDims.width = minAreaSize; - if (areaResize.dir === "tl" || areaResize.dir === "bl") { - newDims.x = - areaInitDimensions.x + areaInitDimensions.width - minAreaSize; - } - } - - if (newDims.height <= minAreaSize) { - newDims.height = minAreaSize; - if (areaResize.dir === "tl" || areaResize.dir === "tr") { - newDims.y = - areaInitDimensions.y + areaInitDimensions.height - minAreaSize; - } - } - - updateArea(areaResize.id, { ...newDims }); - return; - } - - if (bulkSelectRect.show) { - setBulkSelectRect((prev) => ({ - ...prev, - x2: pointer.spaces.diagram.x, - y2: pointer.spaces.diagram.y, - })); - } - }; - - /** - * @param {PointerEvent} e - */ - const handlePointerDown = (e) => { - if (!e.isPrimary) return; - - // don't pan if the sidesheet for editing a table is open - if ( - selectedElement.element === ObjectType.TABLE && - selectedElement.open && - !layout.sidebar - ) - return; - - const isMouseLeftButton = e.button === 0; - const isMouseMiddleButton = e.button === 1; - - if (isMouseLeftButton) { - setBulkSelectRect({ - x1: pointer.spaces.diagram.x, - y1: pointer.spaces.diagram.y, - x2: pointer.spaces.diagram.x, - y2: pointer.spaces.diagram.y, - show: elementPointerDown === null || !elementPointerDown.element.locked, - ctrlKey: e.ctrlKey, - metaKey: e.metaKey, - }); - if (elementPointerDown !== null) { - handlePointerDownOnElement(e, elementPointerDown); - } - pointer.setStyle("crosshair"); - } else if (isMouseMiddleButton) { - setPanning({ - isPanning: true, - panStart: transform.pan, - // Diagram space depends on the current panning. - // Use screen space to avoid circular dependencies and undefined behavior. - cursorStart: pointer.spaces.screen, - }); - pointer.setStyle("grabbing"); - } - }; - - const isDragging = () => { - return dragging.type !== ObjectType.NONE && dragging.id !== -1; - }; - - const didDrag = () => { - if (!isDragging()) return false; - // checking any element is sufficient - const { currentCoords, initialCoords } = bulkSelectedElements[0]; - return ( - currentCoords.x !== initialCoords.x || currentCoords.y !== initialCoords.y - ); - }; - - const didResize = (id) => { - return !( - areas[id].x === areaInitDimensions.x && - areas[id].y === areaInitDimensions.y && - areas[id].width === areaInitDimensions.width && - areas[id].height === areaInitDimensions.height - ); - }; - - const didPan = () => - !( - transform.pan.x === panning.panStart.x && - transform.pan.y === panning.panStart.y - ); - - /** - * @param {PointerEvent} e - */ - const handlePointerUp = (e) => { - if (selectedElement.open && !layout.sidebar) return; - - if (!e.isPrimary) return; - - if (didDrag()) { - setUndoStack((prev) => [ - ...prev, - { - action: Action.MOVE, - bulk: true, - message: t("bulk_update"), - elements: bulkSelectedElements.map((el) => ({ - id: el.id, - type: el.type, - undo: el.initialCoords, - redo: el.currentCoords, - })), - }, - ]); - setRedoStack([]); - setBulkSelectedElements((prev) => - prev.map((el) => ({ - ...el, - initialCoords: { ...el.currentCoords }, - })), - ); - } - - if (bulkSelectRect.show) { - setBulkSelectRect((prev) => ({ - ...prev, - x2: pointer.spaces.diagram.x, - y2: pointer.spaces.diagram.y, - show: false, - })); - if (!isDragging()) { - collectSelectedElements(); - } - } - setDragging(notDragging); - - if (panning.isPanning && didPan()) { - setSaveState(State.SAVING); - } - setPanning((old) => ({ ...old, isPanning: false })); - pointer.setStyle("default"); - - if (linking) handleLinking(); - setLinking(false); - - if (areaResize.id !== -1 && didResize(areaResize.id)) { - setUndoStack((prev) => [ - ...prev, - { - action: Action.EDIT, - element: ObjectType.AREA, - aid: areaResize.id, - undo: { - ...areas[areaResize.id], - x: areaInitDimensions.x, - y: areaInitDimensions.y, - width: areaInitDimensions.width, - height: areaInitDimensions.height, - }, - redo: areas[areaResize.id], - message: t("edit_area", { - areaName: areas[areaResize.id].name, - extra: "[resize]", - }), - }, - ]); - setRedoStack([]); - } - setAreaResize({ id: -1, dir: "none" }); - setAreaInitDimensions({ - x: 0, - y: 0, - width: 0, - height: 0, - }); - }; - - const handleGripField = () => { - setPanning((old) => ({ ...old, isPanning: false })); - setDragging(notDragging); - setLinking(true); - }; - - const getCardinality = (startField, endField) => { - const startIsUnique = startField.unique || startField.primary; - const endIsUnique = endField.unique || endField.primary; - - if (startIsUnique && endIsUnique) { - return Cardinality.ONE_TO_ONE; - } - - if (startIsUnique && !endIsUnique) { - return Cardinality.ONE_TO_MANY; - } - - if (!startIsUnique && endIsUnique) { - return Cardinality.MANY_TO_ONE; - } - - return Cardinality.ONE_TO_ONE; - }; - - const handleLinking = () => { - if (hoveredTable.tableId === null) return; - if (hoveredTable.fieldId === null) return; - - const { fields: startTableFields, name: startTableName } = tables.find( - (t) => t.id === linkingLine.startTableId, - ); - const startField = startTableFields.find( - (f) => f.id === linkingLine.startFieldId, - ); - const { fields: endTableFields, name: endTableName } = tables.find( - (t) => t.id === hoveredTable.tableId, - ); - const endField = endTableFields.find((f) => f.id === hoveredTable.fieldId); - - if (!areFieldsCompatible(database, startField.type, endField.type)) { - Toast.info(t("cannot_connect")); - return; - } - if ( - linkingLine.startTableId === hoveredTable.tableId && - linkingLine.startFieldId === hoveredTable.fieldId - ) - return; - - const cardinality = getCardinality(startField, endField); - - const newRelationship = { - ...linkingLine, - cardinality, - endTableId: hoveredTable.tableId, - endFieldId: hoveredTable.fieldId, - updateConstraint: Constraint.NONE, - deleteConstraint: Constraint.NONE, - name: `fk_${startTableName}_${startField.name}_${endTableName}`, - id: nanoid(), - }; - delete newRelationship.startX; - delete newRelationship.startY; - delete newRelationship.endX; - delete newRelationship.endY; - addRelationship(newRelationship); - }; - - useEventListener( - "wheel", - (e) => { - e.preventDefault(); - - if (e.ctrlKey || e.metaKey) { - // How "eager" the viewport is to - // center the cursor's coordinates - const eagernessFactor = 0.05; - setTransform((prev) => ({ - pan: { - x: - prev.pan.x - - (pointer.spaces.diagram.x - prev.pan.x) * - eagernessFactor * - Math.sign(e.deltaY), - y: - prev.pan.y - - (pointer.spaces.diagram.y - prev.pan.y) * - eagernessFactor * - Math.sign(e.deltaY), - }, - zoom: e.deltaY <= 0 ? prev.zoom * 1.05 : prev.zoom / 1.05, - })); - } else if (e.shiftKey) { - setTransform((prev) => ({ - ...prev, - pan: { - ...prev.pan, - x: prev.pan.x + e.deltaY / prev.zoom, - }, - })); - } else { - setTransform((prev) => ({ - ...prev, - pan: { - x: prev.pan.x + e.deltaX / prev.zoom, - y: prev.pan.y + e.deltaY / prev.zoom, - }, - })); - } - }, - canvasRef, - { passive: false }, - ); - - return ( -
-
- - {settings.showGrid && ( - <> - - - - - - - - )} - {areas.map((a) => ( - { - elementPointerDown = { - element: a, - type: ObjectType.AREA, - }; - }} - /> - ))} - {relationships.map((e, i) => ( - - ))} - {tables.map((table) => ( - { - elementPointerDown = { - element: table, - type: ObjectType.TABLE, - }; - }} - /> - ))} - {linking && ( - - )} - {notes.map((n) => ( - { - elementPointerDown = { - element: n, - type: ObjectType.NOTE, - }; - }} - /> - ))} - {bulkSelectRect.show && ( - - )} - - - {settings.showDebugCoordinates && ( -
-
- - - - - - - - - - - - - - - - - -
- {t("transform")} -
pan xpan yscale
{transform.pan.x.toFixed(2)}{transform.pan.y.toFixed(2)}{transform.zoom.toFixed(4)}
- - - - - - - - - - - - - - - - - - - - -
{t("viewbox")}
lefttopwidthheight
{viewBox.left.toFixed(2)}{viewBox.top.toFixed(2)}{viewBox.width.toFixed(2)}{viewBox.height.toFixed(2)}
- - - - - - - - - - - - - - - - - - - - - - - -
{t("cursor_coordinates")}
{t("coordinate_space")}xy
{t("coordinate_space_screen")}{pointer.spaces.screen.x.toFixed(2)}{pointer.spaces.screen.y.toFixed(2)}
{t("coordinate_space_diagram")}{pointer.spaces.diagram.x.toFixed(2)}{pointer.spaces.diagram.y.toFixed(2)}
-
- )} -
- ); -} +import { useRef, useState } from "react"; + +import { + +Action, + +Cardinality, + +Constraint, + +darkBgTheme, + +ObjectType, + +gridSize, + +gridCircleRadius, + +minAreaSize, + +} from "../../data/constants"; + +import { Toast } from "@douyinfe/semi-ui"; + +import Table from "./Table"; + +import Area from "./Area"; + +import Relationship from "./Relationship"; + +import Note from "./Note"; + +import { + +useCanvas, + +useSettings, + +useTransform, + +useDiagram, + +useUndoRedo, + +useSelect, + +useAreas, + +useNotes, + +useLayout, + +useSaveState, + +} from "../../hooks"; + +import { useTranslation } from "react-i18next"; + +import { useEventListener } from "usehooks-ts"; + +import { areFieldsCompatible, getTableHeight } from "../../utils/utils"; + +import { getRectFromEndpoints, isInsideRect } from "../../utils/rect"; + +import { State, noteWidth } from "../../data/constants"; + +import { nanoid } from "nanoid"; + + + +export default function Canvas() { + +const { t } = useTranslation(); + + + +const canvasRef = useRef(null); + +const canvasContextValue = useCanvas(); + +const { + +canvas: { viewBox }, + +pointer, + +} = canvasContextValue; + + + +const { tables, updateTable, relationships, addRelationship, database } = + +useDiagram(); + +const { setSaveState } = useSaveState(); + +const { areas, updateArea } = useAreas(); + +const { notes, updateNote } = useNotes(); + +const { layout } = useLayout(); + +const { settings } = useSettings(); + +const { setUndoStack, setRedoStack } = useUndoRedo(); + +const { transform, setTransform } = useTransform(); + +const { + +selectedElement, + +setSelectedElement, + +bulkSelectedElements, + +setBulkSelectedElements, + +} = useSelect(); + +const notDragging = { + +id: -1, + +type: ObjectType.NONE, + +grabOffset: { x: 0, y: 0 }, + +}; + +const [dragging, setDragging] = useState(notDragging); + +const [linking, setLinking] = useState(false); + +const [linkingLine, setLinkingLine] = useState({ + +startTableId: -1, + +startFieldId: -1, + +endTableId: -1, + +endFieldId: -1, + +startX: 0, + +startY: 0, + +endX: 0, + +endY: 0, + +}); + +const [hoveredTable, setHoveredTable] = useState({ + +tableId: null, + +fieldId: null, + +}); + +const [panning, setPanning] = useState({ + +isPanning: false, + +panStart: { x: 0, y: 0 }, + +cursorStart: { x: 0, y: 0 }, + +}); + +const [areaResize, setAreaResize] = useState({ id: -1, dir: "none" }); + +const [areaInitDimensions, setAreaInitDimensions] = useState({ + +x: 0, + +y: 0, + +width: 0, + +height: 0, + +}); + +const [bulkSelectRect, setBulkSelectRect] = useState({ + +x1: 0, + +y1: 0, + +x2: 0, + +y2: 0, + +show: false, + +ctrlKey: false, + +metaKey: false, + +}); + +// this is used to store the element that is clicked on + +// at the moment, and shouldn't be a part of the state + +let elementPointerDown = null; + + + +const isSameElement = (el1, el2) => { + +return el1.id === el2.id && el1.type === el2.type; + +}; + + + +const collectSelectedElements = () => { + +const rect = getRectFromEndpoints(bulkSelectRect); + +const elements = []; + +const shouldAddElement = (elementRect, element) => { + +// if ctrl key is pressed, only add the elements that are not already selected + +// can theoretically be optimized later if the selected elements is + +// a map from id to element (after the ids are made unique) + +return ( + +isInsideRect(elementRect, rect) && + +((!bulkSelectRect.ctrlKey && !bulkSelectRect.metaKey) || + +!bulkSelectedElements.some((el) => isSameElement(el, element))) + +); + +}; + + + +tables.forEach((table) => { + +if (table.locked) return; + + + +const element = { + +id: table.id, + +type: ObjectType.TABLE, + +currentCoords: { x: table.x, y: table.y }, + +initialCoords: { x: table.x, y: table.y }, + +}; + +const tableRect = { + +x: table.x, + +y: table.y, + +width: settings.tableWidth, + +height: getTableHeight(table), + +}; + +if (shouldAddElement(tableRect, element)) { + +elements.push(element); + +} + +}); + + + +areas.forEach((area) => { + +if (area.locked) return; + + + +const element = { + +id: area.id, + +type: ObjectType.AREA, + +currentCoords: { x: area.x, y: area.y }, + +initialCoords: { x: area.x, y: area.y }, + +}; + +const areaRect = { + +x: area.x, + +y: area.y, + +width: area.width, + +height: area.height, + +}; + +if (shouldAddElement(areaRect, element)) { + +elements.push(element); + +} + +}); + + + +notes.forEach((note) => { + +if (note.locked) return; + + + +const element = { + +id: note.id, + +type: ObjectType.NOTE, + +currentCoords: { x: note.x, y: note.y }, + +initialCoords: { x: note.x, y: note.y }, + +}; + +const noteRect = { + +x: note.x, + +y: note.y, + +width: noteWidth, + +height: note.height, + +}; + +if (shouldAddElement(noteRect, element)) { + +elements.push(element); + +} + +}); + + + +if (bulkSelectRect.ctrlKey || bulkSelectRect.metaKey) { + +setBulkSelectedElements([...bulkSelectedElements, ...elements]); + +} else { + +setBulkSelectedElements(elements); + +} + +}; + + + +const handlePointerDownOnElement = (e, { element, type }) => { + +if (selectedElement.open && !layout.sidebar) return; + + + +if (!e.isPrimary) return; + + + +if (!element.locked || !(e.ctrlKey || e.metaKey)) { + +setSelectedElement((prev) => ({ + +...prev, + +element: type, + +id: element.id, + +open: false, + +})); + +} + + + +if (element.locked) { + +if (!(e.ctrlKey || e.metaKey)) { + +setBulkSelectedElements([]); + +} + +return; + +} + + + +setBulkSelectRect((prev) => ({ + +...prev, + +show: false, + +})); + + + +// this is the object that will be added to the bulk selected elements + +// if necessary + +const elementInBulk = { + +id: element.id, + +type, + +currentCoords: { x: element.x, y: element.y }, + +initialCoords: { x: element.x, y: element.y }, + +}; + + + +const isSelected = bulkSelectedElements.some((el) => + +isSameElement(el, elementInBulk), + +); + + + +if (e.ctrlKey || e.metaKey) { + +if (isSelected) { + +if (bulkSelectedElements.length > 1) { + +setBulkSelectedElements( + +bulkSelectedElements.filter( + +(el) => !isSameElement(el, elementInBulk), + +), + +); + +setSelectedElement({ + +...selectedElement, + +element: ObjectType.NONE, + +id: -1, + +open: false, + +}); + +} + +} else { + +setBulkSelectedElements([...bulkSelectedElements, elementInBulk]); + +} + +setDragging(notDragging); + +return; + +} + + + +if (!isSelected) { + +setBulkSelectedElements([elementInBulk]); + +} + +setDragging({ + +id: element.id, + +type, + +grabOffset: { + +x: pointer.spaces.diagram.x - element.x, + +y: pointer.spaces.diagram.y - element.y, + +}, + +}); + +}; + + + +const coordinatesAfterSnappingToGrid = ({ x, y }) => { + +if (settings.snapToGrid) { + +return { + +x: Math.round(x / gridSize) * gridSize, + +y: Math.round(y / gridSize) * gridSize, + +}; + +} + +return { x, y }; + +}; + + + +/** + +* @param {PointerEvent} e + +*/ + +const handlePointerMove = (e) => { + +if (selectedElement.open && !layout.sidebar) return; + + + +if (!e.isPrimary) return; + + + +if (panning.isPanning) { + +setTransform((prev) => ({ + +...prev, + +pan: { + +x: + +panning.panStart.x + + +(panning.cursorStart.x - pointer.spaces.screen.x) / transform.zoom, + +y: + +panning.panStart.y + + +(panning.cursorStart.y - pointer.spaces.screen.y) / transform.zoom, + +}, + +})); + +return; + +} + + + +if (layout.readOnly) return; + + + +if (linking) { + +setLinkingLine({ + +...linkingLine, + +endX: pointer.spaces.diagram.x, + +endY: pointer.spaces.diagram.y, + +}); + +return; + +} + + + +if (isDragging()) { + +const { x: mainElementFinalX, y: mainElementFinalY } = + +coordinatesAfterSnappingToGrid({ + +x: pointer.spaces.diagram.x - dragging.grabOffset.x, + +y: pointer.spaces.diagram.y - dragging.grabOffset.y, + +}); + + + +const { currentCoords } = bulkSelectedElements.find((el) => + +isSameElement(el, dragging), + +); + + + +const deltaX = mainElementFinalX - currentCoords.x; + +const deltaY = mainElementFinalY - currentCoords.y; + + + +const newBulkSelectedElements = []; + +bulkSelectedElements.forEach((el) => { + +const elementFinalCoords = { + +x: el.currentCoords.x + deltaX, + +y: el.currentCoords.y + deltaY, + +}; + +if (el.type === ObjectType.TABLE) { + +updateTable(el.id, { ...elementFinalCoords }); + +} + +if (el.type === ObjectType.AREA) { + +updateArea(el.id, { ...elementFinalCoords }); + +} + +if (el.type === ObjectType.NOTE) { + +updateNote(el.id, { ...elementFinalCoords }); + +} + +newBulkSelectedElements.push({ + +...el, + +currentCoords: elementFinalCoords, + +}); + +}); + + + +setBulkSelectedElements(newBulkSelectedElements); + +return; + +} + + + +if (areaResize.id !== -1) { + +if (areaResize.dir === "none") return; + +let newDims = { ...areaInitDimensions }; + +setPanning((old) => ({ ...old, isPanning: false })); + +const { x, y } = coordinatesAfterSnappingToGrid(pointer.spaces.diagram); + + + +switch (areaResize.dir) { + +case "br": + +newDims.width = x - areaInitDimensions.x; + +newDims.height = y - areaInitDimensions.y; + +break; + +case "tl": + +newDims.x = x; + +newDims.y = y; + +newDims.width = areaInitDimensions.width - (x - areaInitDimensions.x); + +newDims.height = + +areaInitDimensions.height - (y - areaInitDimensions.y); + +break; + +case "tr": + +newDims.y = y; + +newDims.width = x - areaInitDimensions.x; + +newDims.height = + +areaInitDimensions.height - (y - areaInitDimensions.y); + +break; + +case "bl": + +newDims.x = x; + +newDims.width = areaInitDimensions.width - (x - areaInitDimensions.x); + +newDims.height = y - areaInitDimensions.y; + +break; + +} + + + +if (newDims.width <= minAreaSize) { + +newDims.width = minAreaSize; + +if (areaResize.dir === "tl" || areaResize.dir === "bl") { + +newDims.x = + +areaInitDimensions.x + areaInitDimensions.width - minAreaSize; + +} + +} + + + +if (newDims.height <= minAreaSize) { + +newDims.height = minAreaSize; + +if (areaResize.dir === "tl" || areaResize.dir === "tr") { + +newDims.y = + +areaInitDimensions.y + areaInitDimensions.height - minAreaSize; + +} + +} + + + +updateArea(areaResize.id, { ...newDims }); + +return; + +} + + + +if (bulkSelectRect.show) { + +setBulkSelectRect((prev) => ({ + +...prev, + +x2: pointer.spaces.diagram.x, + +y2: pointer.spaces.diagram.y, + +})); + +} + +}; + + + +/** + +* @param {PointerEvent} e + +*/ + +const handlePointerDown = (e) => { + +if (!e.isPrimary) return; + + + +// don't pan if the sidesheet for editing a table is open + +if ( + +selectedElement.element === ObjectType.TABLE && + +selectedElement.open && + +!layout.sidebar + +) + +return; + + + +const isMouseLeftButton = e.button === 0; + +const isMouseMiddleButton = e.button === 1; + + + +if (isMouseLeftButton) { + +setBulkSelectRect({ + +x1: pointer.spaces.diagram.x, + +y1: pointer.spaces.diagram.y, + +x2: pointer.spaces.diagram.x, + +y2: pointer.spaces.diagram.y, + +show: elementPointerDown === null || !elementPointerDown.element.locked, + +ctrlKey: e.ctrlKey, + +metaKey: e.metaKey, + +}); + +if (elementPointerDown !== null) { + +handlePointerDownOnElement(e, elementPointerDown); + +} + +pointer.setStyle("crosshair"); + +} else if (isMouseMiddleButton) { + +setPanning({ + +isPanning: true, + +panStart: transform.pan, + +// Diagram space depends on the current panning. + +// Use screen space to avoid circular dependencies and undefined behavior. + +cursorStart: pointer.spaces.screen, + +}); + +pointer.setStyle("grabbing"); + +} + +}; + + + +const isDragging = () => { + +return dragging.type !== ObjectType.NONE && dragging.id !== -1; + +}; + + + +const didDrag = () => { + +if (!isDragging()) return false; + +// checking any element is sufficient + +const { currentCoords, initialCoords } = bulkSelectedElements[0]; + +return ( + +currentCoords.x !== initialCoords.x || currentCoords.y !== initialCoords.y + +); + +}; + + + +const didResize = (id) => { + +return !( + +areas[id].x === areaInitDimensions.x && + +areas[id].y === areaInitDimensions.y && + +areas[id].width === areaInitDimensions.width && + +areas[id].height === areaInitDimensions.height + +); + +}; + + + +const didPan = () => + +!( + +transform.pan.x === panning.panStart.x && + +transform.pan.y === panning.panStart.y + +); + + + +/** + +* @param {PointerEvent} e + +*/ + +const handlePointerUp = (e) => { + +if (selectedElement.open && !layout.sidebar) return; + + + +if (!e.isPrimary) return; + + + +if (didDrag()) { + +setUndoStack((prev) => [ + +...prev, + +{ + +action: Action.MOVE, + +bulk: true, + +message: t("bulk_update"), + +elements: bulkSelectedElements.map((el) => ({ + +id: el.id, + +type: el.type, + +undo: el.initialCoords, + +redo: el.currentCoords, + +})), + +}, + +]); + +setRedoStack([]); + +setBulkSelectedElements((prev) => + +prev.map((el) => ({ + +...el, + +initialCoords: { ...el.currentCoords }, + +})), + +); + +} + + + +if (bulkSelectRect.show) { + +setBulkSelectRect((prev) => ({ + +...prev, + +x2: pointer.spaces.diagram.x, + +y2: pointer.spaces.diagram.y, + +show: false, + +})); + +if (!isDragging()) { + +collectSelectedElements(); + +} + +} + +setDragging(notDragging); + + + +if (panning.isPanning && didPan()) { + +setSaveState(State.SAVING); + +} + +setPanning((old) => ({ ...old, isPanning: false })); + +pointer.setStyle("default"); + + + +if (linking) handleLinking(); + +setLinking(false); + + + +if (areaResize.id !== -1 && didResize(areaResize.id)) { + +setUndoStack((prev) => [ + +...prev, + +{ + +action: Action.EDIT, + +element: ObjectType.AREA, + +aid: areaResize.id, + +undo: { + +...areas[areaResize.id], + +x: areaInitDimensions.x, + +y: areaInitDimensions.y, + +width: areaInitDimensions.width, + +height: areaInitDimensions.height, + +}, + +redo: areas[areaResize.id], + +message: t("edit_area", { + +areaName: areas[areaResize.id].name, + +extra: "[resize]", + +}), + +}, + +]); + +setRedoStack([]); + +} + +setAreaResize({ id: -1, dir: "none" }); + +setAreaInitDimensions({ + +x: 0, + +y: 0, + +width: 0, + +height: 0, + +}); + +}; + + + +const handleGripField = () => { + +setPanning((old) => ({ ...old, isPanning: false })); + +setDragging(notDragging); + +setLinking(true); + +}; + + + +const getCardinality = (startField, endField) => { + +const startIsUnique = startField.unique || startField.primary; + +const endIsUnique = endField.unique || endField.primary; + + + +if (startIsUnique && endIsUnique) { + +return Cardinality.ONE_TO_ONE; + +} + + + +if (startIsUnique && !endIsUnique) { + +return Cardinality.ONE_TO_MANY; + +} + + + +if (!startIsUnique && endIsUnique) { + +return Cardinality.MANY_TO_ONE; + +} + + + +return Cardinality.ONE_TO_ONE; + +}; + + + +const handleLinking = () => { + +if (hoveredTable.tableId === null) return; + +if (hoveredTable.fieldId === null) return; + + + +const { fields: startTableFields, name: startTableName } = tables.find( + +(t) => t.id === linkingLine.startTableId, + +); + +const startField = startTableFields.find( + +(f) => f.id === linkingLine.startFieldId, + +); + +const { fields: endTableFields, name: endTableName } = tables.find( + +(t) => t.id === hoveredTable.tableId, + +); + +const endField = endTableFields.find((f) => f.id === hoveredTable.fieldId); + + + +if (!areFieldsCompatible(database, startField.type, endField.type)) { + +Toast.info(t("cannot_connect")); + +return; + +} + +if ( + +linkingLine.startTableId === hoveredTable.tableId && + +linkingLine.startFieldId === hoveredTable.fieldId + +) + +return; + + + +const cardinality = getCardinality(startField, endField); + + + +const newRelationship = { + +...linkingLine, + +cardinality, + +endTableId: hoveredTable.tableId, + +endFieldId: hoveredTable.fieldId, + +updateConstraint: Constraint.NONE, + +deleteConstraint: Constraint.NONE, + +name: `fk_${startTableName}_${startField.name}_${endTableName}`, + +id: nanoid(), + +}; + +delete newRelationship.startX; + +delete newRelationship.startY; + +delete newRelationship.endX; + +delete newRelationship.endY; + +addRelationship(newRelationship); + +}; + + + +useEventListener( + +"wheel", + +(e) => { + +e.preventDefault(); + + + +if (e.ctrlKey || e.metaKey) { + +// How "eager" the viewport is to + +// center the cursor's coordinates + +const eagernessFactor = 0.05; + +setTransform((prev) => ({ + +pan: { + +x: + +prev.pan.x - + +(pointer.spaces.diagram.x - prev.pan.x) * + +eagernessFactor * + +Math.sign(e.deltaY), + +y: + +prev.pan.y - + +(pointer.spaces.diagram.y - prev.pan.y) * + +eagernessFactor * + +Math.sign(e.deltaY), + +}, + +zoom: e.deltaY <= 0 ? prev.zoom * 1.05 : prev.zoom / 1.05, + +})); + +} else if (e.shiftKey) { + +setTransform((prev) => ({ + +...prev, + +pan: { + +...prev.pan, + +x: prev.pan.x + e.deltaY / prev.zoom, + +}, + +})); + +} else { + +setTransform((prev) => ({ + +...prev, + +pan: { + +x: prev.pan.x + e.deltaX / prev.zoom, + +y: prev.pan.y + e.deltaY / prev.zoom, + +}, + +})); + +} + +}, + +canvasRef, + +{ passive: false }, + +); + + + +return ( + +
+ +
+ + + +{settings.showGrid && ( + +<> + + + + + + + + + + + + + + + +)} + +{areas.map((a) => ( + + { + +elementPointerDown = { + +element: a, + +type: ObjectType.AREA, + +}; + +}} + +/> + +))} + +{relationships.map((e, i) => { + +// Calculate relationships sharing the same start or end field + +const relatedRelationships = relationships.filter( + +(r) => + +(r.startTableId === e.startTableId && r.startFieldId === e.startFieldId) || + +(r.endTableId === e.endTableId && r.endFieldId === e.endFieldId) + +); + +const relationshipIndex = relatedRelationships.findIndex((r) => r.id === e.id); + +const totalRelated = relatedRelationships.length; + + + +return ( + + + +); + +})} + +{tables.map((table) => ( + + { + +elementPointerDown = { + +element: table, + +type: ObjectType.TABLE, + +}; + +}} + +/> + +))} + +{linking && ( + + + +)} + +{notes.map((n) => ( + + { + +elementPointerDown = { + +element: n, + +type: ObjectType.NOTE, + +}; + +}} + +/> + +))} + +{bulkSelectRect.show && ( + + + +)} + + + + + +{settings.showDebugCoordinates && ( + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +{t("transform")} + +
pan xpan yscale
{transform.pan.x.toFixed(2)}{transform.pan.y.toFixed(2)}{transform.zoom.toFixed(4)}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{t("viewbox")}
lefttopwidthheight
{viewBox.left.toFixed(2)}{viewBox.top.toFixed(2)}{viewBox.width.toFixed(2)}{viewBox.height.toFixed(2)}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{t("cursor_coordinates")}
{t("coordinate_space")}xy
{t("coordinate_space_screen")}{pointer.spaces.screen.x.toFixed(2)}{pointer.spaces.screen.y.toFixed(2)}
{t("coordinate_space_diagram")}{pointer.spaces.diagram.x.toFixed(2)}{pointer.spaces.diagram.y.toFixed(2)}
+ +
+ +)} + +
+ +); + +} \ No newline at end of file diff --git a/src/components/EditorCanvas/Relationship.jsx b/src/components/EditorCanvas/Relationship.jsx index 72457de..2f436c4 100644 --- a/src/components/EditorCanvas/Relationship.jsx +++ b/src/components/EditorCanvas/Relationship.jsx @@ -1,220 +1,571 @@ -import { useMemo, useRef, useState, useEffect } from "react"; -import { Cardinality, ObjectType, Tab } from "../../data/constants"; -import { calcPath } from "../../utils/calcPath"; -import { useDiagram, useSettings, useLayout, useSelect } from "../../hooks"; -import { useTranslation } from "react-i18next"; -import { SideSheet } from "@douyinfe/semi-ui"; -import RelationshipInfo from "../EditorSidePanel/RelationshipsTab/RelationshipInfo"; - -const labelFontSize = 16; - -export default function Relationship({ data }) { - const { settings } = useSettings(); - const { tables } = useDiagram(); - const { layout } = useLayout(); - 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 pathRef = useRef(); - const labelRef = useRef(); - - let cardinalityStart = "1"; - let cardinalityEnd = "1"; - - switch (data.cardinality) { - // the translated values are to ensure backwards compatibility - case t(Cardinality.MANY_TO_ONE): - case Cardinality.MANY_TO_ONE: - cardinalityStart = data.manyLabel || "n"; - cardinalityEnd = "1"; - break; - case t(Cardinality.ONE_TO_MANY): - case Cardinality.ONE_TO_MANY: - cardinalityStart = "1"; - cardinalityEnd = data.manyLabel || "n"; - break; - case t(Cardinality.ONE_TO_ONE): - case Cardinality.ONE_TO_ONE: - cardinalityStart = "1"; - cardinalityEnd = "1"; - break; - default: - break; - } - - let cardinalityStartX = 0; - let cardinalityEndX = 0; - let cardinalityStartY = 0; - let cardinalityEndY = 0; - let labelX = 0; - let labelY = 0; - - let labelWidth = labelRef.current?.getBBox().width ?? 0; - let labelHeight = labelRef.current?.getBBox().height ?? 0; - - const cardinalityOffset = 28; - - if (pathRef.current) { - const pathLength = pathRef.current.getTotalLength(); - - const labelPoint = pathRef.current.getPointAtLength(pathLength / 2); - labelX = labelPoint.x - (labelWidth ?? 0) / 2; - labelY = labelPoint.y + (labelHeight ?? 0) / 2; - - const point1 = pathRef.current.getPointAtLength(cardinalityOffset); - cardinalityStartX = point1.x; - cardinalityStartY = point1.y; - const point2 = pathRef.current.getPointAtLength( - pathLength - cardinalityOffset, - ); - cardinalityEndX = point2.x; - cardinalityEndY = point2.y; - } - - const edit = () => { - if (!layout.sidebar) { - setSelectedElement((prev) => ({ - ...prev, - element: ObjectType.RELATIONSHIP, - id: data.id, - open: true, - })); - } else { - setSelectedElement((prev) => ({ - ...prev, - currentTab: Tab.RELATIONSHIPS, - element: ObjectType.RELATIONSHIP, - id: data.id, - open: true, - })); - if (selectedElement.currentTab !== Tab.RELATIONSHIPS) return; - document - .getElementById(`scroll_ref_${data.id}`) - .scrollIntoView({ behavior: "smooth" }); - } - }; - - return ( - <> - - {/* invisible wider path for better hover ux */} - - - {settings.showRelationshipLabels && ( - - {data.name} - - )} - {pathRef.current && settings.showCardinality && ( - <> - - - - )} - - { - setSelectedElement((prev) => ({ - ...prev, - open: false, - })); - }} - style={{ paddingBottom: "16px" }} - > -
- -
-
- - ); -} - -function CardinalityLabel({ x, y, text, r = 12, padding = 14 }) { - const [textWidth, setTextWidth] = useState(0); - const textRef = useRef(null); - - useEffect(() => { - if (textRef.current) { - const bbox = textRef.current.getBBox(); - setTextWidth(bbox.width); - } - }, [text]); - - return ( - - - - {text} - - - ); -} +import { useMemo, useRef, useState, useEffect } from "react"; + +import { Cardinality, ObjectType, Tab } from "../../data/constants"; + +import { calcPath } from "../../utils/calcPath"; + +import { useDiagram, useSettings, useLayout, useSelect } from "../../hooks"; + +import { useTranslation } from "react-i18next"; + +import { SideSheet } from "@douyinfe/semi-ui"; + +import RelationshipInfo from "../EditorSidePanel/RelationshipsTab/RelationshipInfo"; + + + +const labelFontSize = 16; + + + +export default function Relationship({ data, relationshipIndex = 0, totalRelated = 1 }) { + +const { settings } = useSettings(); + +const { tables } = useDiagram(); + +const { layout } = useLayout(); + +const { selectedElement, setSelectedElement } = useSelect(); + +const { t } = useTranslation(); + +const [isHovered, setIsHovered] = useState(false); + + + +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 pathRef = useRef(); + +const labelRef = useRef(); + + + +let cardinalityStart = "1"; + +let cardinalityEnd = "1"; + + + +switch (data.cardinality) { + +// the translated values are to ensure backwards compatibility + +case t(Cardinality.MANY_TO_ONE): + +case Cardinality.MANY_TO_ONE: + +cardinalityStart = data.manyLabel || "n"; + +cardinalityEnd = "1"; + +break; + +case t(Cardinality.ONE_TO_MANY): + +case Cardinality.ONE_TO_MANY: + +cardinalityStart = "1"; + +cardinalityEnd = data.manyLabel || "n"; + +break; + +case t(Cardinality.ONE_TO_ONE): + +case Cardinality.ONE_TO_ONE: + +cardinalityStart = "1"; + +cardinalityEnd = "1"; + +break; + +default: + +break; + +} + + + +let cardinalityStartX = 0; + +let cardinalityEndX = 0; + +let cardinalityStartY = 0; + +let cardinalityEndY = 0; + +let labelX = 0; + +let labelY = 0; + + + +let labelWidth = labelRef.current?.getBBox().width ?? 0; + +let labelHeight = labelRef.current?.getBBox().height ?? 0; + + + +const cardinalityOffset = 28; + +// Calculate perpendicular offset for multiple relationships on same field + +const lateralOffset = totalRelated > 1 ? (relationshipIndex - (totalRelated - 1) / 2) * 20 : 0; + + + +if (pathRef.current) { + +const pathLength = pathRef.current.getTotalLength(); + + + +const labelPoint = pathRef.current.getPointAtLength(pathLength / 2); + +labelX = labelPoint.x - (labelWidth ?? 0) / 2; + +labelY = labelPoint.y + (labelHeight ?? 0) / 2; + + + +const point1 = pathRef.current.getPointAtLength(cardinalityOffset); + +const point2 = pathRef.current.getPointAtLength( + +pathLength - cardinalityOffset, + +); + + + +// Calculate perpendicular offset for start cardinality + +const startTangent = pathRef.current.getPointAtLength(cardinalityOffset + 1); + +const startAngle = Math.atan2(startTangent.y - point1.y, startTangent.x - point1.x); + +const startPerpAngle = startAngle + Math.PI / 2; + + + +cardinalityStartX = point1.x + Math.cos(startPerpAngle) * lateralOffset; + +cardinalityStartY = point1.y + Math.sin(startPerpAngle) * lateralOffset; + + + +// Calculate perpendicular offset for end cardinality + +const endTangent = pathRef.current.getPointAtLength(pathLength - cardinalityOffset - 1); + +const endAngle = Math.atan2(point2.y - endTangent.y, point2.x - endTangent.x); + +const endPerpAngle = endAngle + Math.PI / 2; + + + +cardinalityEndX = point2.x + Math.cos(endPerpAngle) * lateralOffset; + +cardinalityEndY = point2.y + Math.sin(endPerpAngle) * lateralOffset; + +} + + + +const edit = () => { + +if (!layout.sidebar) { + +setSelectedElement((prev) => ({ + +...prev, + +element: ObjectType.RELATIONSHIP, + +id: data.id, + +open: true, + +})); + +} else { + +setSelectedElement((prev) => ({ + +...prev, + +currentTab: Tab.RELATIONSHIPS, + +element: ObjectType.RELATIONSHIP, + +id: data.id, + +open: true, + +})); + +if (selectedElement.currentTab !== Tab.RELATIONSHIPS) return; + +document + +.getElementById(`scroll_ref_${data.id}`) + +.scrollIntoView({ behavior: "smooth" }); + +} + +}; + + + +return ( + +<> + + setIsHovered(true)} + +onMouseLeave={() => setIsHovered(false)} + +> + +{/* invisible wider path for better hover ux */} + + + + + +{settings.showRelationshipLabels && ( + + + +{data.name} + + + +)} + +{pathRef.current && settings.showCardinality && ( + +<> + + + + + + + +)} + +{/* Tooltip on hover */} + +{isHovered && ( + + + + + + + +{data.name} + + + + + +{data.cardinality} + + + + + +)} + + + + { + +setSelectedElement((prev) => ({ + +...prev, + +open: false, + +})); + +}} + +style={{ paddingBottom: "16px" }} + +> + +
+ + + +
+ +
+ + + +); + +} + + + +function CardinalityLabel({ x, y, text, r = 12, padding = 14, isHovered = false }) { + +const [textWidth, setTextWidth] = useState(0); + +const textRef = useRef(null); + + + +useEffect(() => { + +if (textRef.current) { + +const bbox = textRef.current.getBBox(); + +setTextWidth(bbox.width); + +} + +}, [text]); + + + +return ( + + + + + + + +{text} + + + + + +); + +} \ No newline at end of file diff --git a/src/context/SettingsContext.jsx b/src/context/SettingsContext.jsx index 9c50bf8..858e616 100644 --- a/src/context/SettingsContext.jsx +++ b/src/context/SettingsContext.jsx @@ -1,43 +1,87 @@ -import { createContext, useEffect, useState } from "react"; -import { tableWidth } from "../data/constants"; - -const defaultSettings = { - strictMode: false, - showFieldSummary: true, - showGrid: true, - snapToGrid: false, - showDataTypes: true, - mode: "light", - autosave: true, - showCardinality: true, - showRelationshipLabels: true, - tableWidth: tableWidth, - showDebugCoordinates: false, -}; - -export const SettingsContext = createContext(defaultSettings); - -export default function SettingsContextProvider({ children }) { - const [settings, setSettings] = useState(defaultSettings); - - useEffect(() => { - const settings = localStorage.getItem("settings"); - if (settings) { - setSettings(JSON.parse(settings)); - } - }, []); - - useEffect(() => { - document.body.setAttribute("theme-mode", settings.mode); - }, [settings.mode]); - - useEffect(() => { - localStorage.setItem("settings", JSON.stringify(settings)); - }, [settings]); - - return ( - - {children} - - ); -} +import { createContext, useEffect, useState } from "react"; + +import { tableWidth } from "../data/constants"; + + + +const defaultSettings = { + +strictMode: false, + +showFieldSummary: true, + +showGrid: true, + +snapToGrid: false, + +showDataTypes: true, + +mode: "light", + +autosave: true, + +showCardinality: true, + +showRelationshipLabels: true, + +tableWidth: tableWidth, + +showDebugCoordinates: false, + +generateForeignKeys: true, + +}; + + + +export const SettingsContext = createContext(defaultSettings); + + + +export default function SettingsContextProvider({ children }) { + +const [settings, setSettings] = useState(defaultSettings); + + + +useEffect(() => { + +const settings = localStorage.getItem("settings"); + +if (settings) { + +setSettings(JSON.parse(settings)); + +} + +}, []); + + + +useEffect(() => { + +document.body.setAttribute("theme-mode", settings.mode); + +}, [settings.mode]); + + + +useEffect(() => { + +localStorage.setItem("settings", JSON.stringify(settings)); + +}, [settings]); + + + +return ( + + + +{children} + + + +); + +} \ No newline at end of file diff --git a/src/utils/exportSQL/index.js b/src/utils/exportSQL/index.js index 987fb26..b990055 100644 --- a/src/utils/exportSQL/index.js +++ b/src/utils/exportSQL/index.js @@ -1,26 +1,51 @@ -import { DB } from "../../data/constants"; -import { toMariaDB } from "./mariadb"; -import { toMSSQL } from "./mssql"; -import { toMySQL } from "./mysql"; -import { toOracleSQL } from "./oraclesql"; -import { toPostgres } from "./postgres"; -import { toSqlite } from "./sqlite"; - -export function exportSQL(diagram) { - switch (diagram.database) { - case DB.SQLITE: - return toSqlite(diagram); - case DB.MYSQL: - return toMySQL(diagram); - case DB.POSTGRES: - return toPostgres(diagram); - case DB.MARIADB: - return toMariaDB(diagram); - case DB.MSSQL: - return toMSSQL(diagram); - case DB.ORACLESQL: - return toOracleSQL(diagram); - default: - return ""; - } -} +import { DB } from "../../data/constants"; + +import { toMariaDB } from "./mariadb"; + +import { toMSSQL } from "./mssql"; + +import { toMySQL } from "./mysql"; + +import { toOracleSQL } from "./oraclesql"; + +import { toPostgres } from "./postgres"; + +import { toSqlite } from "./sqlite"; + + + +export function exportSQL(diagram, generateForeignKeys = true) { + +switch (diagram.database) { + +case DB.SQLITE: + +return toSqlite(diagram, generateForeignKeys); + +case DB.MYSQL: + +return toMySQL(diagram, generateForeignKeys); + +case DB.POSTGRES: + +return toPostgres(diagram, generateForeignKeys); + +case DB.MARIADB: + +return toMariaDB(diagram, generateForeignKeys); + +case DB.MSSQL: + +return toMSSQL(diagram, generateForeignKeys); + +case DB.ORACLESQL: + +return toOracleSQL(diagram, generateForeignKeys); + +default: + +return ""; + +} + +} \ No newline at end of file diff --git a/src/utils/exportSQL/mariadb.js b/src/utils/exportSQL/mariadb.js index 8bab3bc..21b51c6 100644 --- a/src/utils/exportSQL/mariadb.js +++ b/src/utils/exportSQL/mariadb.js @@ -1,74 +1,149 @@ -import { escapeQuotes, parseDefault } from "./shared"; - -import { dbToTypes } from "../../data/datatypes"; -import { DB } from "../../data/constants"; - -function parseType(field) { - let res = field.type; - - if (field.type === "SET" || field.type === "ENUM") { - res += `${field.values ? "(" + field.values.map((value) => "'" + value + "'").join(", ") + ")" : ""}`; - } - - if (dbToTypes[DB.MARIADB][field.type].isSized) { - res += `${field.size && field.size !== "" ? "(" + field.size + ")" : ""}`; - } - - return res; -} - -export function toMariaDB(diagram) { - return `${diagram.tables - .map( - (table) => - `CREATE OR REPLACE TABLE \`${table.name}\` (\n${table.fields - .map( - (field) => - `\t\`${field.name}\` ${parseType(field)}${field.unsigned ? " UNSIGNED" : ""}${field.notNull ? " NOT NULL" : ""}${ - field.increment ? " AUTO_INCREMENT" : "" - }${field.unique ? " UNIQUE" : ""}${ - field.default !== "" - ? ` DEFAULT ${parseDefault(field, diagram.database)}` - : "" - }${ - field.check === "" || - !dbToTypes[diagram.database][field.type].hasCheck - ? "" - : ` CHECK(${field.check})` - }${field.comment ? ` COMMENT '${escapeQuotes(field.comment)}'` : ""}`, - ) - .join(",\n")}${ - table.fields.filter((f) => f.primary).length > 0 - ? `,\n\tPRIMARY KEY(${table.fields - .filter((f) => f.primary) - .map((f) => `\`${f.name}\``) - .join(", ")})` - : "" - }\n)${table.comment ? ` COMMENT='${escapeQuotes(table.comment)}'` : ""};${`\n${table.indices - .map( - (i) => - `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX \`${ - i.name - }\`\nON \`${table.name}\` (${i.fields - .map((f) => `\`${f}\``) - .join(", ")});`, - ) - .join("")}`}`, - ) - .join("\n")}\n${diagram.references - .map((r) => { - const { name: startName, fields: startFields } = diagram.tables.find( - (t) => t.id === r.startTableId, - ); - - const { name: endName, fields: endFields } = diagram.tables.find( - (t) => t.id === r.endTableId, - ); - return `ALTER TABLE \`${startName}\`\nADD FOREIGN KEY(\`${ - startFields.find((f) => f.id === r.startFieldId).name - }\`) REFERENCES \`${endName}\`(\`${ - endFields.find((f) => f.id === r.endFieldId).name - }\`)\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; - }) - .join("\n")}`; -} +import { escapeQuotes, parseDefault } from "./shared"; + + + +import { dbToTypes } from "../../data/datatypes"; + +import { DB } from "../../data/constants"; + + + +function parseType(field) { + +let res = field.type; + + + +if (field.type === "SET" || field.type === "ENUM") { + +res += `${field.values ? "(" + field.values.map((value) => "'" + value + "'").join(", ") + ")" : ""}`; + +} + + + +if (dbToTypes[DB.MARIADB][field.type].isSized) { + +res += `${field.size && field.size !== "" ? "(" + field.size + ")" : ""}`; + +} + + + +return res; + +} + + + +export function toMariaDB(diagram, generateForeignKeys = true) { + +return `${diagram.tables + +.map( + +(table) => + +`CREATE OR REPLACE TABLE \`${table.name}\` (\n${table.fields + +.map( + +(field) => + +`\t\`${field.name}\` ${parseType(field)}${field.unsigned ? " UNSIGNED" : ""}${field.notNull ? " NOT NULL" : ""}${ + +field.increment ? " AUTO_INCREMENT" : "" + +}${field.unique ? " UNIQUE" : ""}${ + +field.default !== "" + +? ` DEFAULT ${parseDefault(field, diagram.database)}` + +: "" + +}${ + +field.check === "" || + +!dbToTypes[diagram.database][field.type].hasCheck + +? "" + +: ` CHECK(${field.check})` + +}${field.comment ? ` COMMENT '${escapeQuotes(field.comment)}'` : ""}`, + +) + +.join(",\n")}${ + +table.fields.filter((f) => f.primary).length > 0 + +? `,\n\tPRIMARY KEY(${table.fields + +.filter((f) => f.primary) + +.map((f) => `\`${f.name}\``) + +.join(", ")})` + +: "" + +}\n)${table.comment ? ` COMMENT='${escapeQuotes(table.comment)}'` : ""};${`\n${table.indices + +.map( + +(i) => + +`\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX \`${ + +i.name + +}\`\nON \`${table.name}\` (${i.fields + +.map((f) => `\`${f}\``) + +.join(", ")});`, + +) + +.join("")}`}`, + +) + +.join("\n")}${generateForeignKeys ? `\n${diagram.references + +.map((r) => { + +const { name: startName, fields: startFields } = diagram.tables.find( + +(t) => t.id === r.startTableId, + +); + + + +const { name: endName, fields: endFields } = diagram.tables.find( + +(t) => t.id === r.endTableId, + +); + +return `ALTER TABLE \`${startName}\`\nADD FOREIGN KEY(\`${ + +startFields.find((f) => f.id === r.startFieldId).name + +}\`) REFERENCES \`${endName}\`(\`${ + +endFields.find((f) => f.id === r.endFieldId).name + +}\`)\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; + +}) + +.join("\n")}` : ""}`; + +} + + \ No newline at end of file diff --git a/src/utils/exportSQL/mssql.js b/src/utils/exportSQL/mssql.js index a2354d3..588fc85 100644 --- a/src/utils/exportSQL/mssql.js +++ b/src/utils/exportSQL/mssql.js @@ -1,116 +1,231 @@ -import { parseDefault, escapeQuotes } from "./shared"; - -import { dbToTypes } from "../../data/datatypes"; -import { DB } from "../../data/constants"; - -function generateAddExtendedPropertySQL(value, level1name, level2name = null) { - if (!value || value.trim() === "") { - return ""; - } - const escapedValue = escapeQuotes(value.replace(/\n/g, " ")); - const escapedTableName = escapeQuotes(level1name); - - if (level2name) { - const escapedColumnName = escapeQuotes(level2name); - return ` -EXEC sys.sp_addextendedproperty - @name=N'MS_Description', @value=N'${escapedValue}', - @level0type=N'SCHEMA',@level0name=N'dbo', - @level1type=N'TABLE',@level1name=N'${escapedTableName}', - @level2type=N'COLUMN',@level2name=N'${escapedColumnName}'; -GO -`; - } else { - return ` -EXEC sys.sp_addextendedproperty - @name=N'MS_Description', @value=N'${escapedValue}', - @level0type=N'SCHEMA',@level0name=N'dbo', - @level1type=N'TABLE',@level1name=N'${escapedTableName}'; -GO -`; - } -} - -export function toMSSQL(diagram) { - const tablesSql = diagram.tables - .map((table) => { - const fieldsSql = table.fields - .map((field) => { - const typeMetaData = dbToTypes[DB.MSSQL][field.type.toUpperCase()]; - const isSized = typeMetaData.isSized || typeMetaData.hasPrecision; - - return `\t[${field.name}] ${field.type}${field.size && isSized ? `(${field.size})` : ""}${ - field.notNull ? " NOT NULL" : "" - }${field.increment ? " IDENTITY" : ""}${ - field.unique ? " UNIQUE" : "" - }${ - field.default !== "" - ? ` DEFAULT ${parseDefault(field, diagram.database)}` - : "" - }${ - field.check === "" || - !dbToTypes[diagram.database][field.type].hasCheck - ? "" - : ` CHECK(${field.check})` - }`; - }) - .join(",\n"); - - const primaryKeys = table.fields.filter((f) => f.primary); - const primaryKeySql = - primaryKeys.length > 0 - ? `,\n\tPRIMARY KEY(${primaryKeys - .map((f) => `[${f.name}]`) - .join(", ")})` - : ""; - - const createTableSql = `CREATE TABLE [${table.name}] (\n${fieldsSql}${primaryKeySql}\n);\nGO\n`; - - const tableCommentSql = generateAddExtendedPropertySQL( - table.comment, - table.name, - ); - - const columnCommentsSql = table.fields - .map((field) => - generateAddExtendedPropertySQL(field.comment, table.name, field.name), - ) - .join(""); - - const indicesSql = table.indices - .map( - (i) => - `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX [${ - i.name - }]\nON [${table.name}] (${i.fields - .map((f) => `[${f}]`) - .join(", ")});\nGO\n`, - ) - .join(""); - - return `${createTableSql}${tableCommentSql}${columnCommentsSql}${indicesSql}`; - }) - .join("\n"); - - const referencesSql = diagram.references - .map((r) => { - const startTable = diagram.tables.find((t) => t.id === r.startTableId); - const endTable = diagram.tables.find((t) => t.id === r.endTableId); - - if (!startTable || !endTable) return ""; - - const startField = startTable.fields.find((f) => f.id === r.startFieldId); - const endField = endTable.fields.find((f) => f.id === r.endFieldId); - - if (!startField || !endField) return ""; - - return `\nALTER TABLE [${startTable.name}] -ADD FOREIGN KEY([${startField.name}]) -REFERENCES [${endTable.name}]([${endField.name}]) -ON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()}; -GO`; - }) - .join(""); - - return `${tablesSql}\n${referencesSql}`; -} +import { parseDefault, escapeQuotes } from "./shared"; + + + +import { dbToTypes } from "../../data/datatypes"; + +import { DB } from "../../data/constants"; + + + +function generateAddExtendedPropertySQL(value, level1name, level2name = null) { + +if (!value || value.trim() === "") { + +return ""; + +} + +const escapedValue = escapeQuotes(value.replace(/\n/g, " ")); + +const escapedTableName = escapeQuotes(level1name); + + + +if (level2name) { + +const escapedColumnName = escapeQuotes(level2name); + +return ` + +EXEC sys.sp_addextendedproperty + +@name=N'MS_Description', @value=N'${escapedValue}', + +@level0type=N'SCHEMA',@level0name=N'dbo', + +@level1type=N'TABLE',@level1name=N'${escapedTableName}', + +@level2type=N'COLUMN',@level2name=N'${escapedColumnName}'; + +GO + +`; + +} else { + +return ` + +EXEC sys.sp_addextendedproperty + +@name=N'MS_Description', @value=N'${escapedValue}', + +@level0type=N'SCHEMA',@level0name=N'dbo', + +@level1type=N'TABLE',@level1name=N'${escapedTableName}'; + +GO + +`; + +} + +} + + + +export function toMSSQL(diagram, generateForeignKeys = true) { + +const tablesSql = diagram.tables + +.map((table) => { + +const fieldsSql = table.fields + +.map((field) => { + +const typeMetaData = dbToTypes[DB.MSSQL][field.type.toUpperCase()]; + +const isSized = typeMetaData.isSized || typeMetaData.hasPrecision; + + + +return `\t[${field.name}] ${field.type}${field.size && isSized ? `(${field.size})` : ""}${ + +field.notNull ? " NOT NULL" : "" + +}${field.increment ? " IDENTITY" : ""}${ + +field.unique ? " UNIQUE" : "" + +}${ + +field.default !== "" + +? ` DEFAULT ${parseDefault(field, diagram.database)}` + +: "" + +}${ + +field.check === "" || + +!dbToTypes[diagram.database][field.type].hasCheck + +? "" + +: ` CHECK(${field.check})` + +}`; + +}) + +.join(",\n"); + + + +const primaryKeys = table.fields.filter((f) => f.primary); + +const primaryKeySql = + +primaryKeys.length > 0 + +? `,\n\tPRIMARY KEY(${primaryKeys + +.map((f) => `[${f.name}]`) + +.join(", ")})` + +: ""; + + + +const createTableSql = `CREATE TABLE [${table.name}] (\n${fieldsSql}${primaryKeySql}\n);\nGO\n`; + + + +const tableCommentSql = generateAddExtendedPropertySQL( + +table.comment, + +table.name, + +); + + + +const columnCommentsSql = table.fields + +.map((field) => + +generateAddExtendedPropertySQL(field.comment, table.name, field.name), + +) + +.join(""); + + + +const indicesSql = table.indices + +.map( + +(i) => + +`\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX [${ + +i.name + +}]\nON [${table.name}] (${i.fields + +.map((f) => `[${f}]`) + +.join(", ")});\nGO\n`, + +) + +.join(""); + + + +return `${createTableSql}${tableCommentSql}${columnCommentsSql}${indicesSql}`; + +}) + +.join("\n"); + + + +const referencesSql = generateForeignKeys ? diagram.references + +.map((r) => { + +const startTable = diagram.tables.find((t) => t.id === r.startTableId); + +const endTable = diagram.tables.find((t) => t.id === r.endTableId); + + + +if (!startTable || !endTable) return ""; + + + +const startField = startTable.fields.find((f) => f.id === r.startFieldId); + +const endField = endTable.fields.find((f) => f.id === r.endFieldId); + + + +if (!startField || !endField) return ""; + + + +return `\nALTER TABLE [${startTable.name}] + +ADD FOREIGN KEY([${startField.name}]) + +REFERENCES [${endTable.name}]([${endField.name}]) + +ON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()}; + +GO`; + +}) + +.join("") : ""; + + + +return `${tablesSql}${referencesSql ? `\n${referencesSql}` : ""}`; + +} \ No newline at end of file diff --git a/src/utils/exportSQL/mysql.js b/src/utils/exportSQL/mysql.js index f35dbda..5e658c5 100644 --- a/src/utils/exportSQL/mysql.js +++ b/src/utils/exportSQL/mysql.js @@ -1,81 +1,161 @@ -import { escapeQuotes, parseDefault } from "./shared"; - -import { dbToTypes } from "../../data/datatypes"; -import { DB } from "../../data/constants"; - -function parseType(field) { - let res = field.type; - - if (field.type === "SET" || field.type === "ENUM") { - res += `${field.values ? "(" + field.values.map((value) => "'" + value + "'").join(", ") + ")" : ""}`; - } - - if ( - dbToTypes[DB.MYSQL][field.type].isSized || - dbToTypes[DB.MYSQL][field.type].hasPrecision - ) { - res += `${field.size && field.size !== "" ? "(" + field.size + ")" : ""}`; - } - - return res; -} - -export function toMySQL(diagram) { - return `${diagram.tables - .map( - (table) => - `CREATE TABLE IF NOT EXISTS \`${table.name}\` (\n${table.fields - .map( - (field) => - `\t\`${field.name}\` ${parseType(field)}${ - dbToTypes[DB.MYSQL][field.type]?.signed && field.unsigned - ? " UNSIGNED" - : "" - }${field.notNull ? " NOT NULL" : ""}${ - field.increment ? " AUTO_INCREMENT" : "" - }${field.unique ? " UNIQUE" : ""}${ - field.default !== "" - ? ` DEFAULT ${parseDefault(field, diagram.database)}` - : "" - }${ - field.check === "" || - !dbToTypes[diagram.database][field.type].hasCheck - ? "" - : ` CHECK(${field.check})` - }${field.comment ? ` COMMENT '${escapeQuotes(field.comment)}'` : ""}`, - ) - .join(",\n")}${ - table.fields.filter((f) => f.primary).length > 0 - ? `,\n\tPRIMARY KEY(${table.fields - .filter((f) => f.primary) - .map((f) => `\`${f.name}\``) - .join(", ")})` - : "" - }\n)${table.comment ? ` COMMENT='${escapeQuotes(table.comment)}'` : ""};\n${`\n${table.indices - .map( - (i) => - `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX \`${ - i.name - }\`\nON \`${table.name}\` (${i.fields - .map((f) => `\`${f}\``) - .join(", ")});`, - ) - .join("")}`}`, - ) - .join("\n")}\n${diagram.references - .map((r) => { - const { name: startName, fields: startFields } = diagram.tables.find( - (t) => t.id === r.startTableId, - ); - - const { name: endName, fields: endFields } = diagram.tables.find( - (t) => t.id === r.endTableId, - ); - return `ALTER TABLE \`${startName}\`\nADD FOREIGN KEY(\`${ - startFields.find((f) => f.id === r.startFieldId).name - }\`) REFERENCES \`${endName}\`(\`${ - endFields.find((f) => f.id === r.endFieldId).name - }\`)\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; - }) - .join("\n")}`; -} +import { escapeQuotes, parseDefault } from "./shared"; + + + +import { dbToTypes } from "../../data/datatypes"; + +import { DB } from "../../data/constants"; + + + +function parseType(field) { + +let res = field.type; + + + +if (field.type === "SET" || field.type === "ENUM") { + +res += `${field.values ? "(" + field.values.map((value) => "'" + value + "'").join(", ") + ")" : ""}`; + +} + + + +if ( + +dbToTypes[DB.MYSQL][field.type].isSized || + +dbToTypes[DB.MYSQL][field.type].hasPrecision + +) { + +res += `${field.size && field.size !== "" ? "(" + field.size + ")" : ""}`; + +} + + + +return res; + +} + + + +export function toMySQL(diagram, generateForeignKeys = true) { + +return `${diagram.tables + +.map( + +(table) => + +`CREATE TABLE IF NOT EXISTS \`${table.name}\` (\n${table.fields + +.map( + +(field) => + +`\t\`${field.name}\` ${parseType(field)}${ + +dbToTypes[DB.MYSQL][field.type]?.signed && field.unsigned + +? " UNSIGNED" + +: "" + +}${field.notNull ? " NOT NULL" : ""}${ + +field.increment ? " AUTO_INCREMENT" : "" + +}${field.unique ? " UNIQUE" : ""}${ + +field.default !== "" + +? ` DEFAULT ${parseDefault(field, diagram.database)}` + +: "" + +}${ + +field.check === "" || + +!dbToTypes[diagram.database][field.type].hasCheck + +? "" + +: ` CHECK(${field.check})` + +}${field.comment ? ` COMMENT '${escapeQuotes(field.comment)}'` : ""}`, + +) + +.join(",\n")}${ + +table.fields.filter((f) => f.primary).length > 0 + +? `,\n\tPRIMARY KEY(${table.fields + +.filter((f) => f.primary) + +.map((f) => `\`${f.name}\``) + +.join(", ")})` + +: "" + +}\n)${table.comment ? ` COMMENT='${escapeQuotes(table.comment)}'` : ""};\n${`\n${table.indices + +.map( + +(i) => + +`\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX \`${ + +i.name + +}\`\nON \`${table.name}\` (${i.fields + +.map((f) => `\`${f}\``) + +.join(", ")});`, + +) + +.join("")}`}`, + +) + +.join("\n")}${generateForeignKeys ? `\n${diagram.references + +.map((r) => { + +const { name: startName, fields: startFields } = diagram.tables.find( + +(t) => t.id === r.startTableId, + +); + + + +const { name: endName, fields: endFields } = diagram.tables.find( + +(t) => t.id === r.endTableId, + +); + +return `ALTER TABLE \`${startName}\`\nADD FOREIGN KEY(\`${ + +startFields.find((f) => f.id === r.startFieldId).name + +}\`) REFERENCES \`${endName}\`(\`${ + +endFields.find((f) => f.id === r.endFieldId).name + +}\`)\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; + +}) + +.join("\n")}` : ""}`; + +} \ No newline at end of file diff --git a/src/utils/exportSQL/oraclesql.js b/src/utils/exportSQL/oraclesql.js index 0a44c56..cb35918 100644 --- a/src/utils/exportSQL/oraclesql.js +++ b/src/utils/exportSQL/oraclesql.js @@ -1,63 +1,125 @@ -import { dbToTypes } from "../../data/datatypes"; -import { parseDefault } from "./shared"; - -export function toOracleSQL(diagram) { - return `${diagram.tables - .map( - (table) => - `${ - table.comment === "" ? "" : `/* ${table.comment} */\n` - }CREATE TABLE "${table.name}" (\n${table.fields - .map( - (field) => - `${field.comment === "" ? "" : `\t-- ${field.comment}\n`}\t"${ - field.name - }" ${field.type}${ - field.size !== undefined && field.size !== "" - ? "(" + field.size + ")" - : "" - }${field.notNull ? " NOT NULL" : ""}${ - field.increment ? " GENERATED ALWAYS AS IDENTITY" : "" - }${field.unique ? " UNIQUE" : ""}${ - field.default !== "" - ? ` DEFAULT ${parseDefault(field, diagram.database)}` - : "" - }${ - field.check === "" || - !dbToTypes[diagram.database][field.type].hasCheck - ? "" - : ` CHECK(${field.check})` - }${field.comment ? ` -- ${field.comment}` : ""}`, - ) - .join(",\n")}${ - table.fields.filter((f) => f.primary).length > 0 - ? `,\n\tPRIMARY KEY(${table.fields - .filter((f) => f.primary) - .map((f) => `"${f.name}"`) - .join(", ")})` - : "" - }\n)${table.comment ? ` -- ${table.comment}` : ""};\n${`\n${table.indices - .map( - (i) => - `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX "${i.name}"\nON "${table.name}" (${i.fields - .map((f) => `"${f}"`) - .join(", ")});`, - ) - .join("")}`}`, - ) - .join("\n")}\n${diagram.references - .map((r) => { - const { name: startName, fields: startFields } = diagram.tables.find( - (t) => t.id === r.startTableId, - ); - const { name: endName, fields: endFields } = diagram.tables.find( - (t) => t.id === r.endTableId, - ); - return `ALTER TABLE "${startName}"\nADD CONSTRAINT "${r.name}" FOREIGN KEY ("${ - startFields.find((f) => f.id === r.startFieldId).name - }") REFERENCES "${endName}" ("${ - endFields.find((f) => f.id === r.endFieldId).name - }")\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; - }) - .join("\n")}`; -} +import { dbToTypes } from "../../data/datatypes"; + +import { parseDefault } from "./shared"; + + + +export function toOracleSQL(diagram, generateForeignKeys = true) { + +return `${diagram.tables + +.map( + +(table) => + +`${ + +table.comment === "" ? "" : `/* ${table.comment} */\n` + +}CREATE TABLE "${table.name}" (\n${table.fields + +.map( + +(field) => + +`${field.comment === "" ? "" : `\t-- ${field.comment}\n`}\t"${ + +field.name + +}" ${field.type}${ + +field.size !== undefined && field.size !== "" + +? "(" + field.size + ")" + +: "" + +}${field.notNull ? " NOT NULL" : ""}${ + +field.increment ? " GENERATED ALWAYS AS IDENTITY" : "" + +}${field.unique ? " UNIQUE" : ""}${ + +field.default !== "" + +? ` DEFAULT ${parseDefault(field, diagram.database)}` + +: "" + +}${ + +field.check === "" || + +!dbToTypes[diagram.database][field.type].hasCheck + +? "" + +: ` CHECK(${field.check})` + +}${field.comment ? ` -- ${field.comment}` : ""}`, + +) + +.join(",\n")}${ + +table.fields.filter((f) => f.primary).length > 0 + +? `,\n\tPRIMARY KEY(${table.fields + +.filter((f) => f.primary) + +.map((f) => `"${f.name}"`) + +.join(", ")})` + +: "" + +}\n)${table.comment ? ` -- ${table.comment}` : ""};\n${`\n${table.indices + +.map( + +(i) => + +`\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX "${i.name}"\nON "${table.name}" (${i.fields + +.map((f) => `"${f}"`) + +.join(", ")});`, + +) + +.join("")}`}`, + +) + +.join("\n")}${generateForeignKeys ? `\n${diagram.references + +.map((r) => { + +const { name: startName, fields: startFields } = diagram.tables.find( + +(t) => t.id === r.startTableId, + +); + +const { name: endName, fields: endFields } = diagram.tables.find( + +(t) => t.id === r.endTableId, + +); + +return `ALTER TABLE "${startName}"\nADD CONSTRAINT "${r.name}" FOREIGN KEY ("${ + +startFields.find((f) => f.id === r.startFieldId).name + +}") REFERENCES "${endName}" ("${ + +endFields.find((f) => f.id === r.endFieldId).name + +}")\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; + +}) + +.join("\n")}` : ""}`; + +} \ No newline at end of file diff --git a/src/utils/exportSQL/postgres.js b/src/utils/exportSQL/postgres.js index 3dc6c0e..b7c7561 100644 --- a/src/utils/exportSQL/postgres.js +++ b/src/utils/exportSQL/postgres.js @@ -1,114 +1,227 @@ -import { escapeQuotes, exportFieldComment, parseDefault } from "./shared"; -import { dbToTypes } from "../../data/datatypes"; - -export function toPostgres(diagram) { - const enumStatements = diagram.enums - .map( - (e) => - `CREATE TYPE "${e.name}" AS ENUM (\n${e.values - .map((v) => `\t'${v}'`) - .join(",\n")}\n);\n`, - ) - .join("\n"); - - const typeStatements = diagram.types - .map( - (type) => - `CREATE TYPE ${type.name} AS (\n${type.fields - .map((f) => `\t${f.name} ${f.type}`) - .join(",\n")}\n);\n\n${ - type.comment?.trim() - ? `COMMENT ON TYPE "${type.name}" IS '${escapeQuotes(type.comment)}';\n` - : "" - }`, - ) - .join("\n"); - - const tableStatements = diagram.tables - .map((table) => { - const inheritsClause = - Array.isArray(table.inherits) && table.inherits.length > 0 - ? `\n) INHERITS (${table.inherits.map((parent) => `"${parent}"`).join(", ")})` - : "\n)"; - - const fieldDefinitions = table.fields - .map( - (field) => - `${exportFieldComment(field.comment)}\t"${ - field.name - }" ${field.type}${ - field.size ? `(${field.size})` : "" - }${field.isArray ? " ARRAY" : ""}${field.notNull ? " NOT NULL" : ""}${ - field.unique ? " UNIQUE" : "" - }${field.increment ? " GENERATED BY DEFAULT AS IDENTITY" : ""}${ - field.default?.trim() - ? ` DEFAULT ${parseDefault(field, diagram.database)}` - : "" - }${ - field.check && dbToTypes[diagram.database][field.type]?.hasCheck - ? ` CHECK(${field.check})` - : "" - }`, - ) - .join(",\n"); - - const primaryKeyClause = table.fields.some((f) => f.primary) - ? `,\n\tPRIMARY KEY(${table.fields - .filter((f) => f.primary) - .map((f) => `"${f.name}"`) - .join(", ")})` - : ""; - - const commentStatements = [ - table.comment?.trim() - ? `COMMENT ON TABLE "${table.name}" IS '${escapeQuotes(table.comment)}';` - : "", - ...table.fields - .map((field) => - field.comment?.trim() - ? `COMMENT ON COLUMN "${table.name}"."${field.name}" IS '${escapeQuotes(field.comment)}';` - : "", - ) - .filter(Boolean), - ].join("\n"); - - const indexStatements = table.indices - .map( - (i) => - `CREATE ${i.unique ? "UNIQUE " : ""}INDEX "${i.name}"\nON "${table.name}" (${i.fields - .map((f) => `"${f}"`) - .join(", ")});`, - ) - .join("\n"); - - return `CREATE TABLE IF NOT EXISTS "${table.name}" (\n${fieldDefinitions}${primaryKeyClause}${inheritsClause};\n\n${commentStatements}\n${indexStatements}`; - }) - .join("\n\n"); - - const foreignKeyStatements = diagram.references - .map((r) => { - const startTable = diagram.tables.find((t) => t.id === r.startTableId); - const endTable = diagram.tables.find((t) => t.id === r.endTableId); - const startField = startTable?.fields.find( - (f) => f.id === r.startFieldId, - ); - const endField = endTable?.fields.find((f) => f.id === r.endFieldId); - - if (!startTable || !endTable || !startField || !endField) return ""; - - return `ALTER TABLE "${startTable.name}"\nADD FOREIGN KEY("${startField.name}") REFERENCES "${endTable.name}"("${endField.name}")\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; - }) - .filter(Boolean) - .join("\n"); - - return [ - enumStatements, - enumStatements.trim() && typeStatements - ? "\n" + typeStatements - : typeStatements, - tableStatements, - foreignKeyStatements, - ] - .filter(Boolean) - .join("\n"); -} +import { escapeQuotes, exportFieldComment, parseDefault } from "./shared"; + +import { dbToTypes } from "../../data/datatypes"; + + + +export function toPostgres(diagram, generateForeignKeys = true) { + +const enumStatements = diagram.enums + +.map( + +(e) => + +`CREATE TYPE "${e.name}" AS ENUM (\n${e.values + +.map((v) => `\t'${v}'`) + +.join(",\n")}\n);\n`, + +) + +.join("\n"); + + + +const typeStatements = diagram.types + +.map( + +(type) => + +`CREATE TYPE ${type.name} AS (\n${type.fields + +.map((f) => `\t${f.name} ${f.type}`) + +.join(",\n")}\n);\n\n${ + +type.comment?.trim() + +? `COMMENT ON TYPE "${type.name}" IS '${escapeQuotes(type.comment)}';\n` + +: "" + +}`, + +) + +.join("\n"); + + + +const tableStatements = diagram.tables + +.map((table) => { + +const inheritsClause = + +Array.isArray(table.inherits) && table.inherits.length > 0 + +? `\n) INHERITS (${table.inherits.map((parent) => `"${parent}"`).join(", ")})` + +: "\n)"; + + + +const fieldDefinitions = table.fields + +.map( + +(field) => + +`${exportFieldComment(field.comment)}\t"${ + +field.name + +}" ${field.type}${ + +field.size ? `(${field.size})` : "" + +}${field.isArray ? " ARRAY" : ""}${field.notNull ? " NOT NULL" : ""}${ + +field.unique ? " UNIQUE" : "" + +}${field.increment ? " GENERATED BY DEFAULT AS IDENTITY" : ""}${ + +field.default?.trim() + +? ` DEFAULT ${parseDefault(field, diagram.database)}` + +: "" + +}${ + +field.check && dbToTypes[diagram.database][field.type]?.hasCheck + +? ` CHECK(${field.check})` + +: "" + +}`, + +) + +.join(",\n"); + + + +const primaryKeyClause = table.fields.some((f) => f.primary) + +? `,\n\tPRIMARY KEY(${table.fields + +.filter((f) => f.primary) + +.map((f) => `"${f.name}"`) + +.join(", ")})` + +: ""; + + + +const commentStatements = [ + +table.comment?.trim() + +? `COMMENT ON TABLE "${table.name}" IS '${escapeQuotes(table.comment)}';` + +: "", + +...table.fields + +.map((field) => + +field.comment?.trim() + +? `COMMENT ON COLUMN "${table.name}"."${field.name}" IS '${escapeQuotes(field.comment)}';` + +: "", + +) + +.filter(Boolean), + +].join("\n"); + + + +const indexStatements = table.indices + +.map( + +(i) => + +`CREATE ${i.unique ? "UNIQUE " : ""}INDEX "${i.name}"\nON "${table.name}" (${i.fields + +.map((f) => `"${f}"`) + +.join(", ")});`, + +) + +.join("\n"); + + + +return `CREATE TABLE IF NOT EXISTS "${table.name}" (\n${fieldDefinitions}${primaryKeyClause}${inheritsClause};\n\n${commentStatements}\n${indexStatements}`; + +}) + +.join("\n\n"); + + + +const foreignKeyStatements = generateForeignKeys ? diagram.references + +.map((r) => { + +const startTable = diagram.tables.find((t) => t.id === r.startTableId); + +const endTable = diagram.tables.find((t) => t.id === r.endTableId); + +const startField = startTable?.fields.find( + +(f) => f.id === r.startFieldId, + +); + +const endField = endTable?.fields.find((f) => f.id === r.endFieldId); + + + +if (!startTable || !endTable || !startField || !endField) return ""; + + + +return `ALTER TABLE "${startTable.name}"\nADD FOREIGN KEY("${startField.name}") REFERENCES "${endTable.name}"("${endField.name}")\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; + +}) + +.filter(Boolean) + +.join("\n") : ""; + + + +return [ + +enumStatements, + +enumStatements.trim() && typeStatements + +? "\n" + typeStatements + +: typeStatements, + +tableStatements, + +foreignKeyStatements, + +] + +.filter(Boolean) + +.join("\n"); + +} \ No newline at end of file diff --git a/src/utils/exportSQL/shared.js b/src/utils/exportSQL/shared.js index 64100bc..e53de25 100644 --- a/src/utils/exportSQL/shared.js +++ b/src/utils/exportSQL/shared.js @@ -1,49 +1,101 @@ -import { isFunction, isKeyword } from "../utils"; - -import { DB } from "../../data/constants"; -import { dbToTypes } from "../../data/datatypes"; - -export function parseDefault(field, database = DB.GENERIC) { - if ( - isFunction(field.default) || - isKeyword(field.default) || - !dbToTypes[database][field.type].hasQuotes - ) { - return field.default; - } - - return `'${escapeQuotes(field.default)}'`; -} - -export function escapeQuotes(str) { - return str.replace(/[']/g, "'$&"); -} - -export function exportFieldComment(comment) { - if (comment === "") { - return ""; - } - - return comment - .split("\n") - .map((commentLine) => `\t-- ${commentLine}\n`) - .join(""); -} - -export function getInlineFK(table, obj) { - let fks = []; - obj.references.forEach((r) => { - if (r.startTableId === table.id) { - fks.push( - `\tFOREIGN KEY ("${table.fields.find((f) => f.id === r.startFieldId)?.name}") REFERENCES "${ - obj.tables.find((t) => t.id === r.endTableId)?.name - }"("${ - obj.tables - .find((t) => t.id === r.endTableId) - .fields.find((f) => f.id === r.endFieldId)?.name - }")\n\tON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()}`, - ); - } - }); - return fks.join(",\n"); -} +import { isFunction, isKeyword } from "../utils"; + + + +import { DB } from "../../data/constants"; + +import { dbToTypes } from "../../data/datatypes"; + + + +export function parseDefault(field, database = DB.GENERIC) { + +if ( + +isFunction(field.default) || + +isKeyword(field.default) || + +!dbToTypes[database][field.type].hasQuotes + +) { + +return field.default; + +} + + + +return `'${escapeQuotes(field.default)}'`; + +} + + + +export function escapeQuotes(str) { + +return str.replace(/[']/g, "'$&"); + +} + + + +export function exportFieldComment(comment) { + +if (comment === "") { + +return ""; + +} + + + +return comment + +.split("\n") + +.map((commentLine) => `\t-- ${commentLine}\n`) + +.join(""); + +} + + + +export function getInlineFK(table, obj, generateForeignKeys = true) { + +if (!generateForeignKeys) return ""; + + + +let fks = []; + +obj.references.forEach((r) => { + +if (r.startTableId === table.id) { + +fks.push( + +`\tFOREIGN KEY ("${table.fields.find((f) => f.id === r.startFieldId)?.name}") REFERENCES "${ + +obj.tables.find((t) => t.id === r.endTableId)?.name + +}"("${ + +obj.tables + +.find((t) => t.id === r.endTableId) + +.fields.find((f) => f.id === r.endFieldId)?.name + +}")\n\tON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()}`, + +); + +} + +}); + +return fks.join(",\n"); + +} \ No newline at end of file diff --git a/src/utils/exportSQL/sqlite.js b/src/utils/exportSQL/sqlite.js index 25ae186..99f39a5 100644 --- a/src/utils/exportSQL/sqlite.js +++ b/src/utils/exportSQL/sqlite.js @@ -1,44 +1,87 @@ -import { exportFieldComment, getInlineFK, parseDefault } from "./shared"; - -import { dbToTypes } from "../../data/datatypes"; - -export function toSqlite(diagram) { - return diagram.tables - .map((table) => { - const inlineFK = getInlineFK(table, diagram); - return `${ - table.comment === "" ? "" : `/* ${table.comment} */\n` - }CREATE TABLE IF NOT EXISTS "${table.name}" (\n${table.fields - .map( - (field) => - `${exportFieldComment(field.comment)}\t"${ - field.name - }" ${field.type}${field.notNull ? " NOT NULL" : ""}${ - field.unique ? " UNIQUE" : "" - }${field.default !== "" ? ` DEFAULT ${parseDefault(field, diagram.database)}` : ""}${ - field.check === "" || - !dbToTypes[diagram.database][field.type].hasCheck - ? "" - : ` CHECK(${field.check})` - }`, - ) - .join(",\n")}${ - table.fields.filter((f) => f.primary).length > 0 - ? `,\n\tPRIMARY KEY(${table.fields - .filter((f) => f.primary) - .map((f) => `"${f.name}"`) - .join(", ")})${inlineFK !== "" ? ",\n" : ""}` - : "" - }${inlineFK}\n);\n${table.indices - .map( - (i) => - `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS "${ - i.name - }"\nON "${table.name}" (${i.fields - .map((f) => `"${f}"`) - .join(", ")});`, - ) - .join("\n")}`; - }) - .join("\n"); -} \ No newline at end of file +import { exportFieldComment, getInlineFK, parseDefault } from "./shared"; + + + +import { dbToTypes } from "../../data/datatypes"; + + + +export function toSqlite(diagram, generateForeignKeys = true) { + +return diagram.tables + +.map((table) => { + +const inlineFK = getInlineFK(table, diagram, generateForeignKeys); + +return `${ + +table.comment === "" ? "" : `/* ${table.comment} */\n` + +}CREATE TABLE IF NOT EXISTS "${table.name}" (\n${table.fields + +.map( + +(field) => + +`${exportFieldComment(field.comment)}\t"${ + +field.name + +}" ${field.type}${field.notNull ? " NOT NULL" : ""}${ + +field.unique ? " UNIQUE" : "" + +}${field.default !== "" ? ` DEFAULT ${parseDefault(field, diagram.database)}` : ""}${ + +field.check === "" || + +!dbToTypes[diagram.database][field.type].hasCheck + +? "" + +: ` CHECK(${field.check})` + +}`, + +) + +.join(",\n")}${ + +table.fields.filter((f) => f.primary).length > 0 + +? `,\n\tPRIMARY KEY(${table.fields + +.filter((f) => f.primary) + +.map((f) => `"${f.name}"`) + +.join(", ")})${inlineFK !== "" ? ",\n" : ""}` + +: "" + +}${inlineFK}\n);\n${table.indices + +.map( + +(i) => + +`\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS "${ + +i.name + +}"\nON "${table.name}" (${i.fields + +.map((f) => `"${f}"`) + +.join(", ")});`, + +) + +.join("\n")}`; + +}) + +.join("\n"); + +} \ No newline at end of file