2023-09-19 20:47:06 +08:00
|
|
|
import React, { useRef, useState } from "react";
|
2023-09-19 20:46:58 +08:00
|
|
|
import { useDrop } from "react-dnd";
|
2023-09-19 20:47:43 +08:00
|
|
|
import Table from "./table";
|
2023-09-19 20:48:01 +08:00
|
|
|
import { defaultTableTheme, Cardinality, Constraint } from "../data/data";
|
2023-09-19 20:48:04 +08:00
|
|
|
import Area from "./area";
|
2023-09-19 20:47:34 +08:00
|
|
|
|
2023-09-19 20:47:06 +08:00
|
|
|
export default function Canvas(props) {
|
2023-09-19 20:48:04 +08:00
|
|
|
const ObjectType = {
|
|
|
|
NONE: 0,
|
|
|
|
TABLE: 1,
|
|
|
|
AREA: 2,
|
|
|
|
};
|
|
|
|
const [dragging, setDragging] = useState([ObjectType.NONE, -1]);
|
2023-09-19 20:47:35 +08:00
|
|
|
const [linking, setLinking] = useState(false);
|
|
|
|
const [line, setLine] = useState({
|
2023-09-19 20:47:48 +08:00
|
|
|
startTableId: -1,
|
|
|
|
startFieldId: -1,
|
|
|
|
endTableId: -1,
|
|
|
|
endFieldId: -1,
|
2023-09-19 20:47:35 +08:00
|
|
|
startX: 0,
|
|
|
|
startY: 0,
|
|
|
|
endX: 0,
|
|
|
|
endY: 0,
|
2023-09-19 20:47:59 +08:00
|
|
|
name: "",
|
2023-09-19 20:48:01 +08:00
|
|
|
cardinality: Cardinality.ONE_TO_MANY,
|
2023-09-19 20:47:59 +08:00
|
|
|
updateConstraint: Constraint.none,
|
|
|
|
deleteConstraint: Constraint.none,
|
|
|
|
mandatory: false,
|
2023-09-19 20:47:35 +08:00
|
|
|
});
|
2023-09-19 20:47:06 +08:00
|
|
|
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
2023-09-19 20:47:45 +08:00
|
|
|
const [onRect, setOnRect] = useState({
|
|
|
|
tableId: -1,
|
|
|
|
field: -2,
|
|
|
|
});
|
2023-09-19 20:47:13 +08:00
|
|
|
const [panning, setPanning] = useState(false);
|
|
|
|
const [panOffset, setPanOffset] = useState({ x: 0, y: 0 });
|
|
|
|
const [cursor, setCursor] = useState("default");
|
2023-09-19 20:46:43 +08:00
|
|
|
|
|
|
|
const canvas = useRef(null);
|
|
|
|
|
2023-09-19 20:48:04 +08:00
|
|
|
const handleMouseDownRect = (e, id, type) => {
|
2023-09-19 20:47:13 +08:00
|
|
|
const { clientX, clientY } = e;
|
2023-09-19 20:48:04 +08:00
|
|
|
if (type === ObjectType.TABLE) {
|
|
|
|
const table = props.tables.find((t) => t.id === id);
|
|
|
|
setOffset({
|
|
|
|
x: clientX - table.x,
|
|
|
|
y: clientY - table.y,
|
|
|
|
});
|
|
|
|
setDragging([ObjectType.TABLE, id]);
|
|
|
|
} else if (type === ObjectType.AREA) {
|
|
|
|
const area = props.areas.find((t) => t.id === id);
|
|
|
|
setOffset({
|
|
|
|
x: clientX - area.x,
|
|
|
|
y: clientY - area.y,
|
|
|
|
});
|
|
|
|
setDragging([ObjectType.AREA, id]);
|
|
|
|
}
|
2023-09-19 20:47:06 +08:00
|
|
|
};
|
|
|
|
|
2023-09-19 20:47:13 +08:00
|
|
|
const handleMouseMove = (e) => {
|
2023-09-19 20:47:35 +08:00
|
|
|
if (linking) {
|
|
|
|
const rect = canvas.current.getBoundingClientRect();
|
|
|
|
const offsetX = rect.left;
|
|
|
|
const offsetY = rect.top;
|
|
|
|
|
|
|
|
setLine({
|
|
|
|
...line,
|
|
|
|
endX: e.clientX - offsetX,
|
|
|
|
endY: e.clientY - offsetY,
|
|
|
|
});
|
2023-09-19 20:48:05 +08:00
|
|
|
} else if (dragging[0] === ObjectType.NONE && panning) {
|
2023-09-19 20:47:13 +08:00
|
|
|
const dx = e.clientX - panOffset.x;
|
|
|
|
const dy = e.clientY - panOffset.y;
|
|
|
|
setPanOffset({ x: e.clientX, y: e.clientY });
|
|
|
|
|
2023-09-19 20:47:47 +08:00
|
|
|
props.setTables(
|
|
|
|
props.tables.map((t) => ({
|
|
|
|
...t,
|
|
|
|
x: t.x + dx,
|
|
|
|
y: t.y + dy,
|
|
|
|
}))
|
|
|
|
);
|
|
|
|
|
2023-09-19 20:47:59 +08:00
|
|
|
props.setRelationships(
|
|
|
|
props.relationships.map((r) => ({
|
2023-09-19 20:47:48 +08:00
|
|
|
...r,
|
2023-09-19 20:47:47 +08:00
|
|
|
startX: r.startX + dx,
|
|
|
|
startY: r.startY + dy,
|
|
|
|
endX: r.endX + dx,
|
|
|
|
endY: r.endY + dy,
|
|
|
|
}))
|
|
|
|
);
|
2023-09-19 20:48:04 +08:00
|
|
|
|
|
|
|
props.setAreas(
|
|
|
|
props.areas.map((t) => ({
|
|
|
|
...t,
|
|
|
|
x: t.x + dx,
|
|
|
|
y: t.y + dy,
|
|
|
|
}))
|
|
|
|
);
|
|
|
|
} else if (dragging[0] === ObjectType.TABLE && dragging[1] >= 0) {
|
2023-09-19 20:47:41 +08:00
|
|
|
const updatedTables = props.tables.map((t) => {
|
2023-09-19 20:48:04 +08:00
|
|
|
if (t.id === dragging[1]) {
|
2023-09-19 20:47:41 +08:00
|
|
|
const updatedTable = {
|
|
|
|
...t,
|
|
|
|
x: e.clientX - offset.x,
|
|
|
|
y: e.clientY - offset.y,
|
2023-09-19 20:47:13 +08:00
|
|
|
};
|
2023-09-19 20:47:41 +08:00
|
|
|
return updatedTable;
|
2023-09-19 20:47:13 +08:00
|
|
|
}
|
2023-09-19 20:47:41 +08:00
|
|
|
return t;
|
2023-09-19 20:47:13 +08:00
|
|
|
});
|
2023-09-19 20:47:41 +08:00
|
|
|
props.setTables(updatedTables);
|
2023-09-19 20:47:59 +08:00
|
|
|
const updatedRelationShips = props.relationships.map((r) => {
|
2023-09-19 20:48:04 +08:00
|
|
|
if (r.startTableId === dragging[1]) {
|
2023-09-19 20:47:48 +08:00
|
|
|
return {
|
|
|
|
...r,
|
|
|
|
startX: props.tables[r.startTableId].x + 15,
|
|
|
|
startY:
|
|
|
|
props.tables[r.startTableId].y + r.startFieldId * 36 + 40 + 19,
|
|
|
|
};
|
2023-09-19 20:48:04 +08:00
|
|
|
} else if (r.endTableId === dragging[1]) {
|
2023-09-19 20:47:48 +08:00
|
|
|
return {
|
|
|
|
...r,
|
|
|
|
endX: props.tables[r.endTableId].x + 15,
|
|
|
|
endY: props.tables[r.endTableId].y + r.endFieldId * 36 + 40 + 19,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return r;
|
|
|
|
});
|
2023-09-19 20:47:59 +08:00
|
|
|
props.setRelationships(updatedRelationShips);
|
2023-09-19 20:48:04 +08:00
|
|
|
} else if (dragging[0] === ObjectType.AREA && dragging[1] >= 0) {
|
|
|
|
const updatedAreas = props.areas.map((t) => {
|
|
|
|
if (t.id === dragging[1]) {
|
|
|
|
const updatedArea = {
|
|
|
|
...t,
|
|
|
|
x: e.clientX - offset.x,
|
|
|
|
y: e.clientY - offset.y,
|
|
|
|
};
|
|
|
|
return updatedArea;
|
|
|
|
}
|
|
|
|
return t;
|
|
|
|
});
|
|
|
|
props.setAreas(updatedAreas);
|
2023-09-19 20:47:13 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleMouseDown = (e) => {
|
2023-09-19 20:48:05 +08:00
|
|
|
setPanning(true);
|
|
|
|
setPanOffset({ x: e.clientX, y: e.clientY });
|
|
|
|
setCursor("grabbing");
|
2023-09-19 20:47:06 +08:00
|
|
|
};
|
2023-09-19 20:46:43 +08:00
|
|
|
|
2023-09-19 20:47:06 +08:00
|
|
|
const handleMouseUp = () => {
|
2023-09-19 20:48:04 +08:00
|
|
|
setDragging([ObjectType.NONE, -1]);
|
2023-09-19 20:47:13 +08:00
|
|
|
setPanning(false);
|
|
|
|
setCursor("default");
|
2023-09-19 20:47:45 +08:00
|
|
|
if (linking) handleLinking();
|
2023-09-19 20:47:35 +08:00
|
|
|
setLinking(false);
|
2023-09-19 20:47:06 +08:00
|
|
|
};
|
|
|
|
|
2023-09-19 20:47:35 +08:00
|
|
|
const handleGripField = (id) => {
|
|
|
|
setPanning(false);
|
2023-09-19 20:48:04 +08:00
|
|
|
setDragging([ObjectType.NONE, -1]);
|
2023-09-19 20:47:35 +08:00
|
|
|
setLinking(true);
|
|
|
|
};
|
|
|
|
|
2023-09-19 20:47:45 +08:00
|
|
|
const handleLinking = () => {
|
|
|
|
if (onRect.tableId < 0) return;
|
|
|
|
if (onRect.field < 0) return;
|
2023-09-19 20:47:59 +08:00
|
|
|
if (
|
|
|
|
line.startTableId === onRect.tableId &&
|
|
|
|
line.startFieldId === onRect.field
|
|
|
|
)
|
|
|
|
return;
|
|
|
|
props.setRelationships((prev) => [
|
2023-09-19 20:47:45 +08:00
|
|
|
...prev,
|
|
|
|
{
|
2023-09-19 20:47:48 +08:00
|
|
|
...line,
|
|
|
|
endTableId: onRect.tableId,
|
|
|
|
endFieldId: onRect.field,
|
2023-09-19 20:47:59 +08:00
|
|
|
endX: props.tables[onRect.tableId].x + 15,
|
|
|
|
endY: props.tables[onRect.tableId].y + onRect.field * 36 + 40 + 19,
|
|
|
|
name: `${props.tables[line.startTableId].name}_to_${
|
|
|
|
props.tables[onRect.tableId].name
|
|
|
|
}`,
|
2023-09-19 20:47:45 +08:00
|
|
|
},
|
|
|
|
]);
|
|
|
|
};
|
2023-09-19 20:47:41 +08:00
|
|
|
|
2023-09-19 20:47:06 +08:00
|
|
|
const [, drop] = useDrop(
|
|
|
|
() => ({
|
|
|
|
accept: "CARD",
|
|
|
|
drop: (item, monitor) => {
|
|
|
|
const offset = monitor.getClientOffset();
|
|
|
|
const canvasRect = canvas.current.getBoundingClientRect();
|
|
|
|
const x = offset.x - canvasRect.left - 100 * 0.5;
|
|
|
|
const y = offset.y - canvasRect.top - 100 * 0.5;
|
2023-09-19 20:47:41 +08:00
|
|
|
const newTable = {
|
2023-09-19 20:47:54 +08:00
|
|
|
id: props.tables.length,
|
2023-09-19 20:47:59 +08:00
|
|
|
name: `table_${props.tables.length}`,
|
2023-09-19 20:47:41 +08:00
|
|
|
x: x,
|
|
|
|
y: y,
|
|
|
|
fields: [
|
|
|
|
{
|
|
|
|
name: "id",
|
|
|
|
type: "UUID",
|
2023-09-19 20:47:56 +08:00
|
|
|
check: "",
|
2023-09-19 20:47:41 +08:00
|
|
|
default: "",
|
|
|
|
primary: true,
|
|
|
|
unique: true,
|
|
|
|
notNull: true,
|
|
|
|
increment: true,
|
2023-09-19 20:47:52 +08:00
|
|
|
comment: "",
|
2023-09-19 20:47:41 +08:00
|
|
|
},
|
|
|
|
],
|
2023-09-19 20:47:51 +08:00
|
|
|
comment: "",
|
|
|
|
indices: [],
|
2023-09-19 20:47:54 +08:00
|
|
|
color: defaultTableTheme,
|
2023-09-19 20:47:06 +08:00
|
|
|
};
|
2023-09-19 20:47:41 +08:00
|
|
|
props.setTables((prev) => [...prev, newTable]);
|
2023-09-19 20:47:10 +08:00
|
|
|
props.setCode((prev) =>
|
|
|
|
prev === ""
|
2023-09-19 20:47:41 +08:00
|
|
|
? `CREATE TABLE \`${newTable.name}\`;`
|
|
|
|
: `${prev}\n\nCREATE TABLE \`${newTable.name}\`;`
|
2023-09-19 20:47:10 +08:00
|
|
|
);
|
2023-09-19 20:46:43 +08:00
|
|
|
},
|
2023-09-19 20:47:06 +08:00
|
|
|
collect: (monitor) => ({
|
|
|
|
isOver: !!monitor.isOver(),
|
|
|
|
}),
|
|
|
|
}),
|
2023-09-19 20:47:41 +08:00
|
|
|
[props.tables]
|
2023-09-19 20:47:06 +08:00
|
|
|
);
|
2023-09-19 20:47:59 +08:00
|
|
|
|
2023-09-19 20:46:58 +08:00
|
|
|
return (
|
2023-09-19 20:47:06 +08:00
|
|
|
<div ref={drop} className="flex-grow" id="canvas">
|
|
|
|
<div ref={canvas} className="w-full h-screen">
|
|
|
|
<svg
|
|
|
|
onMouseMove={handleMouseMove}
|
2023-09-19 20:47:13 +08:00
|
|
|
onMouseDown={handleMouseDown}
|
2023-09-19 20:47:06 +08:00
|
|
|
onMouseUp={handleMouseUp}
|
2023-09-19 20:47:13 +08:00
|
|
|
style={{ width: "100%", height: "100%", cursor: cursor }}
|
2023-09-19 20:47:06 +08:00
|
|
|
>
|
2023-09-19 20:47:14 +08:00
|
|
|
<defs>
|
|
|
|
<pattern
|
|
|
|
id="smallGrid"
|
|
|
|
width="10"
|
|
|
|
height="10"
|
|
|
|
patternUnits="userSpaceOnUse"
|
|
|
|
>
|
|
|
|
<path
|
|
|
|
d="M 10 0 L 0 0 0 10"
|
|
|
|
fill="none"
|
|
|
|
stroke="lightblue"
|
|
|
|
strokeWidth="0.5"
|
|
|
|
/>
|
|
|
|
</pattern>
|
|
|
|
<pattern
|
|
|
|
id="grid"
|
|
|
|
width="100"
|
|
|
|
height="100"
|
|
|
|
patternUnits="userSpaceOnUse"
|
|
|
|
>
|
|
|
|
<rect width="100" height="100" fill="url(#smallGrid)" />
|
|
|
|
<path
|
|
|
|
d="M 100 0 L 0 0 0 100"
|
|
|
|
fill="none"
|
|
|
|
stroke="lightblue"
|
|
|
|
strokeWidth="1"
|
|
|
|
/>
|
|
|
|
</pattern>
|
|
|
|
</defs>
|
|
|
|
|
|
|
|
<rect width="100%" height="100%" fill="url(#grid)" />
|
2023-09-19 20:48:04 +08:00
|
|
|
{props.areas.map((a) => (
|
|
|
|
<Area
|
|
|
|
key={a.id}
|
|
|
|
areaData={a}
|
|
|
|
onMouseDown={(e) => handleMouseDownRect(e, a.id, ObjectType.AREA)}
|
2023-09-19 20:48:06 +08:00
|
|
|
setPanning={setPanning}
|
|
|
|
setAreas={props.setAreas}
|
2023-09-19 20:48:04 +08:00
|
|
|
></Area>
|
|
|
|
))}
|
2023-09-19 20:47:41 +08:00
|
|
|
{props.tables.map((table, i) => (
|
2023-09-19 20:47:43 +08:00
|
|
|
<Table
|
2023-09-19 20:47:41 +08:00
|
|
|
key={table.id}
|
2023-09-19 20:47:54 +08:00
|
|
|
id={table.id}
|
2023-09-19 20:47:41 +08:00
|
|
|
tableData={table}
|
|
|
|
tables={props.tables}
|
|
|
|
setTables={props.setTables}
|
2023-09-19 20:47:13 +08:00
|
|
|
setOnRect={setOnRect}
|
2023-09-19 20:47:35 +08:00
|
|
|
handleGripField={handleGripField}
|
|
|
|
setLine={setLine}
|
2023-09-19 20:48:04 +08:00
|
|
|
onMouseDown={(e) =>
|
|
|
|
handleMouseDownRect(e, table.id, ObjectType.TABLE)
|
|
|
|
}
|
2023-09-19 20:47:06 +08:00
|
|
|
/>
|
|
|
|
))}
|
2023-09-19 20:47:35 +08:00
|
|
|
{linking && (
|
|
|
|
<line
|
|
|
|
x1={line.startX}
|
|
|
|
y1={line.startY}
|
|
|
|
x2={line.endX}
|
|
|
|
y2={line.endY}
|
|
|
|
stroke="red"
|
|
|
|
strokeDasharray="5,5"
|
|
|
|
/>
|
|
|
|
)}
|
2023-09-19 20:47:59 +08:00
|
|
|
{props.relationships.map((e, i) => (
|
2023-09-19 20:47:45 +08:00
|
|
|
<line
|
2023-09-19 20:47:48 +08:00
|
|
|
key={i}
|
2023-09-19 20:47:45 +08:00
|
|
|
x1={e.startX}
|
|
|
|
y1={e.startY}
|
|
|
|
x2={e.endX}
|
|
|
|
y2={e.endY}
|
|
|
|
stroke="gray"
|
2023-09-19 20:47:59 +08:00
|
|
|
strokeWidth={2.5}
|
|
|
|
onClick={() => {}}
|
2023-09-19 20:47:45 +08:00
|
|
|
/>
|
|
|
|
))}
|
2023-09-19 20:47:06 +08:00
|
|
|
</svg>
|
|
|
|
</div>
|
2023-09-19 20:46:58 +08:00
|
|
|
</div>
|
|
|
|
);
|
2023-09-19 20:46:43 +08:00
|
|
|
}
|