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 useAreas from "../hooks/useAreas";
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 [editField, setEditField] = useState({});
const { layout } = useLayout();
const { settings } = useSettings();
const { transform } = useTransform();
const { setSaveState } = useSaveState();
const { updateArea, deleteArea } = useAreas();
const { setUndoStack, setRedoStack } = useUndoRedo();
const { selectedElement, setSelectedElement } = useSelect();
const handleMouseDown = (e, dir) => {
props.setResize({ id: props.areaData.id, dir: dir });
props.setInitCoords({
x: props.areaData.x,
y: props.areaData.y,
width: props.areaData.width,
height: props.areaData.height,
mouseX: e.clientX / props.zoom,
mouseY: e.clientY / props.zoom,
const handleResize = (e, dir) => {
setResize({ id: data.id, dir: dir });
setInitCoords({
x: data.x,
y: data.y,
width: data.width,
height: data.height,
mouseX: e.clientX / transform.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 (
<g
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => {
setHovered(false);
}}
onMouseLeave={() => setHovered(false)}
>
<foreignObject
key={props.areaData.id}
x={props.areaData.x}
y={props.areaData.y}
width={props.areaData.width > 0 ? props.areaData.width : 0}
height={props.areaData.height > 0 ? props.areaData.height : 0}
onMouseDown={props.onMouseDown}
key={data.id}
x={data.x}
y={data.y}
width={data.width > 0 ? data.width : 0}
height={data.height > 0 ? data.height : 0}
onMouseDown={onMouseDown}
>
<div
className={`border-2 ${
hovered
? "border-dashed border-blue-500"
: selectedElement.element === ObjectType.AREA &&
selectedElement.id === props.areaData.id
selectedElement.id === data.id
? "border-blue-500"
: "border-slate-400"
} w-full h-full cursor-move rounded relative`}
>
<div
className="opacity-40 w-fill p-2 h-full"
style={{ backgroundColor: props.areaData.color }}
style={{ backgroundColor: data.color }}
/>
</div>
<div className="text-color absolute top-2 left-3 select-none">
{props.areaData.name}
{data.name}
</div>
{(hovered ||
(selectedElement.element === ObjectType.AREA &&
selectedElement.id === props.areaData.id &&
selectedElement.open &&
!layout.sidebar)) && (
{(hovered || (areaIsSelected() && !layout.sidebar)) && (
<div className="absolute top-2 right-3">
<Popover
visible={
selectedElement.element === ObjectType.AREA &&
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);
}}
visible={areaIsSelected() && !layout.sidebar}
onClickOutSide={onClickOutSide}
stopPropagation
content={
<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>
}
content={<EditPopoverContent data={data} />}
trigger="custom"
position="rightTop"
showArrow
@ -265,29 +134,8 @@ export default function Area(props) {
backgroundColor: "#2f68ad",
opacity: "0.7",
}}
onClick={() => {
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>
onClick={edit}
/>
</Popover>
</div>
)}
@ -295,47 +143,196 @@ export default function Area(props) {
{hovered && (
<>
<circle
cx={props.areaData.x}
cy={props.areaData.y}
cx={data.x}
cy={data.y}
r={6}
fill={settings.mode === "light" ? "white" : "rgb(28, 31, 35)"}
stroke="#5891db"
strokeWidth={2}
cursor="nwse-resize"
onMouseDown={(e) => handleMouseDown(e, "tl")}
onMouseDown={(e) => handleResize(e, "tl")}
/>
<circle
cx={props.areaData.x + props.areaData.width}
cy={props.areaData.y}
cx={data.x + data.width}
cy={data.y}
r={6}
fill={settings.mode === "light" ? "white" : "rgb(28, 31, 35)"}
stroke="#5891db"
strokeWidth={2}
cursor="nesw-resize"
onMouseDown={(e) => handleMouseDown(e, "tr")}
onMouseDown={(e) => handleResize(e, "tr")}
/>
<circle
cx={props.areaData.x}
cy={props.areaData.y + props.areaData.height}
cx={data.x}
cy={data.y + data.height}
r={6}
fill={settings.mode === "light" ? "white" : "rgb(28, 31, 35)"}
stroke="#5891db"
strokeWidth={2}
cursor="nesw-resize"
onMouseDown={(e) => handleMouseDown(e, "bl")}
onMouseDown={(e) => handleResize(e, "bl")}
/>
<circle
cx={props.areaData.x + props.areaData.width}
cy={props.areaData.y + props.areaData.height}
cx={data.x + data.width}
cy={data.y + data.height}
r={6}
fill={settings.mode === "light" ? "white" : "rgb(28, 31, 35)"}
stroke="#5891db"
strokeWidth={2}
cursor="nwse-resize"
onMouseDown={(e) => handleMouseDown(e, "br")}
onMouseDown={(e) => handleResize(e, "br")}
/>
</>
)}
</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 {
Empty,
Row,
Col,
AutoComplete,
@ -9,10 +8,6 @@ import {
Popover,
Toast,
} from "@douyinfe/semi-ui";
import {
IllustrationNoContent,
IllustrationNoContentDark,
} from "@douyinfe/semi-illustrations";
import {
IconPlus,
IconSearch,
@ -29,26 +24,21 @@ import {
import useUndoRedo from "../hooks/useUndoRedo";
import useAreas from "../hooks/useAreas";
import useSaveState from "../hooks/useSaveState";
import NoElements from "./NoElements";
export default function AreaOverview() {
export default function AreasOverview() {
const { setSaveState } = useSaveState();
const { areas, addArea, deleteArea, updateArea } = useAreas();
const { setUndoStack, setRedoStack } = useUndoRedo();
const [editField, setEditField] = useState({});
const [value, setValue] = useState("");
const [searchText, setSearchText] = useState("");
const [filteredResult, setFilteredResult] = useState(
areas.map((t) => {
return t.name;
})
areas.map((t) => t.name)
);
const handleStringSearch = (value) => {
setFilteredResult(
areas
.map((t) => {
return t.name;
})
.filter((i) => i.includes(value))
areas.map((t) => t.name).filter((i) => i.includes(value))
);
};
@ -58,7 +48,7 @@ export default function AreaOverview() {
<Col span={16}>
<AutoComplete
data={filteredResult}
value={value}
value={searchText}
showClear
prefix={<IconSearch />}
placeholder="Search..."
@ -66,7 +56,7 @@ export default function AreaOverview() {
<div className="p-3 popover-theme">No areas found</div>
}
onSearch={(v) => handleStringSearch(v)}
onChange={(v) => setValue(v)}
onChange={(v) => setSearchText(v)}
onSelect={(v) => {
const { id } = areas.find((t) => t.name === v);
document
@ -77,24 +67,16 @@ export default function AreaOverview() {
/>
</Col>
<Col span={8}>
<Button icon={<IconPlus />} block onClick={() => addArea()}>
<Button icon={<IconPlus />} block onClick={addArea}>
Add area
</Button>
</Col>
</Row>
{areas.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 subject areas"
description="Add subject areas to compartmentalize tables!"
/>
</div>
<NoElements
title="No subject areas"
text="Add subject areas to organize tables!"
/>
) : (
<div className="p-2">
{areas.map((a, i) => (
@ -236,7 +218,7 @@ export default function AreaOverview() {
Toast.success(`Area deleted!`);
deleteArea(i, true);
}}
></Button>
/>
</Col>
</Row>
))}

View File

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

View File

@ -1725,7 +1725,7 @@ export default function ControlPanel({
})
}
limit={1}
></Upload>
/>
{error.type === STATUS.ERROR ? (
<Banner
type="danger"
@ -1803,7 +1803,7 @@ export default function ControlPanel({
]}
onChange={(e) => setData((prev) => ({ ...prev, dbms: e }))}
className="w-full"
></Select>
/>
<Checkbox
aria-label="overwrite checkbox"
checked={data.overwrite}
@ -1914,7 +1914,7 @@ export default function ControlPanel({
}}
>
<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}
</td>
<td className="py-1">
@ -2085,7 +2085,7 @@ export default function ControlPanel({
className="hover-1"
>
<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>
</List.Item>
@ -2166,7 +2166,7 @@ export default function ControlPanel({
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>
</Tooltip>
<Tooltip content="Zoom out" position="bottom">
@ -2176,7 +2176,7 @@ export default function ControlPanel({
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>
</Tooltip>
<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"
onClick={() => setSidesheet(SIDESHEET.TODO)}
>
<i className="fa-regular fa-calendar-check"></i>
<i className="fa-regular fa-calendar-check" />
</button>
</Tooltip>
</div>
@ -2401,11 +2401,7 @@ export default function ControlPanel({
<Dropdown.Menu>
<Dropdown.Item
icon={
layout.header ? (
<IconCheckboxTick />
) : (
<div className="px-2"></div>
)
layout.header ? <IconCheckboxTick /> : <div className="px-2" />
}
onClick={() => invertLayout("header")}
>
@ -2413,11 +2409,7 @@ export default function ControlPanel({
</Dropdown.Item>
<Dropdown.Item
icon={
layout.sidebar ? (
<IconCheckboxTick />
) : (
<div className="px-2"></div>
)
layout.sidebar ? <IconCheckboxTick /> : <div className="px-2" />
}
onClick={() => invertLayout("sidebar")}
>
@ -2425,11 +2417,7 @@ export default function ControlPanel({
</Dropdown.Item>
<Dropdown.Item
icon={
layout.issues ? (
<IconCheckboxTick />
) : (
<div className="px-2"></div>
)
layout.issues ? <IconCheckboxTick /> : <div className="px-2" />
}
onClick={() => invertLayout("issues")}
>
@ -2437,7 +2425,7 @@ export default function ControlPanel({
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
icon={<div className="px-2"></div>}
icon={<div className="px-2" />}
onClick={() => {
if (layout.fullscreen) {
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>
<Divider align="center" layout="vertical" />
<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>
</div>
<Tooltip content="Exit">
@ -49,7 +49,7 @@ export default function Controls() {
exitFullscreen();
}}
>
<i className="bi bi-fullscreen-exit"></i>
<i className="bi bi-fullscreen-exit" />
</button>
</Tooltip>
</div>

View File

@ -39,7 +39,7 @@ export default function Issues() {
className="mt-1"
>
<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
</div>
</Badge>

View File

@ -48,7 +48,9 @@ export default function Navbar() {
</div>
<hr />
<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}
onCancel={() => setOpenMenu(false)}
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}`);
textarea.style.height = "0";
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 });
};
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 (
<g
onMouseEnter={() => setHovered(true)}
@ -100,28 +140,10 @@ export default function Note({ data, onMouseDown }) {
height: data.height,
})
}
onBlur={(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([]);
}}
onBlur={handleBlur}
className="w-full resize-none outline-none overflow-y-hidden border-none select-none"
style={{ backgroundColor: data.color }}
></textarea>
/>
{(hovered ||
(selectedElement.element === ObjectType.NOTE &&
selectedElement.id === data.id &&
@ -253,26 +275,8 @@ export default function Note({ data, onMouseDown }) {
backgroundColor: "#2f68ad",
opacity: "0.7",
}}
onClick={() => {
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>
onClick={edit}
/>
</Popover>
</div>
)}

View File

@ -1,6 +1,5 @@
import { useState } from "react";
import {
Empty,
Row,
Col,
Button,
@ -11,10 +10,6 @@ import {
Input,
Toast,
} from "@douyinfe/semi-ui";
import {
IllustrationNoContent,
IllustrationNoContentDark,
} from "@douyinfe/semi-illustrations";
import {
IconDeleteStroked,
IconPlus,
@ -24,26 +19,21 @@ import {
import { noteThemes, Action, ObjectType } from "../data/constants";
import useUndoRedo from "../hooks/useUndoRedo";
import useNotes from "../hooks/useNotes";
import NoElements from "./NoElements";
export default function NotesOverview() {
const { notes, updateNote, addNote, deleteNote } = useNotes();
const { setUndoStack, setRedoStack } = useUndoRedo();
const [value, setValue] = useState("");
const [searchText, setSearchText] = useState("");
const [editField, setEditField] = useState({});
const [activeKey, setActiveKey] = useState("");
const [filteredResult, setFilteredResult] = useState(
notes.map((t) => {
return t.title;
})
notes.map((t) => t.title)
);
const handleStringSearch = (value) => {
setFilteredResult(
notes
.map((t) => {
return t.title;
})
.filter((i) => i.includes(value))
notes.map((t) => t.title).filter((i) => i.includes(value))
);
};
@ -53,7 +43,7 @@ export default function NotesOverview() {
<Col span={16}>
<AutoComplete
data={filteredResult}
value={value}
value={searchText}
showClear
prefix={<IconSearch />}
placeholder="Search..."
@ -61,7 +51,7 @@ export default function NotesOverview() {
<div className="p-3 popover-theme">No notes found</div>
}
onSearch={(v) => handleStringSearch(v)}
onChange={(v) => setValue(v)}
onChange={(v) => setSearchText(v)}
onSelect={(v) => {
const { id } = notes.find((t) => t.title === v);
setActiveKey(`${id}`);
@ -79,18 +69,7 @@ export default function NotesOverview() {
</Col>
</Row>
{notes.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 text notes"
description="Add notes cuz why not!"
/>
</div>
<NoElements title="No text notes" text="Add notes cuz why not!" />
) : (
<Collapse
activeKey={activeKey}
@ -218,7 +197,7 @@ export default function NotesOverview() {
Toast.success(`Note deleted!`);
deleteNote(i, true);
}}
></Button>
/>
</div>
</div>
</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 { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
import { ListPlugin } from "@lexical/react/LexicalListPlugin";
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
import { ClearEditorPlugin } from "@lexical/react/LexicalClearEditorPlugin";
import { TRANSFORMERS } from "@lexical/markdown";
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
import ToolbarPlugin from "../plugins/ToolbarPlugin";
import ListMaxIndentLevelPlugin from "../plugins/ListMaxIndentLevelPlugin";
import CodeHighlightPlugin from "../plugins/CodeHighlightPlugin";

View File

@ -1,8 +1,8 @@
import { Tabs } from "@douyinfe/semi-ui";
import { Tab } from "../data/constants";
import TableOverview from "./TableOverview";
import ReferenceOverview from "./ReferenceOverview";
import AreaOverview from "./AreaOverview";
import TablesOverview from "./TablesOverview";
import RelationshipsOverview from "./RelationshipsOverview";
import AreasOverview from "./AreasOverview";
import NotesOverview from "./NotesOverview";
import TypesOverview from "./TypesOverview";
import Issues from "./Issues";
@ -20,12 +20,13 @@ export default function SidePanel({ width, resize, setResize }) {
{ tab: "Notes", itemKey: Tab.NOTES },
{ tab: "Types", itemKey: Tab.TYPES },
];
const contentList = [
<TableOverview key={1} />,
<ReferenceOverview key={2} />,
<AreaOverview key={3} />,
<NotesOverview key={4} />,
<TypesOverview key={5} />,
<TablesOverview key="tables" />,
<RelationshipsOverview key="relationships" />,
<AreasOverview key="areas" />,
<NotesOverview key="notes" />,
<TypesOverview key="types" />,
];
return (
@ -57,7 +58,7 @@ export default function SidePanel({ width, resize, setResize }) {
</div>
<div
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)}
>

View File

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

View File

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

View File

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

View File

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

View File

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