This commit is contained in:
1ilit 2024-03-16 02:23:10 +02:00
parent 7a406eb6d3
commit 5895d4c96e
23 changed files with 2183 additions and 2306 deletions

View File

@ -19,240 +19,109 @@ import useUndoRedo from "../hooks/useUndoRedo";
import useSelect from "../hooks/useSelect"; import useSelect from "../hooks/useSelect";
import useAreas from "../hooks/useAreas"; import useAreas from "../hooks/useAreas";
import useSaveState from "../hooks/useSaveState"; import useSaveState from "../hooks/useSaveState";
import useTransform from "../hooks/useTransform";
export default function Area(props) { export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
const [editField, setEditField] = useState({});
const { layout } = useLayout(); const { layout } = useLayout();
const { settings } = useSettings(); const { settings } = useSettings();
const { transform } = useTransform();
const { setSaveState } = useSaveState(); const { setSaveState } = useSaveState();
const { updateArea, deleteArea } = useAreas();
const { setUndoStack, setRedoStack } = useUndoRedo();
const { selectedElement, setSelectedElement } = useSelect(); const { selectedElement, setSelectedElement } = useSelect();
const handleMouseDown = (e, dir) => { const handleResize = (e, dir) => {
props.setResize({ id: props.areaData.id, dir: dir }); setResize({ id: data.id, dir: dir });
props.setInitCoords({ setInitCoords({
x: props.areaData.x, x: data.x,
y: props.areaData.y, y: data.y,
width: props.areaData.width, width: data.width,
height: props.areaData.height, height: data.height,
mouseX: e.clientX / props.zoom, mouseX: e.clientX / transform.zoom,
mouseY: e.clientY / props.zoom, mouseY: e.clientY / transform.zoom,
}); });
}; };
const edit = () => {
if (layout.sidebar) {
setSelectedElement((prev) => ({
...prev,
element: ObjectType.AREA,
id: data.id,
currentTab: Tab.AREAS,
open: true,
}));
if (selectedElement.currentTab !== Tab.AREAS) return;
document
.getElementById(`scroll_area_${data.id}`)
.scrollIntoView({ behavior: "smooth" });
} else {
setSelectedElement((prev) => ({
...prev,
element: ObjectType.AREA,
id: data.id,
open: true,
}));
}
};
const onClickOutSide = () => {
if (selectedElement.editFromToolbar) {
setSelectedElement((prev) => ({
...prev,
editFromToolbar: false,
}));
return;
}
setSelectedElement((prev) => ({
...prev,
open: false,
}));
setSaveState(State.SAVING);
};
const areaIsSelected = () =>
selectedElement.element === ObjectType.AREA &&
selectedElement.id === data.id &&
selectedElement.open;
return ( return (
<g <g
onMouseEnter={() => setHovered(true)} onMouseEnter={() => setHovered(true)}
onMouseLeave={() => { onMouseLeave={() => setHovered(false)}
setHovered(false);
}}
> >
<foreignObject <foreignObject
key={props.areaData.id} key={data.id}
x={props.areaData.x} x={data.x}
y={props.areaData.y} y={data.y}
width={props.areaData.width > 0 ? props.areaData.width : 0} width={data.width > 0 ? data.width : 0}
height={props.areaData.height > 0 ? props.areaData.height : 0} height={data.height > 0 ? data.height : 0}
onMouseDown={props.onMouseDown} onMouseDown={onMouseDown}
> >
<div <div
className={`border-2 ${ className={`border-2 ${
hovered hovered
? "border-dashed border-blue-500" ? "border-dashed border-blue-500"
: selectedElement.element === ObjectType.AREA && : selectedElement.element === ObjectType.AREA &&
selectedElement.id === props.areaData.id selectedElement.id === data.id
? "border-blue-500" ? "border-blue-500"
: "border-slate-400" : "border-slate-400"
} w-full h-full cursor-move rounded relative`} } w-full h-full cursor-move rounded relative`}
> >
<div <div
className="opacity-40 w-fill p-2 h-full" className="opacity-40 w-fill p-2 h-full"
style={{ backgroundColor: props.areaData.color }} style={{ backgroundColor: data.color }}
/> />
</div> </div>
<div className="text-color absolute top-2 left-3 select-none"> <div className="text-color absolute top-2 left-3 select-none">
{props.areaData.name} {data.name}
</div> </div>
{(hovered || {(hovered || (areaIsSelected() && !layout.sidebar)) && (
(selectedElement.element === ObjectType.AREA &&
selectedElement.id === props.areaData.id &&
selectedElement.open &&
!layout.sidebar)) && (
<div className="absolute top-2 right-3"> <div className="absolute top-2 right-3">
<Popover <Popover
visible={ visible={areaIsSelected() && !layout.sidebar}
selectedElement.element === ObjectType.AREA && onClickOutSide={onClickOutSide}
selectedElement.id === props.areaData.id &&
selectedElement.open &&
!layout.sidebar
}
onClickOutSide={() => {
if (selectedElement.editFromToolbar) {
setSelectedElement((prev) => ({
...prev,
editFromToolbar: false,
}));
return;
}
setSelectedElement((prev) => ({
...prev,
open: false,
}));
setSaveState(State.SAVING);
}}
stopPropagation stopPropagation
content={ content={<EditPopoverContent data={data} />}
<div className="popover-theme">
<div className="font-semibold mb-2 ms-1">
Edit subject area
</div>
<div className="w-[280px] flex items-center mb-2">
<Input
value={props.areaData.name}
placeholder="Name"
className="me-2"
onChange={(value) =>
updateArea(props.areaData.id, { name: value })
}
onFocus={(e) => setEditField({ name: e.target.value })}
onBlur={(e) => {
if (e.target.value === editField.name) return;
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.AREA,
aid: props.areaData.id,
undo: editField,
redo: { name: e.target.value },
message: `Edit area name to ${e.target.value}`,
},
]);
setRedoStack([]);
}}
/>
<Popover
content={
<div className="popover-theme">
<div className="flex justify-between items-center p-2">
<div className="font-medium">Theme</div>
<Button
type="tertiary"
size="small"
onClick={() => {
updateArea(props.areaData.id, {
color: defaultBlue,
});
setSaveState(State.SAVING);
}}
>
Clear
</Button>
</div>
<hr />
<div className="py-3">
<div>
{tableThemes
.slice(0, Math.ceil(tableThemes.length / 2))
.map((c) => (
<button
key={c}
style={{ backgroundColor: c }}
className="p-3 rounded-full mx-1"
onClick={() => {
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.AREA,
aid: props.areaData.id,
undo: { color: props.areaData.color },
redo: { color: c },
message: `Edit area color to ${c}`,
},
]);
setRedoStack([]);
updateArea(props.areaData.id, {
color: c,
});
}}
>
{props.areaData.color === c ? (
<IconCheckboxTick
style={{ color: "white" }}
/>
) : (
<IconCheckboxTick style={{ color: c }} />
)}
</button>
))}
</div>
<div className="mt-3">
{tableThemes
.slice(Math.ceil(tableThemes.length / 2))
.map((c) => (
<button
key={c}
style={{ backgroundColor: c }}
className="p-3 rounded-full mx-1"
onClick={() => {
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.AREA,
aid: props.areaData.id,
undo: { color: props.areaData.color },
redo: { color: c },
message: `Edit area color to ${c}`,
},
]);
setRedoStack([]);
updateArea(props.areaData.id, {
color: c,
});
}}
>
<IconCheckboxTick
style={{
color:
props.areaData.color === c
? "white"
: c,
}}
/>
</button>
))}
</div>
</div>
</div>
}
position="rightTop"
showArrow
>
<div
className="h-[32px] w-[32px] rounded"
style={{ backgroundColor: props.areaData.color }}
/>
</Popover>
</div>
<div className="flex">
<Button
icon={<IconDeleteStroked />}
type="danger"
block
onClick={() => {
Toast.success(`Area deleted!`);
deleteArea(props.areaData.id, true);
}}
>
Delete
</Button>
</div>
</div>
}
trigger="custom" trigger="custom"
position="rightTop" position="rightTop"
showArrow showArrow
@ -265,29 +134,8 @@ export default function Area(props) {
backgroundColor: "#2f68ad", backgroundColor: "#2f68ad",
opacity: "0.7", opacity: "0.7",
}} }}
onClick={() => { onClick={edit}
if (layout.sidebar) { />
setSelectedElement((prev) => ({
...prev,
element: ObjectType.AREA,
id: props.areaData.id,
currentTab: Tab.AREAS,
open: true,
}));
if (selectedElement.currentTab !== Tab.AREAS) return;
document
.getElementById(`scroll_area_${props.areaData.id}`)
.scrollIntoView({ behavior: "smooth" });
} else {
setSelectedElement((prev) => ({
...prev,
element: ObjectType.AREA,
id: props.areaData.id,
open: true,
}));
}
}}
></Button>
</Popover> </Popover>
</div> </div>
)} )}
@ -295,47 +143,196 @@ export default function Area(props) {
{hovered && ( {hovered && (
<> <>
<circle <circle
cx={props.areaData.x} cx={data.x}
cy={props.areaData.y} cy={data.y}
r={6} r={6}
fill={settings.mode === "light" ? "white" : "rgb(28, 31, 35)"} fill={settings.mode === "light" ? "white" : "rgb(28, 31, 35)"}
stroke="#5891db" stroke="#5891db"
strokeWidth={2} strokeWidth={2}
cursor="nwse-resize" cursor="nwse-resize"
onMouseDown={(e) => handleMouseDown(e, "tl")} onMouseDown={(e) => handleResize(e, "tl")}
/> />
<circle <circle
cx={props.areaData.x + props.areaData.width} cx={data.x + data.width}
cy={props.areaData.y} cy={data.y}
r={6} r={6}
fill={settings.mode === "light" ? "white" : "rgb(28, 31, 35)"} fill={settings.mode === "light" ? "white" : "rgb(28, 31, 35)"}
stroke="#5891db" stroke="#5891db"
strokeWidth={2} strokeWidth={2}
cursor="nesw-resize" cursor="nesw-resize"
onMouseDown={(e) => handleMouseDown(e, "tr")} onMouseDown={(e) => handleResize(e, "tr")}
/> />
<circle <circle
cx={props.areaData.x} cx={data.x}
cy={props.areaData.y + props.areaData.height} cy={data.y + data.height}
r={6} r={6}
fill={settings.mode === "light" ? "white" : "rgb(28, 31, 35)"} fill={settings.mode === "light" ? "white" : "rgb(28, 31, 35)"}
stroke="#5891db" stroke="#5891db"
strokeWidth={2} strokeWidth={2}
cursor="nesw-resize" cursor="nesw-resize"
onMouseDown={(e) => handleMouseDown(e, "bl")} onMouseDown={(e) => handleResize(e, "bl")}
/> />
<circle <circle
cx={props.areaData.x + props.areaData.width} cx={data.x + data.width}
cy={props.areaData.y + props.areaData.height} cy={data.y + data.height}
r={6} r={6}
fill={settings.mode === "light" ? "white" : "rgb(28, 31, 35)"} fill={settings.mode === "light" ? "white" : "rgb(28, 31, 35)"}
stroke="#5891db" stroke="#5891db"
strokeWidth={2} strokeWidth={2}
cursor="nwse-resize" cursor="nwse-resize"
onMouseDown={(e) => handleMouseDown(e, "br")} onMouseDown={(e) => handleResize(e, "br")}
/> />
</> </>
)} )}
</g> </g>
); );
} }
function EditPopoverContent({ data }) {
const [editField, setEditField] = useState({});
const { setSaveState } = useSaveState();
const { updateArea, deleteArea } = useAreas();
const { setUndoStack, setRedoStack } = useUndoRedo();
return (
<div className="popover-theme">
<div className="font-semibold mb-2 ms-1">Edit subject area</div>
<div className="w-[280px] flex items-center mb-2">
<Input
value={data.name}
placeholder="Name"
className="me-2"
onChange={(value) => updateArea(data.id, { name: value })}
onFocus={(e) => setEditField({ name: e.target.value })}
onBlur={(e) => {
if (e.target.value === editField.name) return;
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.AREA,
aid: data.id,
undo: editField,
redo: { name: e.target.value },
message: `Edit area name to ${e.target.value}`,
},
]);
setRedoStack([]);
}}
/>
<Popover
content={
<div className="popover-theme">
<div className="flex justify-between items-center p-2">
<div className="font-medium">Theme</div>
<Button
type="tertiary"
size="small"
onClick={() => {
updateArea(data.id, {
color: defaultBlue,
});
setSaveState(State.SAVING);
}}
>
Clear
</Button>
</div>
<hr />
<div className="py-3">
<div>
{tableThemes
.slice(0, Math.ceil(tableThemes.length / 2))
.map((c) => (
<button
key={c}
style={{ backgroundColor: c }}
className="p-3 rounded-full mx-1"
onClick={() => {
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.AREA,
aid: data.id,
undo: { color: data.color },
redo: { color: c },
message: `Edit area color to ${c}`,
},
]);
setRedoStack([]);
updateArea(data.id, {
color: c,
});
}}
>
{data.color === c ? (
<IconCheckboxTick style={{ color: "white" }} />
) : (
<IconCheckboxTick style={{ color: c }} />
)}
</button>
))}
</div>
<div className="mt-3">
{tableThemes
.slice(Math.ceil(tableThemes.length / 2))
.map((c) => (
<button
key={c}
style={{ backgroundColor: c }}
className="p-3 rounded-full mx-1"
onClick={() => {
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.AREA,
aid: data.id,
undo: { color: data.color },
redo: { color: c },
message: `Edit area color to ${c}`,
},
]);
setRedoStack([]);
updateArea(data.id, {
color: c,
});
}}
>
<IconCheckboxTick
style={{
color: data.color === c ? "white" : c,
}}
/>
</button>
))}
</div>
</div>
</div>
}
position="rightTop"
showArrow
>
<div
className="h-[32px] w-[32px] rounded"
style={{ backgroundColor: data.color }}
/>
</Popover>
</div>
<div className="flex">
<Button
icon={<IconDeleteStroked />}
type="danger"
block
onClick={() => {
Toast.success(`Area deleted!`);
deleteArea(data.id, true);
}}
>
Delete
</Button>
</div>
</div>
);
}

View File

@ -1,6 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { import {
Empty,
Row, Row,
Col, Col,
AutoComplete, AutoComplete,
@ -9,10 +8,6 @@ import {
Popover, Popover,
Toast, Toast,
} from "@douyinfe/semi-ui"; } from "@douyinfe/semi-ui";
import {
IllustrationNoContent,
IllustrationNoContentDark,
} from "@douyinfe/semi-illustrations";
import { import {
IconPlus, IconPlus,
IconSearch, IconSearch,
@ -29,26 +24,21 @@ import {
import useUndoRedo from "../hooks/useUndoRedo"; import useUndoRedo from "../hooks/useUndoRedo";
import useAreas from "../hooks/useAreas"; import useAreas from "../hooks/useAreas";
import useSaveState from "../hooks/useSaveState"; import useSaveState from "../hooks/useSaveState";
import NoElements from "./NoElements";
export default function AreaOverview() { export default function AreasOverview() {
const { setSaveState } = useSaveState(); const { setSaveState } = useSaveState();
const { areas, addArea, deleteArea, updateArea } = useAreas(); const { areas, addArea, deleteArea, updateArea } = useAreas();
const { setUndoStack, setRedoStack } = useUndoRedo(); const { setUndoStack, setRedoStack } = useUndoRedo();
const [editField, setEditField] = useState({}); const [editField, setEditField] = useState({});
const [value, setValue] = useState(""); const [searchText, setSearchText] = useState("");
const [filteredResult, setFilteredResult] = useState( const [filteredResult, setFilteredResult] = useState(
areas.map((t) => { areas.map((t) => t.name)
return t.name;
})
); );
const handleStringSearch = (value) => { const handleStringSearch = (value) => {
setFilteredResult( setFilteredResult(
areas areas.map((t) => t.name).filter((i) => i.includes(value))
.map((t) => {
return t.name;
})
.filter((i) => i.includes(value))
); );
}; };
@ -58,7 +48,7 @@ export default function AreaOverview() {
<Col span={16}> <Col span={16}>
<AutoComplete <AutoComplete
data={filteredResult} data={filteredResult}
value={value} value={searchText}
showClear showClear
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder="Search..." placeholder="Search..."
@ -66,7 +56,7 @@ export default function AreaOverview() {
<div className="p-3 popover-theme">No areas found</div> <div className="p-3 popover-theme">No areas found</div>
} }
onSearch={(v) => handleStringSearch(v)} onSearch={(v) => handleStringSearch(v)}
onChange={(v) => setValue(v)} onChange={(v) => setSearchText(v)}
onSelect={(v) => { onSelect={(v) => {
const { id } = areas.find((t) => t.name === v); const { id } = areas.find((t) => t.name === v);
document document
@ -77,24 +67,16 @@ export default function AreaOverview() {
/> />
</Col> </Col>
<Col span={8}> <Col span={8}>
<Button icon={<IconPlus />} block onClick={() => addArea()}> <Button icon={<IconPlus />} block onClick={addArea}>
Add area Add area
</Button> </Button>
</Col> </Col>
</Row> </Row>
{areas.length <= 0 ? ( {areas.length <= 0 ? (
<div className="select-none mt-2"> <NoElements
<Empty title="No subject areas"
image={ text="Add subject areas to organize tables!"
<IllustrationNoContent style={{ width: 154, height: 154 }} /> />
}
darkModeImage={
<IllustrationNoContentDark style={{ width: 154, height: 154 }} />
}
title="No subject areas"
description="Add subject areas to compartmentalize tables!"
/>
</div>
) : ( ) : (
<div className="p-2"> <div className="p-2">
{areas.map((a, i) => ( {areas.map((a, i) => (
@ -236,7 +218,7 @@ export default function AreaOverview() {
Toast.success(`Area deleted!`); Toast.success(`Area deleted!`);
deleteArea(i, true); deleteArea(i, true);
}} }}
></Button> />
</Col> </Col>
</Row> </Row>
))} ))}

View File

@ -1,10 +1,10 @@
import { useRef, useState, useEffect } from "react"; import { useRef, useState, useEffect } from "react";
import Table from "./Table";
import { Action, Cardinality, Constraint, ObjectType } from "../data/constants"; import { Action, Cardinality, Constraint, ObjectType } from "../data/constants";
import { Toast } from "@douyinfe/semi-ui";
import Table from "./Table";
import Area from "./Area"; import Area from "./Area";
import Relationship from "./Relationship"; import Relationship from "./Relationship";
import Note from "./Note"; import Note from "./Note";
import { Toast } from "@douyinfe/semi-ui";
import useSettings from "../hooks/useSettings"; import useSettings from "../hooks/useSettings";
import useTransform from "../hooks/useTransform"; import useTransform from "../hooks/useTransform";
import useTables from "../hooks/useTables"; import useTables from "../hooks/useTables";
@ -28,7 +28,7 @@ export default function Canvas() {
prevY: 0, prevY: 0,
}); });
const [linking, setLinking] = useState(false); const [linking, setLinking] = useState(false);
const [line, setLine] = useState({ const [linkingLink, setLinkingLine] = useState({
startTableId: -1, startTableId: -1,
startFieldId: -1, startFieldId: -1,
endTableId: -1, endTableId: -1,
@ -44,12 +44,17 @@ export default function Canvas() {
mandatory: false, mandatory: false,
}); });
const [offset, setOffset] = useState({ x: 0, y: 0 }); const [offset, setOffset] = useState({ x: 0, y: 0 });
const [onRect, setOnRect] = useState({ const [hoveredTable, setHoveredTable] = useState({
tableId: -1, tableId: -1,
field: -2, field: -2,
}); });
const [panning, setPanning] = useState({ state: false, x: 0, y: 0 }); const [panning, setPanning] = useState({
const [panOffset, setPanOffset] = useState({ x: 0, y: 0 }); isPanning: false,
x: 0,
y: 0,
dx: 0,
dy: 0,
});
const [areaResize, setAreaResize] = useState({ id: -1, dir: "none" }); const [areaResize, setAreaResize] = useState({ id: -1, dir: "none" });
const [initCoords, setInitCoords] = useState({ const [initCoords, setInitCoords] = useState({
x: 0, x: 0,
@ -63,7 +68,7 @@ export default function Canvas() {
const canvas = useRef(null); const canvas = useRef(null);
const handleMouseDownRect = (e, id, type) => { const handleMouseDownOnElement = (e, id, type) => {
const { clientX, clientY } = e; const { clientX, clientY } = e;
if (type === ObjectType.TABLE) { if (type === ObjectType.TABLE) {
const table = tables.find((t) => t.id === id); const table = tables.find((t) => t.id === id);
@ -72,7 +77,7 @@ export default function Canvas() {
y: clientY / transform.zoom - table.y, y: clientY / transform.zoom - table.y,
}); });
setDragging({ setDragging({
element: ObjectType.TABLE, element: type,
id: id, id: id,
prevX: table.x, prevX: table.x,
prevY: table.y, prevY: table.y,
@ -84,7 +89,7 @@ export default function Canvas() {
y: clientY / transform.zoom - area.y, y: clientY / transform.zoom - area.y,
}); });
setDragging({ setDragging({
element: ObjectType.AREA, element: type,
id: id, id: id,
prevX: area.x, prevX: area.x,
prevY: area.y, prevY: area.y,
@ -96,7 +101,7 @@ export default function Canvas() {
y: clientY / transform.zoom - note.y, y: clientY / transform.zoom - note.y,
}); });
setDragging({ setDragging({
element: ObjectType.NOTE, element: type,
id: id, id: id,
prevX: note.x, prevX: note.x,
prevY: note.y, prevY: note.y,
@ -113,29 +118,26 @@ export default function Canvas() {
const handleMouseMove = (e) => { const handleMouseMove = (e) => {
if (linking) { if (linking) {
const rect = canvas.current.getBoundingClientRect(); const rect = canvas.current.getBoundingClientRect();
const offsetX = rect.left; setLinkingLine({
const offsetY = rect.top; ...linkingLink,
endX: (e.clientX - rect.left - transform.pan?.x) / transform.zoom,
setLine({ endY: (e.clientY - rect.top - transform.pan?.y) / transform.zoom,
...line,
endX: (e.clientX - offsetX - transform.pan?.x) / transform.zoom,
endY: (e.clientY - offsetY - transform.pan?.y) / transform.zoom,
}); });
} else if ( } else if (
panning.state && panning.isPanning &&
dragging.element === ObjectType.NONE && dragging.element === ObjectType.NONE &&
areaResize.id === -1 areaResize.id === -1
) { ) {
if (!settings.panning) { if (!settings.panning) {
return; return;
} }
const dx = e.clientX - panOffset.x; const dx = e.clientX - panning.dx;
const dy = e.clientY - panOffset.y; const dy = e.clientY - panning.dy;
setTransform((prev) => ({ setTransform((prev) => ({
...prev, ...prev,
pan: { x: prev.pan?.x + dx, y: prev.pan?.y + dy }, pan: { x: prev.pan?.x + dx, y: prev.pan?.y + dy },
})); }));
setPanOffset({ x: e.clientX, y: e.clientY }); setPanning((prev) => ({ ...prev, dx: e.clientX, dy: e.clientY }));
} else if (dragging.element === ObjectType.TABLE && dragging.id >= 0) { } else if (dragging.element === ObjectType.TABLE && dragging.id >= 0) {
const dx = e.clientX / transform.zoom - offset.x; const dx = e.clientX / transform.zoom - offset.x;
const dy = e.clientY / transform.zoom - offset.y; const dy = e.clientY / transform.zoom - offset.y;
@ -154,44 +156,41 @@ export default function Canvas() {
updateNote(dragging.id, { x: dx, y: dy }); updateNote(dragging.id, { x: dx, y: dy });
} else if (areaResize.id !== -1) { } else if (areaResize.id !== -1) {
if (areaResize.dir === "none") return; if (areaResize.dir === "none") return;
let newDims = { ...initCoords };
let newX = initCoords.x; delete newDims.mouseX;
let newY = initCoords.y; delete newDims.mouseY;
let newWidth = initCoords.width;
let newHeight = initCoords.height;
const mouseX = e.clientX / transform.zoom; const mouseX = e.clientX / transform.zoom;
const mouseY = e.clientY / transform.zoom; const mouseY = e.clientY / transform.zoom;
setPanning({ state: false, x: 0, y: 0 }); setPanning({ isPanning: false, x: 0, y: 0 });
if (areaResize.dir === "br") { if (areaResize.dir === "br") {
newWidth = initCoords.width + (mouseX - initCoords.mouseX); newDims.width = initCoords.width + (mouseX - initCoords.mouseX);
newHeight = initCoords.height + (mouseY - initCoords.mouseY); newDims.height = initCoords.height + (mouseY - initCoords.mouseY);
} else if (areaResize.dir === "tl") { } else if (areaResize.dir === "tl") {
newX = initCoords.x + (mouseX - initCoords.mouseX); newDims.x = initCoords.x + (mouseX - initCoords.mouseX);
newY = initCoords.y + (mouseY - initCoords.mouseY); newDims.y = initCoords.y + (mouseY - initCoords.mouseY);
newWidth = initCoords.width - (mouseX - initCoords.mouseX); newDims.width = initCoords.width - (mouseX - initCoords.mouseX);
newHeight = initCoords.height - (mouseY - initCoords.mouseY); newDims.height = initCoords.height - (mouseY - initCoords.mouseY);
} else if (areaResize.dir === "tr") { } else if (areaResize.dir === "tr") {
newY = initCoords.y + (mouseY - initCoords.mouseY); newDims.y = initCoords.y + (mouseY - initCoords.mouseY);
newWidth = initCoords.width + (mouseX - initCoords.mouseX); newDims.width = initCoords.width + (mouseX - initCoords.mouseX);
newHeight = initCoords.height - (mouseY - initCoords.mouseY); newDims.height = initCoords.height - (mouseY - initCoords.mouseY);
} else if (areaResize.dir === "bl") { } else if (areaResize.dir === "bl") {
newX = initCoords.x + (mouseX - initCoords.mouseX); newDims.x = initCoords.x + (mouseX - initCoords.mouseX);
newWidth = initCoords.width - (mouseX - initCoords.mouseX); newDims.width = initCoords.width - (mouseX - initCoords.mouseX);
newHeight = initCoords.height + (mouseY - initCoords.mouseY); newDims.height = initCoords.height + (mouseY - initCoords.mouseY);
} }
updateArea(areaResize.id, { updateArea(areaResize.id, { ...newDims });
x: newX,
y: newY,
width: newWidth,
height: newHeight,
});
} }
}; };
const handleMouseDown = (e) => { const handleMouseDown = (e) => {
setPanning({ state: true, ...transform.pan }); setPanning({
setPanOffset({ x: e.clientX, y: e.clientY }); isPanning: true,
...transform.pan,
dx: e.clientX,
dy: e.clientY,
});
setCursor("grabbing"); setCursor("grabbing");
}; };
@ -229,7 +228,7 @@ export default function Canvas() {
const didPan = () => const didPan = () =>
!(transform.pan?.x === panning.x && transform.pan?.y === panning.y); !(transform.pan?.x === panning.x && transform.pan?.y === panning.y);
const getMoveInfo = () => { const getMovedElementDetails = () => {
switch (dragging.element) { switch (dragging.element) {
case ObjectType.TABLE: case ObjectType.TABLE:
return { return {
@ -256,7 +255,7 @@ export default function Canvas() {
const handleMouseUp = () => { const handleMouseUp = () => {
if (coordsDidUpdate(dragging.element)) { if (coordsDidUpdate(dragging.element)) {
const info = getMoveInfo(); const info = getMovedElementDetails();
setUndoStack((prev) => [ setUndoStack((prev) => [
...prev, ...prev,
{ {
@ -273,7 +272,7 @@ export default function Canvas() {
setRedoStack([]); setRedoStack([]);
} }
setDragging({ element: ObjectType.NONE, id: -1, prevX: 0, prevY: 0 }); setDragging({ element: ObjectType.NONE, id: -1, prevX: 0, prevY: 0 });
if (panning.state && didPan()) { if (panning.isPanning && didPan()) {
setUndoStack((prev) => [ setUndoStack((prev) => [
...prev, ...prev,
{ {
@ -291,7 +290,7 @@ export default function Canvas() {
open: false, open: false,
})); }));
} }
setPanning({ state: false, x: 0, y: 0 }); setPanning({ isPanning: false, x: 0, y: 0 });
setCursor("default"); setCursor("default");
if (linking) handleLinking(); if (linking) handleLinking();
setLinking(false); setLinking(false);
@ -333,29 +332,29 @@ export default function Canvas() {
}; };
const handleLinking = () => { const handleLinking = () => {
if (onRect.tableId < 0) return; if (hoveredTable.tableId < 0) return;
if (onRect.field < 0) return; if (hoveredTable.field < 0) return;
if ( if (
tables[line.startTableId].fields[line.startFieldId].type !== tables[linkingLink.startTableId].fields[linkingLink.startFieldId].type !==
tables[onRect.tableId].fields[onRect.field].type tables[hoveredTable.tableId].fields[hoveredTable.field].type
) { ) {
Toast.info("Cannot connect"); Toast.info("Cannot connect");
return; return;
} }
if ( if (
line.startTableId === onRect.tableId && linkingLink.startTableId === hoveredTable.tableId &&
line.startFieldId === onRect.field linkingLink.startFieldId === hoveredTable.field
) )
return; return;
addRelationship(true, { addRelationship(true, {
...line, ...linkingLink,
endTableId: onRect.tableId, endTableId: hoveredTable.tableId,
endFieldId: onRect.field, endFieldId: hoveredTable.field,
endX: tables[onRect.tableId].x + 15, endX: tables[hoveredTable.tableId].x + 15,
endY: tables[onRect.tableId].y + onRect.field * 36 + 69, endY: tables[hoveredTable.tableId].y + hoveredTable.field * 36 + 69,
name: `${tables[line.startTableId].name}_${ name: `${tables[linkingLink.startTableId].name}_${
tables[line.startTableId].fields[line.startFieldId].name tables[linkingLink.startTableId].fields[linkingLink.startFieldId].name
}_fk`, }_fk`,
id: relationships.length, id: relationships.length,
}); });
@ -434,14 +433,12 @@ export default function Canvas() {
{areas.map((a) => ( {areas.map((a) => (
<Area <Area
key={a.id} key={a.id}
areaData={a} data={a}
onMouseDown={(e) => onMouseDown={(e) =>
handleMouseDownRect(e, a.id, ObjectType.AREA) handleMouseDownOnElement(e, a.id, ObjectType.AREA)
} }
setResize={setAreaResize} setResize={setAreaResize}
initCoords={initCoords}
setInitCoords={setInitCoords} setInitCoords={setInitCoords}
zoom={transform.zoom}
></Area> ></Area>
))} ))}
{relationships.map((e, i) => ( {relationships.map((e, i) => (
@ -451,11 +448,11 @@ export default function Canvas() {
<Table <Table
key={table.id} key={table.id}
tableData={table} tableData={table}
setOnRect={setOnRect} setHoveredTable={setHoveredTable}
handleGripField={handleGripField} handleGripField={handleGripField}
setLine={setLine} setLinkingLine={setLinkingLine}
onMouseDown={(e) => onMouseDown={(e) =>
handleMouseDownRect(e, table.id, ObjectType.TABLE) handleMouseDownOnElement(e, table.id, ObjectType.TABLE)
} }
active={ active={
selectedElement.element === ObjectType.TABLE && selectedElement.element === ObjectType.TABLE &&
@ -469,7 +466,7 @@ export default function Canvas() {
))} ))}
{linking && ( {linking && (
<path <path
d={`M ${line.startX} ${line.startY} L ${line.endX} ${line.endY}`} d={`M ${linkingLink.startX} ${linkingLink.startY} L ${linkingLink.endX} ${linkingLink.endY}`}
stroke="red" stroke="red"
strokeDasharray="8,8" strokeDasharray="8,8"
/> />
@ -479,7 +476,7 @@ export default function Canvas() {
key={n.id} key={n.id}
data={n} data={n}
onMouseDown={(e) => onMouseDown={(e) =>
handleMouseDownRect(e, n.id, ObjectType.NOTE) handleMouseDownOnElement(e, n.id, ObjectType.NOTE)
} }
></Note> ></Note>
))} ))}

View File

@ -1725,7 +1725,7 @@ export default function ControlPanel({
}) })
} }
limit={1} limit={1}
></Upload> />
{error.type === STATUS.ERROR ? ( {error.type === STATUS.ERROR ? (
<Banner <Banner
type="danger" type="danger"
@ -1803,7 +1803,7 @@ export default function ControlPanel({
]} ]}
onChange={(e) => setData((prev) => ({ ...prev, dbms: e }))} onChange={(e) => setData((prev) => ({ ...prev, dbms: e }))}
className="w-full" className="w-full"
></Select> />
<Checkbox <Checkbox
aria-label="overwrite checkbox" aria-label="overwrite checkbox"
checked={data.overwrite} checked={data.overwrite}
@ -1914,7 +1914,7 @@ export default function ControlPanel({
}} }}
> >
<td className="py-1"> <td className="py-1">
<i className="bi bi-file-earmark-text text-[16px] me-1 opacity-60"></i> <i className="bi bi-file-earmark-text text-[16px] me-1 opacity-60" />
{d.name} {d.name}
</td> </td>
<td className="py-1"> <td className="py-1">
@ -2085,7 +2085,7 @@ export default function ControlPanel({
className="hover-1" className="hover-1"
> >
<div className="flex items-center py-1 w-full"> <div className="flex items-center py-1 w-full">
<i className="block fa-regular fa-circle fa-xs"></i> <i className="block fa-regular fa-circle fa-xs" />
<div className="ms-2">{e.message}</div> <div className="ms-2">{e.message}</div>
</div> </div>
</List.Item> </List.Item>
@ -2166,7 +2166,7 @@ export default function ControlPanel({
setTransform((prev) => ({ ...prev, zoom: prev.zoom * 1.2 })) setTransform((prev) => ({ ...prev, zoom: prev.zoom * 1.2 }))
} }
> >
<i className="fa-solid fa-magnifying-glass-plus"></i> <i className="fa-solid fa-magnifying-glass-plus" />
</button> </button>
</Tooltip> </Tooltip>
<Tooltip content="Zoom out" position="bottom"> <Tooltip content="Zoom out" position="bottom">
@ -2176,7 +2176,7 @@ export default function ControlPanel({
setTransform((prev) => ({ ...prev, zoom: prev.zoom / 1.2 })) setTransform((prev) => ({ ...prev, zoom: prev.zoom / 1.2 }))
} }
> >
<i className="fa-solid fa-magnifying-glass-minus"></i> <i className="fa-solid fa-magnifying-glass-minus" />
</button> </button>
</Tooltip> </Tooltip>
<Divider layout="vertical" margin="8px" /> <Divider layout="vertical" margin="8px" />
@ -2241,7 +2241,7 @@ export default function ControlPanel({
className="py-1 px-2 hover-2 rounded text-xl -mt-0.5" className="py-1 px-2 hover-2 rounded text-xl -mt-0.5"
onClick={() => setSidesheet(SIDESHEET.TODO)} onClick={() => setSidesheet(SIDESHEET.TODO)}
> >
<i className="fa-regular fa-calendar-check"></i> <i className="fa-regular fa-calendar-check" />
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
@ -2401,11 +2401,7 @@ export default function ControlPanel({
<Dropdown.Menu> <Dropdown.Menu>
<Dropdown.Item <Dropdown.Item
icon={ icon={
layout.header ? ( layout.header ? <IconCheckboxTick /> : <div className="px-2" />
<IconCheckboxTick />
) : (
<div className="px-2"></div>
)
} }
onClick={() => invertLayout("header")} onClick={() => invertLayout("header")}
> >
@ -2413,11 +2409,7 @@ export default function ControlPanel({
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item <Dropdown.Item
icon={ icon={
layout.sidebar ? ( layout.sidebar ? <IconCheckboxTick /> : <div className="px-2" />
<IconCheckboxTick />
) : (
<div className="px-2"></div>
)
} }
onClick={() => invertLayout("sidebar")} onClick={() => invertLayout("sidebar")}
> >
@ -2425,11 +2417,7 @@ export default function ControlPanel({
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item <Dropdown.Item
icon={ icon={
layout.issues ? ( layout.issues ? <IconCheckboxTick /> : <div className="px-2" />
<IconCheckboxTick />
) : (
<div className="px-2"></div>
)
} }
onClick={() => invertLayout("issues")} onClick={() => invertLayout("issues")}
> >
@ -2437,7 +2425,7 @@ export default function ControlPanel({
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Divider /> <Dropdown.Divider />
<Dropdown.Item <Dropdown.Item
icon={<div className="px-2"></div>} icon={<div className="px-2" />}
onClick={() => { onClick={() => {
if (layout.fullscreen) { if (layout.fullscreen) {
exitFullscreen(); exitFullscreen();

View File

@ -19,7 +19,7 @@ export default function Controls() {
})) }))
} }
> >
<i className="bi bi-dash-lg"></i> <i className="bi bi-dash-lg" />
</button> </button>
<Divider align="center" layout="vertical" /> <Divider align="center" layout="vertical" />
<div className="px-3 py-2">{parseInt(transform.zoom * 100)}%</div> <div className="px-3 py-2">{parseInt(transform.zoom * 100)}%</div>
@ -33,7 +33,7 @@ export default function Controls() {
})) }))
} }
> >
<i className="bi bi-plus-lg"></i> <i className="bi bi-plus-lg" />
</button> </button>
</div> </div>
<Tooltip content="Exit"> <Tooltip content="Exit">
@ -49,7 +49,7 @@ export default function Controls() {
exitFullscreen(); exitFullscreen();
}} }}
> >
<i className="bi bi-fullscreen-exit"></i> <i className="bi bi-fullscreen-exit" />
</button> </button>
</Tooltip> </Tooltip>
</div> </div>

View File

@ -39,7 +39,7 @@ export default function Issues() {
className="mt-1" className="mt-1"
> >
<div className="pe-3 select-none"> <div className="pe-3 select-none">
<i className="fa-solid fa-triangle-exclamation me-2 text-yellow-500"></i> <i className="fa-solid fa-triangle-exclamation me-2 text-yellow-500" />
Issues Issues
</div> </div>
</Badge> </Badge>

View File

@ -48,7 +48,9 @@ export default function Navbar() {
</div> </div>
<hr /> <hr />
<SideSheet <SideSheet
title={<img src={logo} alt="logo" className="sm:h-[32px]" />} title={
<img src={logo} alt="logo" className="sm:h-[32px] md:h-[42px]" />
}
visible={openMenu} visible={openMenu}
onCancel={() => setOpenMenu(false)} onCancel={() => setOpenMenu(false)}
width={window.innerWidth} width={window.innerWidth}

View File

@ -0,0 +1,20 @@
import {
IllustrationNoContent,
IllustrationNoContentDark,
} from "@douyinfe/semi-illustrations";
import { Empty } from "@douyinfe/semi-ui";
export default function NoElements({ title, text }) {
return (
<div className="select-none mt-2">
<Empty
image={<IllustrationNoContent style={{ width: 154, height: 154 }} />}
darkModeImage={
<IllustrationNoContentDark style={{ width: 154, height: 154 }} />
}
title={title}
description={text}
/>
</div>
);
}

View File

@ -28,10 +28,50 @@ export default function Note({ data, onMouseDown }) {
const textarea = document.getElementById(`note_${data.id}`); const textarea = document.getElementById(`note_${data.id}`);
textarea.style.height = "0"; textarea.style.height = "0";
textarea.style.height = textarea.scrollHeight + "px"; textarea.style.height = textarea.scrollHeight + "px";
const newHeight = textarea.scrollHeight + 41; const newHeight = textarea.scrollHeight + 42;
updateNote(data.id, { content: e.target.value, height: newHeight }); updateNote(data.id, { content: e.target.value, height: newHeight });
}; };
const handleBlur = (e) => {
if (e.target.value === editField.content) return;
const textarea = document.getElementById(`note_${data.id}`);
textarea.style.height = "0";
textarea.style.height = textarea.scrollHeight + "px";
const newHeight = textarea.scrollHeight + 16 + 20 + 4;
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.NOTE,
nid: data.id,
undo: editField,
redo: { content: e.target.value, height: newHeight },
message: `Edit note content to "${e.target.value}"`,
},
]);
setRedoStack([]);
};
const edit = () => {
if (layout.sidebar) {
setSelectedElement((prev) => ({
...prev,
currentTab: Tab.NOTES,
}));
if (selectedElement.currentTab !== Tab.NOTES) return;
document
.getElementById(`scroll_note_${data.id}`)
.scrollIntoView({ behavior: "smooth" });
} else {
setSelectedElement((prev) => ({
...prev,
element: ObjectType.NOTE,
id: data.id,
open: true,
}));
}
};
return ( return (
<g <g
onMouseEnter={() => setHovered(true)} onMouseEnter={() => setHovered(true)}
@ -100,28 +140,10 @@ export default function Note({ data, onMouseDown }) {
height: data.height, height: data.height,
}) })
} }
onBlur={(e) => { onBlur={handleBlur}
if (e.target.value === editField.content) return;
const textarea = document.getElementById(`note_${data.id}`);
textarea.style.height = "0";
textarea.style.height = textarea.scrollHeight + "px";
const newHeight = textarea.scrollHeight + 16 + 20 + 4;
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.NOTE,
nid: data.id,
undo: editField,
redo: { content: e.target.value, height: newHeight },
message: `Edit note content to "${e.target.value}"`,
},
]);
setRedoStack([]);
}}
className="w-full resize-none outline-none overflow-y-hidden border-none select-none" className="w-full resize-none outline-none overflow-y-hidden border-none select-none"
style={{ backgroundColor: data.color }} style={{ backgroundColor: data.color }}
></textarea> />
{(hovered || {(hovered ||
(selectedElement.element === ObjectType.NOTE && (selectedElement.element === ObjectType.NOTE &&
selectedElement.id === data.id && selectedElement.id === data.id &&
@ -253,26 +275,8 @@ export default function Note({ data, onMouseDown }) {
backgroundColor: "#2f68ad", backgroundColor: "#2f68ad",
opacity: "0.7", opacity: "0.7",
}} }}
onClick={() => { onClick={edit}
if (layout.sidebar) { />
setSelectedElement((prev) => ({
...prev,
currentTab: Tab.NOTES,
}));
if (selectedElement.currentTab !== Tab.NOTES) return;
document
.getElementById(`scroll_note_${data.id}`)
.scrollIntoView({ behavior: "smooth" });
} else {
setSelectedElement((prev) => ({
...prev,
element: ObjectType.NOTE,
id: data.id,
open: true,
}));
}
}}
></Button>
</Popover> </Popover>
</div> </div>
)} )}

View File

@ -1,6 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { import {
Empty,
Row, Row,
Col, Col,
Button, Button,
@ -11,10 +10,6 @@ import {
Input, Input,
Toast, Toast,
} from "@douyinfe/semi-ui"; } from "@douyinfe/semi-ui";
import {
IllustrationNoContent,
IllustrationNoContentDark,
} from "@douyinfe/semi-illustrations";
import { import {
IconDeleteStroked, IconDeleteStroked,
IconPlus, IconPlus,
@ -24,26 +19,21 @@ import {
import { noteThemes, Action, ObjectType } from "../data/constants"; import { noteThemes, Action, ObjectType } from "../data/constants";
import useUndoRedo from "../hooks/useUndoRedo"; import useUndoRedo from "../hooks/useUndoRedo";
import useNotes from "../hooks/useNotes"; import useNotes from "../hooks/useNotes";
import NoElements from "./NoElements";
export default function NotesOverview() { export default function NotesOverview() {
const { notes, updateNote, addNote, deleteNote } = useNotes(); const { notes, updateNote, addNote, deleteNote } = useNotes();
const { setUndoStack, setRedoStack } = useUndoRedo(); const { setUndoStack, setRedoStack } = useUndoRedo();
const [value, setValue] = useState(""); const [searchText, setSearchText] = useState("");
const [editField, setEditField] = useState({}); const [editField, setEditField] = useState({});
const [activeKey, setActiveKey] = useState(""); const [activeKey, setActiveKey] = useState("");
const [filteredResult, setFilteredResult] = useState( const [filteredResult, setFilteredResult] = useState(
notes.map((t) => { notes.map((t) => t.title)
return t.title;
})
); );
const handleStringSearch = (value) => { const handleStringSearch = (value) => {
setFilteredResult( setFilteredResult(
notes notes.map((t) => t.title).filter((i) => i.includes(value))
.map((t) => {
return t.title;
})
.filter((i) => i.includes(value))
); );
}; };
@ -53,7 +43,7 @@ export default function NotesOverview() {
<Col span={16}> <Col span={16}>
<AutoComplete <AutoComplete
data={filteredResult} data={filteredResult}
value={value} value={searchText}
showClear showClear
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder="Search..." placeholder="Search..."
@ -61,7 +51,7 @@ export default function NotesOverview() {
<div className="p-3 popover-theme">No notes found</div> <div className="p-3 popover-theme">No notes found</div>
} }
onSearch={(v) => handleStringSearch(v)} onSearch={(v) => handleStringSearch(v)}
onChange={(v) => setValue(v)} onChange={(v) => setSearchText(v)}
onSelect={(v) => { onSelect={(v) => {
const { id } = notes.find((t) => t.title === v); const { id } = notes.find((t) => t.title === v);
setActiveKey(`${id}`); setActiveKey(`${id}`);
@ -79,18 +69,7 @@ export default function NotesOverview() {
</Col> </Col>
</Row> </Row>
{notes.length <= 0 ? ( {notes.length <= 0 ? (
<div className="select-none mt-2"> <NoElements title="No text notes" text="Add notes cuz why not!" />
<Empty
image={
<IllustrationNoContent style={{ width: 154, height: 154 }} />
}
darkModeImage={
<IllustrationNoContentDark style={{ width: 154, height: 154 }} />
}
title="No text notes"
description="Add notes cuz why not!"
/>
</div>
) : ( ) : (
<Collapse <Collapse
activeKey={activeKey} activeKey={activeKey}
@ -218,7 +197,7 @@ export default function NotesOverview() {
Toast.success(`Note deleted!`); Toast.success(`Note deleted!`);
deleteNote(i, true); deleteNote(i, true);
}} }}
></Button> />
</div> </div>
</div> </div>
</Collapse.Panel> </Collapse.Panel>

View File

@ -1,295 +0,0 @@
import { useState } from "react";
import {
AutoComplete,
Collapse,
Empty,
Row,
Col,
Select,
Button,
Popover,
Table,
} from "@douyinfe/semi-ui";
import {
IconDeleteStroked,
IconLoopTextStroked,
IconMore,
IconSearch,
} from "@douyinfe/semi-icons";
import {
IllustrationNoContent,
IllustrationNoContentDark,
} from "@douyinfe/semi-illustrations";
import { Cardinality, Constraint, Action, ObjectType } from "../data/constants";
import useTables from "../hooks/useTables";
import useUndoRedo from "../hooks/useUndoRedo";
export default function ReferenceOverview() {
const columns = [
{
title: "Primary",
dataIndex: "primary",
},
{
title: "Foreign",
dataIndex: "foreign",
},
];
const { tables, relationships, setRelationships, deleteRelationship } =
useTables();
const { setUndoStack, setRedoStack } = useUndoRedo();
const [refActiveIndex, setRefActiveIndex] = useState("");
const [value, setValue] = useState("");
const [filteredResult, setFilteredResult] = useState(
relationships.map((t) => {
return t.name;
})
);
const handleStringSearch = (value) => {
setFilteredResult(
relationships
.map((t) => {
return t.name;
})
.filter((i) => i.includes(value))
);
};
return (
<>
<AutoComplete
data={filteredResult}
value={value}
showClear
prefix={<IconSearch />}
placeholder="Search..."
emptyContent={
<div className="p-3 popover-theme">No relationships found</div>
}
onSearch={(v) => handleStringSearch(v)}
onChange={(v) => setValue(v)}
onSelect={(v) => {
const { id } = relationships.find((t) => t.name === v);
setRefActiveIndex(`${id}`);
document
.getElementById(`scroll_ref_${id}`)
.scrollIntoView({ behavior: "smooth" });
}}
className="w-full"
/>
<Collapse
activeKey={refActiveIndex}
onChange={(k) => setRefActiveIndex(k)}
accordion
>
{relationships.length <= 0 ? (
<div className="select-none mt-2">
<Empty
image={
<IllustrationNoContent style={{ width: 154, height: 154 }} />
}
darkModeImage={
<IllustrationNoContentDark
style={{ width: 154, height: 154 }}
/>
}
title="No relationships"
description="Drag to connect fields and form relationships!"
/>
</div>
) : (
relationships.map((r, i) => (
<div id={`scroll_ref_${r.id}`} key={i}>
<Collapse.Panel header={<div>{r.name}</div>} itemKey={`${i}`}>
<div className="flex justify-between items-center mb-3">
<div className="me-3">
<span className="font-semibold">Primary: </span>
{tables[r.endTableId].name}
</div>
<div className="mx-1">
<span className="font-semibold">Foreign: </span>
{tables[r.startTableId].name}
</div>
<div className="ms-1">
<Popover
content={
<div className="p-2 popover-theme">
<Table
columns={columns}
dataSource={[
{
key: "1",
foreign: `${tables[r.startTableId].name}(${
tables[r.startTableId].fields[r.startFieldId]
.name
})`,
primary: `${tables[r.endTableId].name}(${
tables[r.endTableId].fields[r.endFieldId].name
})`,
},
]}
pagination={false}
size="small"
bordered
/>
<div className="mt-2">
<Button
icon={<IconLoopTextStroked />}
block
onClick={() => {
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.RELATIONSHIP,
rid: i,
undo: {
startTableId: r.startTableId,
startFieldId: r.startFieldId,
endTableId: r.endTableId,
endFieldId: r.endFieldId,
},
redo: {
startTableId: r.endTableId,
startFieldId: r.endFieldId,
endTableId: r.startTableId,
endFieldId: r.startFieldId,
},
message: `Swap primary and foreign tables`,
},
]);
setRedoStack([]);
setRelationships((prev) =>
prev.map((e, idx) =>
idx === i
? {
...e,
startTableId: e.endTableId,
startFieldId: e.endFieldId,
endTableId: e.startTableId,
endFieldId: e.startFieldId,
}
: e
)
);
}}
>
Swap
</Button>
</div>
</div>
}
trigger="click"
position="rightTop"
showArrow
>
<Button icon={<IconMore />} type="tertiary"></Button>
</Popover>
</div>
</div>
<div className="font-semibold my-1">Cardinality</div>
<Select
optionList={Object.values(Cardinality).map((v) => ({
label: v,
value: v,
}))}
value={r.cardinality}
className="w-full"
onChange={(value) => {
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.RELATIONSHIP,
rid: i,
undo: { cardinality: r.cardinality },
redo: { cardinality: value },
message: `Edit relationship cardinality`,
},
]);
setRedoStack([]);
setRelationships((prev) =>
prev.map((e, idx) =>
idx === i ? { ...e, cardinality: value } : e
)
);
}}
></Select>
<Row gutter={6} className="my-3">
<Col span={12}>
<div className="font-semibold">On update: </div>
<Select
optionList={Object.values(Constraint).map((v) => ({
label: v,
value: v,
}))}
value={r.updateConstraint}
className="w-full"
onChange={(value) => {
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.RELATIONSHIP,
rid: i,
undo: { updateConstraint: r.updateConstraint },
redo: { updateConstraint: value },
message: `Edit relationship update constraint`,
},
]);
setRedoStack([]);
setRelationships((prev) =>
prev.map((e, idx) =>
idx === i ? { ...e, updateConstraint: value } : e
)
);
}}
></Select>
</Col>
<Col span={12}>
<div className="font-semibold">On delete: </div>
<Select
optionList={Object.values(Constraint).map((v) => ({
label: v,
value: v,
}))}
value={r.deleteConstraint}
className="w-full"
onChange={(value) => {
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.RELATIONSHIP,
rid: i,
undo: { deleteConstraint: r.deleteConstraint },
redo: { deleteConstraint: value },
message: `Edit relationship delete constraint`,
},
]);
setRedoStack([]);
setRelationships((prev) =>
prev.map((e, idx) =>
idx === i ? { ...e, deleteConstraint: value } : e
)
);
}}
></Select>
</Col>
</Row>
<Button
icon={<IconDeleteStroked />}
block
type="danger"
onClick={() => deleteRelationship(r.id, true)}
>
Delete
</Button>
</Collapse.Panel>
</div>
))
)}
</Collapse>
</>
);
}

View File

@ -0,0 +1,281 @@
import { useState } from "react";
import {
AutoComplete,
Collapse,
Row,
Col,
Select,
Button,
Popover,
Table,
} from "@douyinfe/semi-ui";
import {
IconDeleteStroked,
IconLoopTextStroked,
IconMore,
IconSearch,
} from "@douyinfe/semi-icons";
import { Cardinality, Constraint, Action, ObjectType } from "../data/constants";
import useTables from "../hooks/useTables";
import useUndoRedo from "../hooks/useUndoRedo";
import NoElements from "./NoElements";
export default function RelationshipsOverview() {
const { relationships } = useTables();
const [refActiveIndex, setRefActiveIndex] = useState("");
const [searchText, setSearchText] = useState("");
const [filteredResult, setFilteredResult] = useState(
relationships.map((t) => t.name)
);
const handleStringSearch = (value) => {
setFilteredResult(
relationships.map((t) => t.name).filter((i) => i.includes(value))
);
};
return (
<>
<AutoComplete
data={filteredResult}
value={searchText}
showClear
prefix={<IconSearch />}
placeholder="Search..."
emptyContent={
<div className="p-3 popover-theme">No relationships found</div>
}
onSearch={(v) => handleStringSearch(v)}
onChange={(v) => setSearchText(v)}
onSelect={(v) => {
const { id } = relationships.find((t) => t.name === v);
setRefActiveIndex(`${id}`);
document
.getElementById(`scroll_ref_${id}`)
.scrollIntoView({ behavior: "smooth" });
}}
className="w-full"
/>
<Collapse
activeKey={refActiveIndex}
onChange={(k) => setRefActiveIndex(k)}
accordion
>
{relationships.length <= 0 ? (
<NoElements
title="No relationships"
text="Drag to connect fields and form relationships!"
/>
) : (
relationships.map((r) => <RelationshipPanel key={r.id} data={r} />)
)}
</Collapse>
</>
);
}
function RelationshipPanel({ data }) {
const columns = [
{
title: "Primary",
dataIndex: "primary",
},
{
title: "Foreign",
dataIndex: "foreign",
},
];
const { tables, setRelationships, deleteRelationship } = useTables();
const { setUndoStack, setRedoStack } = useUndoRedo();
return (
<div id={`scroll_ref_${data.id}`}>
<Collapse.Panel header={data.name} itemKey={`${data.id}`}>
<div className="flex justify-between items-center mb-3">
<div className="me-3">
<span className="font-semibold">Primary: </span>
{tables[data.endTableId].name}
</div>
<div className="mx-1">
<span className="font-semibold">Foreign: </span>
{tables[data.startTableId].name}
</div>
<div className="ms-1">
<Popover
content={
<div className="p-2 popover-theme">
<Table
columns={columns}
dataSource={[
{
key: "1",
foreign: `${tables[data.startTableId].name}(${
tables[data.startTableId].fields[data.startFieldId]
.name
})`,
primary: `${tables[data.endTableId].name}(${
tables[data.endTableId].fields[data.endFieldId].name
})`,
},
]}
pagination={false}
size="small"
bordered
/>
<div className="mt-2">
<Button
icon={<IconLoopTextStroked />}
block
onClick={() => {
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.RELATIONSHIP,
rid: data.id,
undo: {
startTableId: data.startTableId,
startFieldId: data.startFieldId,
endTableId: data.endTableId,
endFieldId: data.endFieldId,
},
redo: {
startTableId: data.endTableId,
startFieldId: data.endFieldId,
endTableId: data.startTableId,
endFieldId: data.startFieldId,
},
message: `Swap primary and foreign tables`,
},
]);
setRedoStack([]);
setRelationships((prev) =>
prev.map((e, idx) =>
idx === data.id
? {
...e,
startTableId: e.endTableId,
startFieldId: e.endFieldId,
endTableId: e.startTableId,
endFieldId: e.startFieldId,
}
: e
)
);
}}
>
Swap
</Button>
</div>
</div>
}
trigger="click"
position="rightTop"
showArrow
>
<Button icon={<IconMore />} type="tertiary" />
</Popover>
</div>
</div>
<div className="font-semibold my-1">Cardinality</div>
<Select
optionList={Object.values(Cardinality).map((v) => ({
label: v,
value: v,
}))}
value={data.cardinality}
className="w-full"
onChange={(value) => {
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.RELATIONSHIP,
rid: data.id,
undo: { cardinality: data.cardinality },
redo: { cardinality: value },
message: `Edit relationship cardinality`,
},
]);
setRedoStack([]);
setRelationships((prev) =>
prev.map((e, idx) =>
idx === data.id ? { ...e, cardinality: value } : e
)
);
}}
></Select>
<Row gutter={6} className="my-3">
<Col span={12}>
<div className="font-semibold">On update: </div>
<Select
optionList={Object.values(Constraint).map((v) => ({
label: v,
value: v,
}))}
value={data.updateConstraint}
className="w-full"
onChange={(value) => {
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.RELATIONSHIP,
rid: data.id,
undo: { updateConstraint: data.updateConstraint },
redo: { updateConstraint: value },
message: `Edit relationship update constraint`,
},
]);
setRedoStack([]);
setRelationships((prev) =>
prev.map((e, idx) =>
idx === data.id ? { ...e, updateConstraint: value } : e
)
);
}}
></Select>
</Col>
<Col span={12}>
<div className="font-semibold">On delete: </div>
<Select
optionList={Object.values(Constraint).map((v) => ({
label: v,
value: v,
}))}
value={data.deleteConstraint}
className="w-full"
onChange={(value) => {
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.RELATIONSHIP,
rid: data.id,
undo: { deleteConstraint: data.deleteConstraint },
redo: { deleteConstraint: value },
message: `Edit relationship delete constraint`,
},
]);
setRedoStack([]);
setRelationships((prev) =>
prev.map((e, idx) =>
idx === data.id ? { ...e, deleteConstraint: value } : e
)
);
}}
></Select>
</Col>
</Row>
<Button
icon={<IconDeleteStroked />}
block
type="danger"
onClick={() => deleteRelationship(data.id, true)}
>
Delete
</Button>
</Collapse.Panel>
</div>
);
}

View File

@ -2,13 +2,12 @@ import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable"; import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"; import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin"; import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"; import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
import { ListPlugin } from "@lexical/react/LexicalListPlugin"; import { ListPlugin } from "@lexical/react/LexicalListPlugin";
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin"; import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
import { ClearEditorPlugin } from "@lexical/react/LexicalClearEditorPlugin"; import { ClearEditorPlugin } from "@lexical/react/LexicalClearEditorPlugin";
import { TRANSFORMERS } from "@lexical/markdown"; import { TRANSFORMERS } from "@lexical/markdown";
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
import ToolbarPlugin from "../plugins/ToolbarPlugin"; import ToolbarPlugin from "../plugins/ToolbarPlugin";
import ListMaxIndentLevelPlugin from "../plugins/ListMaxIndentLevelPlugin"; import ListMaxIndentLevelPlugin from "../plugins/ListMaxIndentLevelPlugin";
import CodeHighlightPlugin from "../plugins/CodeHighlightPlugin"; import CodeHighlightPlugin from "../plugins/CodeHighlightPlugin";

View File

@ -1,8 +1,8 @@
import { Tabs } from "@douyinfe/semi-ui"; import { Tabs } from "@douyinfe/semi-ui";
import { Tab } from "../data/constants"; import { Tab } from "../data/constants";
import TableOverview from "./TableOverview"; import TablesOverview from "./TablesOverview";
import ReferenceOverview from "./ReferenceOverview"; import RelationshipsOverview from "./RelationshipsOverview";
import AreaOverview from "./AreaOverview"; import AreasOverview from "./AreasOverview";
import NotesOverview from "./NotesOverview"; import NotesOverview from "./NotesOverview";
import TypesOverview from "./TypesOverview"; import TypesOverview from "./TypesOverview";
import Issues from "./Issues"; import Issues from "./Issues";
@ -20,12 +20,13 @@ export default function SidePanel({ width, resize, setResize }) {
{ tab: "Notes", itemKey: Tab.NOTES }, { tab: "Notes", itemKey: Tab.NOTES },
{ tab: "Types", itemKey: Tab.TYPES }, { tab: "Types", itemKey: Tab.TYPES },
]; ];
const contentList = [ const contentList = [
<TableOverview key={1} />, <TablesOverview key="tables" />,
<ReferenceOverview key={2} />, <RelationshipsOverview key="relationships" />,
<AreaOverview key={3} />, <AreasOverview key="areas" />,
<NotesOverview key={4} />, <NotesOverview key="notes" />,
<TypesOverview key={5} />, <TypesOverview key="types" />,
]; ];
return ( return (
@ -57,7 +58,7 @@ export default function SidePanel({ width, resize, setResize }) {
</div> </div>
<div <div
className={`flex justify-center items-center p-1 h-auto hover-2 cursor-col-resize ${ className={`flex justify-center items-center p-1 h-auto hover-2 cursor-col-resize ${
resize ? "bg-semi-grey-2" : "" resize && "bg-semi-grey-2"
}`} }`}
onMouseDown={() => setResize(true)} onMouseDown={() => setResize(true)}
> >

View File

@ -131,7 +131,7 @@ export default function Table(props) {
.scrollIntoView({ behavior: "smooth" }); .scrollIntoView({ behavior: "smooth" });
} }
}} }}
></Button> />
<Popover <Popover
content={ content={
<div className="popover-theme"> <div className="popover-theme">
@ -205,7 +205,7 @@ export default function Table(props) {
backgroundColor: "grey", backgroundColor: "grey",
color: "white", color: "white",
}} }}
></Button> />
</Popover> </Popover>
</div> </div>
)} )}
@ -469,7 +469,7 @@ export default function Table(props) {
updateField(props.tableData.id, j, { primary: !f.primary }); updateField(props.tableData.id, j, { primary: !f.primary });
}} }}
icon={<IconKeyStroked />} icon={<IconKeyStroked />}
></Button> />
</Col> </Col>
<Col span={3}> <Col span={3}>
<Popover <Popover
@ -848,7 +848,7 @@ export default function Table(props) {
position="right" position="right"
showArrow showArrow
> >
<Button type="tertiary" icon={<IconMore />}></Button> <Button type="tertiary" icon={<IconMore />} />
</Popover> </Popover>
</Col> </Col>
</Row> </Row>
@ -1000,7 +1000,7 @@ export default function Table(props) {
icon={<IconMore />} icon={<IconMore />}
type="tertiary" type="tertiary"
style={{ marginLeft: "12px" }} style={{ marginLeft: "12px" }}
></Button> />
</Popover> </Popover>
</div> </div>
))} ))}
@ -1229,7 +1229,7 @@ export default function Table(props) {
Toast.success(`Table deleted!`); Toast.success(`Table deleted!`);
deleteTable(props.tableData.id); deleteTable(props.tableData.id);
}} }}
></Button> />
</Col> </Col>
</Row> </Row>
</div> </div>
@ -1241,13 +1241,12 @@ export default function Table(props) {
return ( return (
<div <div
className={`${ className={`${
index === props.tableData.fields.length - 1 index === props.tableData.fields.length - 1 ||
? "" "border-b border-gray-400"
: "border-b border-gray-400"
} h-[36px] px-2 py-1 flex justify-between`} } h-[36px] px-2 py-1 flex justify-between`}
onMouseEnter={() => { onMouseEnter={() => {
setHoveredField(index); setHoveredField(index);
props.setOnRect({ props.setHoveredTable({
tableId: props.tableData.id, tableId: props.tableData.id,
field: index, field: index,
}); });
@ -1261,7 +1260,7 @@ export default function Table(props) {
className={`w-[10px] h-[10px] bg-[#2f68ad] opacity-80 z-50 rounded-full me-2`} className={`w-[10px] h-[10px] bg-[#2f68ad] opacity-80 z-50 rounded-full me-2`}
onMouseDown={() => { onMouseDown={() => {
props.handleGripField(index); props.handleGripField(index);
props.setLine((prev) => ({ props.setLinkingLine((prev) => ({
...prev, ...prev,
startFieldId: index, startFieldId: index,
startTableId: props.tableData.id, startTableId: props.tableData.id,
@ -1271,7 +1270,7 @@ export default function Table(props) {
endY: props.tableData.y + index * 36 + 50 + 19, endY: props.tableData.y + index * 36 + 50 + 19,
})); }));
}} }}
></button> />
{fieldData.name.length <= 11 {fieldData.name.length <= 11
? fieldData.name ? fieldData.name
: fieldData.name.substring(0, 11)} : fieldData.name.substring(0, 11)}
@ -1352,7 +1351,7 @@ export default function Table(props) {
}), }),
}); });
}} }}
></Button> />
) : ( ) : (
fieldData.type fieldData.type
)} )}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -49,9 +49,7 @@ export function Thumbnail({ diagram, i, zoom }) {
width={a.width > 0 ? a.width : 0} width={a.width > 0 ? a.width : 0}
height={a.height > 0 ? a.height : 0} height={a.height > 0 ? a.height : 0}
> >
<div <div className="border border-slate-400 w-full h-full rounded-sm relative">
className={`border border-slate-400 w-full h-full rounded-sm relative`}
>
<div <div
className="opacity-40 w-fill h-full" className="opacity-40 w-fill h-full"
style={{ backgroundColor: a.color }} style={{ backgroundColor: a.color }}
@ -82,7 +80,7 @@ export function Thumbnail({ diagram, i, zoom }) {
<div <div
className="h-2 w-full rounded-t-sm" className="h-2 w-full rounded-t-sm"
style={{ backgroundColor: table.color }} style={{ backgroundColor: table.color }}
></div> />
<div className="rounded-b-[3px]"> <div className="rounded-b-[3px]">
<div <div
className={`font-bold py-1 px-2 border-b ${ className={`font-bold py-1 px-2 border-b ${

View File

@ -235,7 +235,7 @@ export default function Todo() {
showArrow showArrow
className="w-[180px]" className="w-[180px]"
> >
<Button icon={<IconMore />} type="tertiary"></Button> <Button icon={<IconMore />} type="tertiary" />
</Popover> </Popover>
</Col> </Col>
</Row> </Row>

View File

@ -13,7 +13,6 @@ import {
InputNumber, InputNumber,
AutoComplete, AutoComplete,
Toast, Toast,
Empty,
Popover, Popover,
} from "@douyinfe/semi-ui"; } from "@douyinfe/semi-ui";
import { import {
@ -23,32 +22,21 @@ import {
IconInfoCircle, IconInfoCircle,
IconMore, IconMore,
} from "@douyinfe/semi-icons"; } from "@douyinfe/semi-icons";
import {
IllustrationNoContent,
IllustrationNoContentDark,
} from "@douyinfe/semi-illustrations";
import { isSized, hasPrecision, getSize } from "../utils/toSQL"; import { isSized, hasPrecision, getSize } from "../utils/toSQL";
import useUndoRedo from "../hooks/useUndoRedo"; import useUndoRedo from "../hooks/useUndoRedo";
import useTypes from "../hooks/useTypes"; import useTypes from "../hooks/useTypes";
import NoElements from "./NoElements";
export default function TableOverview() { export default function TypesOverview() {
const [value, setValue] = useState(""); const [value, setValue] = useState("");
const { types, addType, deleteType, updateType } = useTypes(); const { types, addType } = useTypes();
const { setUndoStack, setRedoStack } = useUndoRedo();
const [editField, setEditField] = useState({});
const [filteredResult, setFilteredResult] = useState( const [filteredResult, setFilteredResult] = useState(
types.map((t) => { types.map((t) => t.name)
return t.name;
})
); );
const handleStringSearch = (value) => { const handleStringSearch = (value) => {
setFilteredResult( setFilteredResult(
types types.map((t) => t.name).filter((i) => i.includes(value))
.map((t) => {
return t.name;
})
.filter((i) => i.includes(value))
); );
}; };
@ -111,414 +99,403 @@ export default function TableOverview() {
</Popover> </Popover>
</Col> </Col>
</Row> </Row>
<Collapse accordion> {types.length <= 0 ? (
{types.length <= 0 ? ( <NoElements title="No types" text="Make your own custom data types" />
<div className="select-none mt-2"> ) : (
<Empty <Collapse accordion>
image={ {types.map((t, i) => (
<IllustrationNoContent style={{ width: 154, height: 154 }} /> <TypePanel data={t} key={i} index={i} />
} ))}
darkModeImage={ </Collapse>
<IllustrationNoContentDark )}
style={{ width: 154, height: 154 }} </>
/> );
} }
title="No types"
description={<div>Make your own custom data types.</div>} function TypePanel({ index, data }) {
/> const { types, deleteType, updateType } = useTypes();
</div> const { setUndoStack, setRedoStack } = useUndoRedo();
) : ( const [editField, setEditField] = useState({});
types.map((t, i) => (
<div id={`scroll_type_${i}`} key={i}> return (
<Collapse.Panel header={<div>{t.name}</div>} itemKey={`${i}`}> <div id={`scroll_type_${index}`}>
<div className="flex items-center mb-2.5"> <Collapse.Panel header={<div>{data.name}</div>} itemKey={`${index}`}>
<div className="text-md font-semibold">Name: </div> <div className="flex items-center mb-2.5">
<Input <div className="text-md font-semibold">Name: </div>
value={t.name} <Input
validateStatus={t.name === "" ? "error" : "default"} value={data.name}
placeholder="Name" validateStatus={data.name === "" ? "error" : "default"}
className="ms-2" placeholder="Name"
onChange={(value) => updateType(i, { name: value })} className="ms-2"
onFocus={(e) => setEditField({ name: e.target.value })} onChange={(value) => updateType(index, { name: value })}
onBlur={(e) => { onFocus={(e) => setEditField({ name: e.target.value })}
if (e.target.value === editField.name) return; onBlur={(e) => {
setUndoStack((prev) => [ if (e.target.value === editField.name) return;
...prev, setUndoStack((prev) => [
{ ...prev,
action: Action.EDIT, {
element: ObjectType.TYPE, action: Action.EDIT,
component: "self", element: ObjectType.TYPE,
tid: i, component: "self",
undo: editField, tid: index,
redo: { name: e.target.value }, undo: editField,
message: `Edit type name to ${e.target.value}`, redo: { name: e.target.value },
}, message: `Edit type name to ${e.target.value}`,
]); },
setRedoStack([]); ]);
}} setRedoStack([]);
/> }}
</div> />
{t.fields.map((f, j) => ( </div>
<Row gutter={6} key={j} className="hover-1 my-2"> {data.fields.map((f, j) => (
<Col span={10}> <Row gutter={6} key={j} className="hover-1 my-2">
<Input <Col span={10}>
value={f.name} <Input
validateStatus={f.name === "" ? "error" : "default"} value={f.name}
placeholder="Name" validateStatus={f.name === "" ? "error" : "default"}
onChange={(value) => placeholder="Name"
updateType(i, { onChange={(value) =>
fields: t.fields.map((e, id) => updateType(index, {
id === j ? { ...f, name: value } : e fields: data.fields.map((e, id) =>
), id === j ? { ...f, name: value } : e
}) ),
} })
onFocus={(e) => setEditField({ name: e.target.value })} }
onBlur={(e) => { onFocus={(e) => setEditField({ name: e.target.value })}
if (e.target.value === editField.name) return; onBlur={(e) => {
setUndoStack((prev) => [ if (e.target.value === editField.name) return;
...prev, setUndoStack((prev) => [
{ ...prev,
action: Action.EDIT, {
element: ObjectType.TYPE, action: Action.EDIT,
component: "field", element: ObjectType.TYPE,
tid: i, component: "field",
fid: j, tid: index,
undo: editField, fid: j,
redo: { name: e.target.value }, undo: editField,
message: `Edit type field name to ${e.target.value}`, redo: { name: e.target.value },
}, message: `Edit type field name to ${e.target.value}`,
]); },
setRedoStack([]); ]);
}} setRedoStack([]);
/> }}
</Col> />
<Col span={11}> </Col>
<Select <Col span={11}>
className="w-full" <Select
optionList={[ className="w-full"
...sqlDataTypes.map((value) => ({ optionList={[
label: value, ...sqlDataTypes.map((value) => ({
value: value, label: value,
})), value: value,
...types })),
.filter((type) => type.name !== t.name) ...types
.map((type) => ({ .filter((type) => type.name !== data.name)
label: type.name.toUpperCase(), .map((type) => ({
value: type.name.toUpperCase(), label: type.name.toUpperCase(),
})), value: type.name.toUpperCase(),
]} })),
filter ]}
value={f.type} filter
validateStatus={f.type === "" ? "error" : "default"} value={f.type}
placeholder="Type" validateStatus={f.type === "" ? "error" : "default"}
onChange={(value) => { placeholder="Type"
if (value === f.type) return; onChange={(value) => {
setUndoStack((prev) => [ if (value === f.type) return;
...prev, setUndoStack((prev) => [
{ ...prev,
action: Action.EDIT, {
element: ObjectType.TYPE, action: Action.EDIT,
component: "field", element: ObjectType.TYPE,
tid: i, component: "field",
fid: j, tid: index,
undo: { type: f.type }, fid: j,
redo: { type: value }, undo: { type: f.type },
message: `Edit type field type to ${value}`, redo: { type: value },
}, message: `Edit type field type to ${value}`,
]); },
setRedoStack([]); ]);
if (value === "ENUM" || value === "SET") { setRedoStack([]);
updateType(i, { if (value === "ENUM" || value === "SET") {
fields: t.fields.map((e, id) => updateType(index, {
id === j fields: data.fields.map((e, id) =>
? { id === j
...f, ? {
type: value, ...f,
values: f.values ? [...f.values] : [], type: value,
} values: f.values ? [...f.values] : [],
: e }
), : e
}); ),
} else if (isSized(value) || hasPrecision(value)) { });
updateType(i, { } else if (isSized(value) || hasPrecision(value)) {
fields: t.fields.map((e, id) => updateType(index, {
id === j fields: data.fields.map((e, id) =>
? { ...f, type: value, size: getSize(value) } id === j
: e ? { ...f, type: value, size: getSize(value) }
), : e
}); ),
} else { });
updateType(i, { } else {
fields: t.fields.map((e, id) => updateType(index, {
id === j ? { ...f, type: value } : e fields: data.fields.map((e, id) =>
), id === j ? { ...f, type: value } : e
}); ),
});
}
}}
></Select>
</Col>
<Col span={3}>
<Popover
content={
<div className="popover-theme w-[240px]">
{(f.type === "ENUM" || f.type === "SET") && (
<>
<div className="font-semibold mb-1">
{f.type} values
</div>
<TagInput
separator={[",", ", ", " ,"]}
value={f.values}
validateStatus={
!f.values || f.values.length === 0
? "error"
: "default"
} }
}} className="my-2"
></Select> placeholder="Use ',' for batch input"
</Col> onChange={(v) =>
<Col span={3}> updateType(index, {
<Popover fields: data.fields.map((e, id) =>
content={ id === j ? { ...f, values: v } : e
<div className="popover-theme w-[240px]"> ),
{(f.type === "ENUM" || f.type === "SET") && ( })
<> }
<div className="font-semibold mb-1"> onFocus={() => setEditField({ values: f.values })}
{f.type} values onBlur={() => {
</div> if (
<TagInput JSON.stringify(editField.values) ===
separator={[",", ", ", " ,"]} JSON.stringify(f.values)
value={f.values} )
validateStatus={ return;
!f.values || f.values.length === 0 setUndoStack((prev) => [
? "error" ...prev,
: "default" {
} action: Action.EDIT,
className="my-2" element: ObjectType.TYPE,
placeholder="Use ',' for batch input" component: "field",
onChange={(v) => tid: index,
updateType(i, { fid: j,
fields: t.fields.map((e, id) => undo: editField,
id === j ? { ...f, values: v } : e redo: { values: f.values },
), message: `Edit type field values to "${JSON.stringify(
}) f.values
} )}"`,
onFocus={() => },
setEditField({ values: f.values }) ]);
} setRedoStack([]);
onBlur={() => { }}
if ( />
JSON.stringify(editField.values) === </>
JSON.stringify(f.values) )}
) {isSized(f.type) && (
return; <>
setUndoStack((prev) => [ <div className="font-semibold">Size</div>
...prev, <InputNumber
{ className="my-2 w-full"
action: Action.EDIT, placeholder="Set length"
element: ObjectType.TYPE, value={f.size}
component: "field", onChange={(value) =>
tid: i, updateType(index, {
fid: j, fields: data.fields.map((e, id) =>
undo: editField, id === j ? { ...f, size: value } : e
redo: { values: f.values }, ),
message: `Edit type field values to "${JSON.stringify( })
f.values }
)}"`, onFocus={(e) =>
}, setEditField({ size: e.target.value })
]); }
setRedoStack([]); onBlur={(e) => {
}} if (e.target.value === editField.size) return;
/> setUndoStack((prev) => [
</> ...prev,
)} {
{isSized(f.type) && ( action: Action.EDIT,
<> element: ObjectType.TABLE,
<div className="font-semibold">Size</div> component: "field",
<InputNumber tid: index,
className="my-2 w-full" fid: j,
placeholder="Set length" undo: editField,
value={f.size} redo: { size: e.target.value },
onChange={(value) => message: `Edit type field size to ${e.target.value}`,
updateType(i, { },
fields: t.fields.map((e, id) => ]);
id === j ? { ...f, size: value } : e setRedoStack([]);
), }}
}) />
} </>
onFocus={(e) => )}
setEditField({ size: e.target.value }) {hasPrecision(f.type) && (
} <>
onBlur={(e) => { <div className="font-semibold">Precision</div>
if (e.target.value === editField.size) <Input
return; className="my-2 w-full"
setUndoStack((prev) => [ placeholder="Set precision: (size, d)"
...prev, validateStatus={
{ /^\(\d+,\s*\d+\)$|^$/.test(f.size)
action: Action.EDIT, ? "default"
element: ObjectType.TABLE, : "error"
component: "field", }
tid: i, value={f.size}
fid: j, onChange={(value) =>
undo: editField, updateType(index, {
redo: { size: e.target.value }, fields: data.fields.map((e, id) =>
message: `Edit type field size to ${e.target.value}`, id === j ? { ...f, size: value } : e
}, ),
]); })
setRedoStack([]); }
}} onFocus={(e) =>
/> setEditField({ size: e.target.value })
</> }
)} onBlur={(e) => {
{hasPrecision(f.type) && ( if (e.target.value === editField.size) return;
<> setUndoStack((prev) => [
<div className="font-semibold">Precision</div> ...prev,
<Input {
className="my-2 w-full" action: Action.EDIT,
placeholder="Set precision: (size, d)" element: ObjectType.TABLE,
validateStatus={ component: "field",
/^\(\d+,\s*\d+\)$|^$/.test(f.size) tid: index,
? "default" fid: j,
: "error" undo: editField,
} redo: { size: e.target.value },
value={f.size} message: `Edit type field precision to ${e.target.value}`,
onChange={(value) => },
updateType(i, { ]);
fields: t.fields.map((e, id) => setRedoStack([]);
id === j ? { ...f, size: value } : e }}
), />
}) </>
} )}
onFocus={(e) =>
setEditField({ size: e.target.value })
}
onBlur={(e) => {
if (e.target.value === editField.size)
return;
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.TABLE,
component: "field",
tid: i,
fid: j,
undo: editField,
redo: { size: e.target.value },
message: `Edit type field precision to ${e.target.value}`,
},
]);
setRedoStack([]);
}}
/>
</>
)}
<Button
icon={<IconDeleteStroked />}
block
type="danger"
onClick={() => {
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.TYPE,
component: "field_delete",
tid: i,
fid: j,
data: f,
message: `Delete field`,
},
]);
updateType(i, {
fields: t.fields.filter(
(field, k) => k !== j
),
});
}}
>
Delete field
</Button>
</div>
}
showArrow
trigger="click"
position="right"
>
<Button icon={<IconMore />} type="tertiary"></Button>
</Popover>
</Col>
</Row>
))}
<Card
bodyStyle={{ padding: "4px" }}
style={{ marginTop: "12px", marginBottom: "12px" }}
headerLine={false}
>
<Collapse>
<Collapse.Panel header="Comment" itemKey="1">
<TextArea
field="comment"
value={t.comment}
autosize
placeholder="Add comment"
rows={1}
onChange={(value) =>
updateType(i, { comment: value }, false)
}
onFocus={(e) =>
setEditField({ comment: e.target.value })
}
onBlur={(e) => {
if (e.target.value === editField.comment) return;
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.TYPE,
component: "self",
tid: i,
undo: editField,
redo: { comment: e.target.value },
message: `Edit type comment to ${e.target.value}`,
},
]);
setRedoStack([]);
}}
/>
</Collapse.Panel>
</Collapse>
</Card>
<Row gutter={6} className="mt-2">
<Col span={12}>
<Button <Button
icon={<IconPlus />} icon={<IconDeleteStroked />}
block
type="danger"
onClick={() => { onClick={() => {
setUndoStack((prev) => [ setUndoStack((prev) => [
...prev, ...prev,
{ {
action: Action.EDIT, action: Action.EDIT,
element: ObjectType.TYPE, element: ObjectType.TYPE,
component: "field_add", component: "field_delete",
tid: i, tid: index,
message: `Add field to type`, fid: j,
data: f,
message: `Delete field`,
}, },
]); ]);
setRedoStack([]); updateType(index, {
updateType(i, { fields: data.fields.filter((field, k) => k !== j),
fields: [
...t.fields,
{
name: "",
type: "",
},
],
}); });
}} }}
block
> >
Add field Delete field
</Button> </Button>
</Col> </div>
<Col span={12}> }
<Button showArrow
icon={<IconDeleteStroked />} trigger="click"
type="danger" position="right"
onClick={() => { >
Toast.success(`Type deleted!`); <Button icon={<IconMore />} type="tertiary" />
deleteType(i); </Popover>
}} </Col>
block </Row>
> ))}
Delete <Card
</Button> bodyStyle={{ padding: "4px" }}
</Col> style={{ marginTop: "12px", marginBottom: "12px" }}
</Row> headerLine={false}
</Collapse.Panel> >
</div> <Collapse>
)) <Collapse.Panel header="Comment" itemKey="1">
)} <TextArea
</Collapse> field="comment"
</> value={data.comment}
autosize
placeholder="Add comment"
rows={1}
onChange={(value) =>
updateType(index, { comment: value }, false)
}
onFocus={(e) => setEditField({ comment: e.target.value })}
onBlur={(e) => {
if (e.target.value === editField.comment) return;
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.TYPE,
component: "self",
tid: index,
undo: editField,
redo: { comment: e.target.value },
message: `Edit type comment to ${e.target.value}`,
},
]);
setRedoStack([]);
}}
/>
</Collapse.Panel>
</Collapse>
</Card>
<Row gutter={6} className="mt-2">
<Col span={12}>
<Button
icon={<IconPlus />}
onClick={() => {
setUndoStack((prev) => [
...prev,
{
action: Action.EDIT,
element: ObjectType.TYPE,
component: "field_add",
tid: index,
message: `Add field to type`,
},
]);
setRedoStack([]);
updateType(index, {
fields: [
...data.fields,
{
name: "",
type: "",
},
],
});
}}
block
>
Add field
</Button>
</Col>
<Col span={12}>
<Button
icon={<IconDeleteStroked />}
type="danger"
onClick={() => {
Toast.success(`Type deleted!`);
deleteType(index);
}}
block
>
Delete
</Button>
</Col>
</Row>
</Collapse.Panel>
</div>
); );
} }

View File

@ -183,7 +183,7 @@ export default function BugReport() {
} }
theme="borderless" theme="borderless"
onClick={changeTheme} onClick={changeTheme}
></Button> />
</div> </div>
</div> </div>
<hr <hr

View File

@ -123,7 +123,7 @@ export default function Shortcuts() {
} }
theme="borderless" theme="borderless"
onClick={changeTheme} onClick={changeTheme}
></Button> />
<div className="ms-2 lg:inline md:inline sm:hidden"> <div className="ms-2 lg:inline md:inline sm:hidden">
<AutoComplete <AutoComplete
prefix={<IconSearch />} prefix={<IconSearch />}

View File

@ -286,7 +286,7 @@ export default function Survey() {
} }
theme="borderless" theme="borderless"
onClick={changeTheme} onClick={changeTheme}
></Button> />
</div> </div>
</div> </div>
<hr <hr