drawDB/src/components/control_panel.jsx

1110 lines
33 KiB
React
Raw Normal View History

2023-09-19 20:48:46 +08:00
import { React, useContext, useState } from "react";
2023-09-19 20:47:16 +08:00
import {
2023-09-19 20:48:20 +08:00
IconCaretdown,
IconChevronRight,
IconShareStroked,
IconChevronUp,
IconChevronDown,
2023-09-19 20:48:22 +08:00
IconCheckboxTick,
2023-09-19 20:49:18 +08:00
IconSaveStroked,
IconUndo,
IconRedo,
2023-09-19 20:48:20 +08:00
} from "@douyinfe/semi-icons";
import { Link } from "react-router-dom";
import icon from "../assets/icon_dark_64.png";
import {
Avatar,
AvatarGroup,
Button,
Divider,
Dropdown,
2023-09-19 20:49:36 +08:00
InputNumber,
2023-09-19 20:48:26 +08:00
Image,
Modal,
2023-09-19 20:48:53 +08:00
Spin,
2023-09-19 20:49:20 +08:00
Input,
2023-09-19 20:49:28 +08:00
Upload,
Banner,
2023-09-19 20:49:37 +08:00
Toast,
2023-09-19 20:48:20 +08:00
} from "@douyinfe/semi-ui";
2023-09-19 20:48:28 +08:00
import { toPng, toJpeg, toSvg } from "html-to-image";
2023-09-19 20:48:26 +08:00
import { saveAs } from "file-saver";
2023-09-19 20:49:28 +08:00
import {
2023-09-19 20:49:31 +08:00
jsonDiagramIsValid,
2023-09-19 20:49:28 +08:00
enterFullscreen,
exitFullscreen,
2023-09-19 20:49:31 +08:00
ddbDiagramIsValid,
2023-09-19 20:49:39 +08:00
dataURItoBlob,
2023-09-19 20:49:28 +08:00
} from "../utils";
2023-09-19 20:49:20 +08:00
import {
AreaContext,
LayoutContext,
NoteContext,
SettingsContext,
TableContext,
2023-09-19 20:49:41 +08:00
UndoRedoContext,
2023-09-19 20:49:20 +08:00
} from "../pages/editor";
import { IconAddTable, IconAddArea, IconAddNote } from "./custom_icons";
2023-09-19 20:49:57 +08:00
import { ObjectType, Action } from "../data/data";
2023-09-19 20:49:20 +08:00
import CodeMirror from "@uiw/react-codemirror";
import { json } from "@codemirror/lang-json";
2023-09-19 20:49:24 +08:00
import jsPDF from "jspdf";
2023-09-19 20:46:48 +08:00
2023-09-19 20:48:34 +08:00
export default function ControlPanel(props) {
2023-09-19 20:49:20 +08:00
const MODAL = {
NONE: 0,
IMG: 1,
CODE: 2,
2023-09-19 20:49:28 +08:00
IMPORT: 3,
2023-09-19 20:49:20 +08:00
};
2023-09-19 20:49:57 +08:00
const STATUS = {
2023-09-19 20:49:29 +08:00
NONE: 0,
WARNING: 1,
ERROR: 2,
OK: 3,
};
2023-09-19 20:49:20 +08:00
const [visible, setVisible] = useState(MODAL.NONE);
const [exportData, setExportData] = useState({
data: "",
filename: `diagram_${new Date().toISOString()}`,
extension: "",
});
2023-09-19 20:49:29 +08:00
const [error, setError] = useState({
2023-09-19 20:49:57 +08:00
type: STATUS.NONE,
2023-09-19 20:49:29 +08:00
message: "",
});
const [data, setData] = useState(null);
2023-09-19 20:48:53 +08:00
const { layout, setLayout } = useContext(LayoutContext);
2023-09-19 20:49:36 +08:00
const { settings, setSettings } = useContext(SettingsContext);
2023-09-19 20:49:57 +08:00
const {
relationships,
tables,
setTables,
addTable,
moveTable,
deleteTable,
setRelationships,
addRelationship,
deleteRelationship,
} = useContext(TableContext);
const { notes, setNotes, moveNote, addNote, deleteNote } =
useContext(NoteContext);
const { areas, setAreas, moveArea, addArea, deleteArea } =
useContext(AreaContext);
2023-09-19 20:49:41 +08:00
const { undoStack, redoStack, setUndoStack, setRedoStack } =
useContext(UndoRedoContext);
2023-09-19 20:48:26 +08:00
2023-09-19 20:49:46 +08:00
const invertLayout = (component) =>
setLayout((prev) => ({ ...prev, [component]: !prev[component] }));
const diagramIsEmpty = () => {
return (
tables.length === 0 &&
relationships.length === 0 &&
notes.length === 0 &&
areas.length === 0
);
};
const overwriteDiagram = () => {
setTables(data.tables);
setRelationships(data.relationships);
setAreas(data.subjectAreas);
setNotes(data.notes);
};
const undo = () => {
if (undoStack.length === 0) return;
const a = undoStack.pop();
if (a.action === Action.ADD) {
if (a.element === ObjectType.TABLE) {
2023-09-19 20:49:57 +08:00
deleteTable(tables[tables.length - 1].id, false);
2023-09-19 20:49:46 +08:00
} else if (a.element === ObjectType.AREA) {
2023-09-19 20:49:57 +08:00
deleteArea(areas[areas.length - 1].id, false);
2023-09-19 20:49:46 +08:00
} else if (a.element === ObjectType.NOTE) {
2023-09-19 20:49:57 +08:00
deleteNote(notes[notes.length - 1].id, false);
2023-09-19 20:49:52 +08:00
} else if (a.element === ObjectType.RELATIONSHIP) {
2023-09-19 20:49:57 +08:00
deleteRelationship(a.data.id, false);
2023-09-19 20:49:46 +08:00
}
setRedoStack((prev) => [...prev, a]);
} else if (a.action === Action.MOVE) {
if (a.element === ObjectType.TABLE) {
setRedoStack((prev) => [
...prev,
{ ...a, x: tables[a.id].x, y: tables[a.id].y },
]);
moveTable(a.id, a.x, a.y);
} else if (a.element === ObjectType.AREA) {
setRedoStack((prev) => [
...prev,
{ ...a, x: areas[a.id].x, y: areas[a.id].y },
]);
moveArea(a.id, a.x, a.y);
} else if (a.element === ObjectType.NOTE) {
setRedoStack((prev) => [
...prev,
{ ...a, x: notes[a.id].x, y: notes[a.id].y },
]);
moveNote(a.id, a.x, a.y);
}
2023-09-19 20:49:52 +08:00
} else if (a.action === Action.DELETE) {
if (a.element === ObjectType.TABLE) {
2023-09-19 20:49:57 +08:00
addTable(false, a.data);
2023-09-19 20:49:52 +08:00
} else if (a.element === ObjectType.RELATIONSHIP) {
2023-09-19 20:49:57 +08:00
addRelationship(false, a.data);
} else if (a.element === ObjectType.NOTE) {
addNote(false, a.data);
} else if (a.element === ObjectType.AREA) {
addArea(false, a.data);
2023-09-19 20:49:52 +08:00
}
setRedoStack((prev) => [...prev, a]);
2023-09-19 20:50:00 +08:00
} else if (a.action === Action.EDIT) {
if (a.element === ObjectType.AREA) {
setAreas((prev) =>
prev.map((n) => {
if (n.id === a.data.undo.id) {
return a.data.undo;
}
return n;
})
);
}
setRedoStack((prev) => [...prev, a]);
} else if (a.action === Action.PAN) {
console.log(a)
setSettings((prev) => ({
...prev,
pan: a.data.undo
}));
setRedoStack((prev) => [...prev, a]);
2023-09-19 20:49:46 +08:00
}
};
const redo = () => {
if (redoStack.length === 0) return;
const a = redoStack.pop();
if (a.action === Action.ADD) {
if (a.element === ObjectType.TABLE) {
2023-09-19 20:49:57 +08:00
addTable(false);
2023-09-19 20:49:46 +08:00
} else if (a.element === ObjectType.AREA) {
2023-09-19 20:49:57 +08:00
addArea(false);
2023-09-19 20:49:46 +08:00
} else if (a.element === ObjectType.NOTE) {
2023-09-19 20:49:57 +08:00
addNote(false);
2023-09-19 20:49:52 +08:00
} else if (a.element === ObjectType.RELATIONSHIP) {
2023-09-19 20:49:57 +08:00
addRelationship(false, a.data);
2023-09-19 20:49:46 +08:00
}
setUndoStack((prev) => [...prev, a]);
} else if (a.action === Action.MOVE) {
if (a.element === ObjectType.TABLE) {
setUndoStack((prev) => [
...prev,
{ ...a, x: tables[a.id].x, y: tables[a.id].y },
]);
moveTable(a.id, a.x, a.y);
} else if (a.element === ObjectType.AREA) {
setUndoStack((prev) => [
...prev,
{ ...a, x: areas[a.id].x, y: areas[a.id].y },
]);
moveArea(a.id, a.x, a.y);
} else if (a.element === ObjectType.NOTE) {
setUndoStack((prev) => [
...prev,
{ ...a, x: notes[a.id].x, y: notes[a.id].y },
]);
moveNote(a.id, a.x, a.y);
}
2023-09-19 20:49:52 +08:00
} else if (a.action === Action.DELETE) {
if (a.element === ObjectType.TABLE) {
2023-09-19 20:49:57 +08:00
deleteTable(a.data.id, false);
2023-09-19 20:49:52 +08:00
} else if (a.element === ObjectType.RELATIONSHIP) {
2023-09-19 20:49:57 +08:00
deleteRelationship(a.data.id, false);
} else if (a.element === ObjectType.NOTE) {
deleteNote(a.data.id, false);
} else if (a.element === ObjectType.AREA) {
deleteArea(a.data.id, false);
2023-09-19 20:49:52 +08:00
}
setUndoStack((prev) => [...prev, a]);
2023-09-19 20:50:00 +08:00
} else if (a.action === Action.EDIT) {
if (a.element === ObjectType.AREA) {
setAreas((prev) =>
prev.map((n) => {
if (n.id === a.data.redo.id) {
return a.data.redo;
}
return n;
})
);
}
setUndoStack((prev) => [...prev, a]);
} else if (a.action === Action.PAN) {
setSettings((prev) => ({
...prev,
pan: a.data.redo
}));
setUndoStack((prev) => [...prev, a]);
2023-09-19 20:49:46 +08:00
}
};
2023-09-19 20:48:26 +08:00
const menu = {
File: {
New: {
children: [],
function: () => console.log("New"),
},
"New window": {
children: [],
function: () => {},
},
Save: {
children: [],
function: () => {},
},
"Save as": {
children: [],
function: () => {},
},
Share: {
children: [],
function: () => {},
},
Rename: {
children: [],
function: () => {},
},
Import: {
children: [],
2023-09-19 20:49:28 +08:00
function: () => {
setVisible(MODAL.IMPORT);
},
2023-09-19 20:48:26 +08:00
},
"Export as": {
children: [
{
2023-09-19 20:48:28 +08:00
PNG: () => {
2023-09-19 20:48:26 +08:00
toPng(document.getElementById("canvas")).then(function (dataUrl) {
2023-09-19 20:49:20 +08:00
setExportData((prev) => ({
...prev,
data: dataUrl,
extension: "png",
}));
2023-09-19 20:48:26 +08:00
});
2023-09-19 20:49:20 +08:00
setVisible(MODAL.IMG);
2023-09-19 20:48:27 +08:00
},
},
{
2023-09-19 20:48:28 +08:00
JPEG: () => {
2023-09-19 20:48:27 +08:00
toJpeg(document.getElementById("canvas"), { quality: 0.95 }).then(
function (dataUrl) {
2023-09-19 20:49:20 +08:00
setExportData((prev) => ({
...prev,
data: dataUrl,
extension: "jpeg",
}));
2023-09-19 20:48:27 +08:00
}
);
2023-09-19 20:49:20 +08:00
setVisible(MODAL.IMG);
},
},
{
JSON: () => {
setVisible(MODAL.CODE);
const result = JSON.stringify(
{
tables: tables,
relationships: relationships,
notes: notes,
2023-09-19 20:49:28 +08:00
subjectAreas: areas,
2023-09-19 20:49:20 +08:00
},
null,
2
);
setExportData((prev) => ({
...prev,
data: result,
extension: "json",
}));
2023-09-19 20:48:26 +08:00
},
},
2023-09-19 20:48:28 +08:00
{
SVG: () => {
const filter = (node) => node.tagName !== "i";
toSvg(document.getElementById("canvas"), { filter: filter }).then(
function (dataUrl) {
2023-09-19 20:49:20 +08:00
setExportData((prev) => ({
...prev,
data: dataUrl,
extension: "svg",
}));
2023-09-19 20:48:28 +08:00
}
);
2023-09-19 20:49:20 +08:00
setVisible(MODAL.IMG);
2023-09-19 20:48:28 +08:00
},
},
2023-09-19 20:49:24 +08:00
{
PDF: () => {
const canvas = document.getElementById("canvas");
2023-09-19 20:49:28 +08:00
toJpeg(canvas).then(function (dataUrl) {
const doc = new jsPDF("l", "px", [
canvas.offsetWidth,
canvas.offsetHeight,
]);
doc.addImage(
dataUrl,
"jpeg",
0,
0,
canvas.offsetWidth,
canvas.offsetHeight
);
doc.save(`${exportData.filename}.pdf`);
});
2023-09-19 20:49:24 +08:00
},
},
2023-09-19 20:49:30 +08:00
{
DRAWDB: () => {
const result = JSON.stringify(
{
author: "Unnamed",
project: "Untitled",
filename: "Untitled",
2023-09-19 20:49:31 +08:00
date: new Date().toISOString(),
2023-09-19 20:49:30 +08:00
tables: tables,
relationships: relationships,
notes: notes,
subjectAreas: areas,
},
null,
2
);
const blob = new Blob([result], {
type: "text/plain;charset=utf-8",
});
saveAs(blob, `${exportData.filename}.ddb`);
},
},
2023-09-19 20:48:26 +08:00
],
function: () => {},
},
"Export source": {
children: [
{ MySQL: () => {} },
{ PostgreSQL: () => {} },
{ DBML: () => {} },
],
function: () => {},
},
Properties: {
children: [],
function: () => {},
},
Close: {
children: [],
function: () => {},
},
},
Edit: {
Undo: {
children: [],
2023-09-19 20:49:46 +08:00
function: undo,
2023-09-19 20:48:26 +08:00
},
Redo: {
children: [],
2023-09-19 20:49:46 +08:00
function: redo,
2023-09-19 20:48:26 +08:00
},
2023-09-19 20:49:39 +08:00
Clear: {
children: [],
function: () => {
setTables([]);
setRelationships([]);
setAreas([]);
setNotes([]);
},
},
2023-09-19 20:49:46 +08:00
Edit: {
children: [],
function: () => {},
},
2023-09-19 20:48:26 +08:00
Cut: {
children: [],
function: () => {},
},
Copy: {
children: [],
function: () => {},
},
2023-09-19 20:49:39 +08:00
Paste: {
2023-09-19 20:48:26 +08:00
children: [],
function: () => {},
},
2023-09-19 20:49:46 +08:00
Duplicate: {
children: [],
function: () => {},
},
Delete: {
children: [],
function: () => {},
},
2023-09-19 20:49:39 +08:00
"Copy as image": {
2023-09-19 20:48:26 +08:00
children: [],
2023-09-19 20:49:39 +08:00
function: () => {
toPng(document.getElementById("canvas")).then(function (dataUrl) {
const blob = dataURItoBlob(dataUrl);
navigator.clipboard
.write([new ClipboardItem({ "image/png": blob })])
.then(() => {
Toast.success("Copied to clipboard.");
})
.catch((error) => {
Toast.error("Could not copy to clipboard.");
});
});
},
2023-09-19 20:48:26 +08:00
},
},
View: {
2023-09-19 20:49:46 +08:00
Header: {
2023-09-19 20:48:26 +08:00
children: [],
2023-09-19 20:49:46 +08:00
function: () =>
setLayout((prev) => ({ ...prev, header: !prev.header })),
2023-09-19 20:48:26 +08:00
},
2023-09-19 20:49:46 +08:00
Sidebar: {
2023-09-19 20:48:26 +08:00
children: [],
2023-09-19 20:49:37 +08:00
function: () =>
2023-09-19 20:49:46 +08:00
setLayout((prev) => ({ ...prev, sidebar: !prev.sidebar })),
2023-09-19 20:48:26 +08:00
},
2023-09-19 20:49:46 +08:00
Issues: {
2023-09-19 20:48:26 +08:00
children: [],
2023-09-19 20:49:46 +08:00
function: () =>
setLayout((prev) => ({ ...prev, issues: !prev.issues })),
2023-09-19 20:48:26 +08:00
},
2023-09-19 20:49:46 +08:00
Services: {
2023-09-19 20:48:26 +08:00
children: [],
2023-09-19 20:49:46 +08:00
function: () =>
setLayout((prev) => ({ ...prev, services: !prev.services })),
2023-09-19 20:48:26 +08:00
},
"Strict mode": {
children: [],
2023-09-19 20:49:14 +08:00
function: () => {
setSettings((prev) => ({ ...prev, strictMode: !prev.strictMode }));
2023-09-19 20:49:37 +08:00
Toast.success(`Stict mode is ${settings.strictMode ? "on" : "off"}.`);
2023-09-19 20:49:14 +08:00
},
2023-09-19 20:48:26 +08:00
},
2023-09-19 20:49:16 +08:00
"Field summary": {
children: [],
function: () => {
2023-09-19 20:49:18 +08:00
setSettings((prev) => ({
...prev,
showFieldSummary: !prev.showFieldSummary,
}));
2023-09-19 20:49:37 +08:00
Toast.success(
`Field summary is ${settings.showFieldSummary ? "off" : "on"}.`
);
2023-09-19 20:49:16 +08:00
},
},
2023-09-19 20:48:26 +08:00
"Reset view": {
children: [],
function: () => {},
},
"View schema": {
children: [],
function: () => {},
},
2023-09-19 20:49:46 +08:00
Grid: {
children: [],
function: () =>
setSettings((prev) => ({ ...prev, showGrid: !prev.showGrid })),
},
2023-09-19 20:48:26 +08:00
Theme: {
children: [{ Light: () => {} }, { Dark: () => {} }],
function: () => {},
},
"Zoom in": {
children: [],
2023-09-19 20:49:36 +08:00
function: () =>
setSettings((prev) => ({ ...prev, zoom: prev.zoom * 1.2 })),
2023-09-19 20:48:26 +08:00
},
"Zoom out": {
children: [],
2023-09-19 20:49:36 +08:00
function: () =>
setSettings((prev) => ({ ...prev, zoom: prev.zoom / 1.2 })),
2023-09-19 20:48:26 +08:00
},
Fullscreen: {
children: [],
2023-09-19 20:48:35 +08:00
function: enterFullscreen,
2023-09-19 20:48:26 +08:00
},
},
Logs: {
"Open logs": {
children: [],
function: () => {},
},
"Commit changes": {
children: [],
function: () => {},
},
"Revert changes": {
children: [],
function: () => {},
},
"View commits": {
children: [],
function: () => {},
},
},
Help: {
Shortcuts: {
children: [],
function: () => {},
},
"Ask us on discord": {
children: [],
function: () => {},
},
"Tweet us": {
children: [],
function: () => {},
},
"Found a bug": {
children: [],
function: () => {},
},
},
};
2023-09-19 20:46:48 +08:00
return (
2023-09-19 20:48:38 +08:00
<div>
2023-09-19 20:48:46 +08:00
{layout.header && header()}
2023-09-19 20:49:18 +08:00
<div className="py-1 px-5 flex justify-between items-center rounded-xl bg-slate-100 my-1 sm:mx-1 md:mx-6 text-slate-700 select-none overflow-x-hidden">
2023-09-19 20:48:20 +08:00
<div className="flex justify-start items-center">
2023-09-19 20:48:34 +08:00
{layoutDropdown()}
2023-09-19 20:48:22 +08:00
<Divider layout="vertical" margin="8px" />
<Dropdown
style={{ width: "180px" }}
position="bottomLeft"
render={
<Dropdown.Menu>
<Dropdown.Item>Fit window</Dropdown.Item>
<Dropdown.Divider />
2023-09-19 20:49:36 +08:00
{[0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0].map((e, i) => (
<Dropdown.Item
key={i}
onClick={() => {
setSettings((prev) => ({ ...prev, zoom: e }));
}}
>
{Math.floor(e * 100)}%
</Dropdown.Item>
2023-09-19 20:48:22 +08:00
))}
<Dropdown.Divider />
<Dropdown.Item>
2023-09-19 20:49:36 +08:00
<InputNumber
field="zoom"
label="Custom zoom"
placeholder="Zoom"
suffix={<div className="p-1">%</div>}
onChange={(v) =>
setSettings((prev) => ({
...prev,
zoom: parseFloat(v) * 0.01,
}))
}
/>
2023-09-19 20:48:22 +08:00
</Dropdown.Item>
</Dropdown.Menu>
}
trigger="click"
>
2023-09-19 20:49:18 +08:00
<div className="py-1 px-2 hover:bg-slate-200 rounded flex items-center justify-center">
2023-09-19 20:49:36 +08:00
<div className="w-[40px]">{Math.floor(settings.zoom * 100)}%</div>
2023-09-19 20:49:18 +08:00
<div>
<IconCaretdown />
</div>
2023-09-19 20:48:22 +08:00
</div>
</Dropdown>
2023-09-19 20:48:29 +08:00
<button
2023-09-19 20:49:18 +08:00
className="py-1 px-2 hover:bg-slate-200 rounded text-lg"
2023-09-19 20:48:29 +08:00
title="Zoom in"
2023-09-19 20:49:36 +08:00
onClick={() =>
setSettings((prev) => ({ ...prev, zoom: prev.zoom * 1.2 }))
}
2023-09-19 20:48:29 +08:00
>
2023-09-19 20:47:16 +08:00
<i className="fa-solid fa-magnifying-glass-plus"></i>
</button>
2023-09-19 20:48:29 +08:00
<button
2023-09-19 20:49:18 +08:00
className="py-1 px-2 hover:bg-slate-200 rounded text-lg"
2023-09-19 20:48:29 +08:00
title="Zoom out"
2023-09-19 20:49:36 +08:00
onClick={() =>
setSettings((prev) => ({ ...prev, zoom: prev.zoom / 1.2 }))
}
2023-09-19 20:48:29 +08:00
>
2023-09-19 20:47:16 +08:00
<i className="fa-solid fa-magnifying-glass-minus"></i>
</button>
2023-09-19 20:48:22 +08:00
<Divider layout="vertical" margin="8px" />
2023-09-19 20:49:18 +08:00
<button
2023-09-19 20:49:20 +08:00
className="py-1 px-2 hover:bg-slate-200 rounded flex items-center"
2023-09-19 20:49:18 +08:00
title="Undo"
2023-09-19 20:49:41 +08:00
onClick={undo}
2023-09-19 20:49:18 +08:00
>
<IconUndo
size="large"
style={{ color: undoStack.length === 0 ? "#9598a6" : "" }}
/>
2023-09-19 20:47:16 +08:00
</button>
2023-09-19 20:49:18 +08:00
<button
2023-09-19 20:49:20 +08:00
className="py-1 px-2 hover:bg-slate-200 rounded flex items-center"
2023-09-19 20:49:18 +08:00
title="Redo"
2023-09-19 20:49:41 +08:00
onClick={redo}
2023-09-19 20:49:18 +08:00
>
<IconRedo
size="large"
style={{ color: redoStack.length === 0 ? "#9598a6" : "" }}
/>
2023-09-19 20:47:16 +08:00
</button>
2023-09-19 20:48:22 +08:00
<Divider layout="vertical" margin="8px" />
2023-09-19 20:49:20 +08:00
<button
className="flex items-center py-1 px-2 hover:bg-slate-200 rounded"
2023-09-19 20:49:18 +08:00
title="Add new table"
2023-09-19 20:49:57 +08:00
onClick={() => addTable()}
2023-09-19 20:48:22 +08:00
>
<IconAddTable />
2023-09-19 20:49:20 +08:00
</button>
<button
className="py-1 px-2 hover:bg-slate-200 rounded flex items-center"
2023-09-19 20:49:18 +08:00
title="Add subject area"
2023-09-19 20:49:57 +08:00
onClick={() => addArea()}
2023-09-19 20:48:29 +08:00
>
<IconAddArea />
2023-09-19 20:49:20 +08:00
</button>
<button
className="py-1 px-2 hover:bg-slate-200 rounded flex items-center"
2023-09-19 20:49:18 +08:00
title="Add new note"
2023-09-19 20:49:57 +08:00
onClick={() => addNote()}
2023-09-19 20:49:18 +08:00
>
<IconAddNote />
2023-09-19 20:49:20 +08:00
</button>
2023-09-19 20:48:22 +08:00
<Divider layout="vertical" margin="8px" />
2023-09-19 20:49:18 +08:00
<button
2023-09-19 20:49:20 +08:00
className="py-1 px-2 hover:bg-slate-200 rounded flex items-center"
2023-09-19 20:49:18 +08:00
title="Save"
>
<IconSaveStroked size="extra-large" />
2023-09-19 20:47:16 +08:00
</button>
2023-09-19 20:48:29 +08:00
<button
2023-09-19 20:49:18 +08:00
className="py-1 px-2 hover:bg-slate-200 rounded text-xl"
2023-09-19 20:48:29 +08:00
title="Commit"
>
2023-09-19 20:48:22 +08:00
<i className="fa-solid fa-code-branch"></i>
2023-09-19 20:47:16 +08:00
</button>
</div>
2023-09-19 20:48:34 +08:00
<button
2023-09-19 20:49:40 +08:00
onClick={() => invertLayout("header")}
2023-09-19 20:49:20 +08:00
className="flex items-center"
2023-09-19 20:48:34 +08:00
>
2023-09-19 20:48:46 +08:00
{layout.header ? <IconChevronUp /> : <IconChevronDown />}
2023-09-19 20:48:20 +08:00
</button>
</div>
2023-09-19 20:48:26 +08:00
<Modal
2023-09-19 20:49:28 +08:00
title={`${visible === MODAL.IMPORT ? "Import" : "Export"} diagram`}
2023-09-19 20:49:20 +08:00
visible={visible !== MODAL.NONE}
onOk={() => {
if (visible === MODAL.IMG) {
saveAs(
exportData.data,
`${exportData.filename}.${exportData.extension}`
);
} else if (visible === MODAL.CODE) {
const blob = new Blob([exportData.data], {
2023-09-19 20:49:30 +08:00
type: "application/json",
2023-09-19 20:49:20 +08:00
});
saveAs(blob, `${exportData.filename}.${exportData.extension}`);
2023-09-19 20:49:28 +08:00
} else if (visible === MODAL.IMPORT) {
2023-09-19 20:49:57 +08:00
if (error.type !== STATUS.ERROR) {
2023-09-19 20:49:50 +08:00
setSettings((prev) => ({ ...prev, pan: { x: 0, y: 0 } }));
2023-09-19 20:49:29 +08:00
overwriteDiagram();
setData(null);
setVisible(MODAL.NONE);
2023-09-19 20:49:52 +08:00
setUndoStack([]);
setRedoStack([]);
2023-09-19 20:49:29 +08:00
}
2023-09-19 20:49:20 +08:00
}
}}
2023-09-19 20:48:27 +08:00
afterClose={() => {
2023-09-19 20:49:20 +08:00
setExportData((prev) => ({
data: "",
extension: "",
filename: `diagram_${new Date().toISOString()}`,
}));
2023-09-19 20:49:29 +08:00
setError({
2023-09-19 20:49:57 +08:00
type: STATUS.NONE,
2023-09-19 20:49:29 +08:00
message: "",
});
setData(null);
2023-09-19 20:48:27 +08:00
}}
2023-09-19 20:49:20 +08:00
onCancel={() => setVisible(MODAL.NONE)}
2023-09-19 20:48:26 +08:00
centered
closeOnEsc={true}
2023-09-19 20:49:28 +08:00
okText={`${visible === MODAL.IMPORT ? "Import" : "Export"}`}
2023-09-19 20:49:50 +08:00
okButtonProps={{
disabled:
(visible === MODAL.IMPORT &&
2023-09-19 20:49:57 +08:00
(error.type === STATUS.ERROR || !data)) ||
2023-09-19 20:49:50 +08:00
((visible === MODAL.IMG || visible === MODAL.CODE) &&
!exportData.data),
}}
2023-09-19 20:48:26 +08:00
cancelText="Cancel"
2023-09-19 20:49:20 +08:00
width={520}
2023-09-19 20:48:26 +08:00
>
2023-09-19 20:49:28 +08:00
{visible === MODAL.IMPORT ? (
<div>
<Upload
action="#"
beforeUpload={({ file, fileList }) => {
const f = fileList[0].fileInstance;
if (!f) {
return;
}
const reader = new FileReader();
reader.onload = function (event) {
2023-09-19 20:49:31 +08:00
let jsonObject = null;
try {
jsonObject = JSON.parse(event.target.result);
} catch (error) {
setError({
2023-09-19 20:49:57 +08:00
type: STATUS.ERROR,
2023-09-19 20:49:31 +08:00
message: "The file contains an error.",
});
return;
}
2023-09-19 20:49:28 +08:00
if (f.type === "application/json") {
2023-09-19 20:49:31 +08:00
if (!jsonDiagramIsValid(jsonObject)) {
2023-09-19 20:49:29 +08:00
setError({
2023-09-19 20:49:57 +08:00
type: STATUS.ERROR,
2023-09-19 20:49:31 +08:00
message:
"The file is missing necessary properties for a diagram.",
2023-09-19 20:49:29 +08:00
});
2023-09-19 20:49:28 +08:00
return;
}
2023-09-19 20:49:31 +08:00
} else if (f.name.split(".").pop() === "ddb") {
if (!ddbDiagramIsValid(jsonObject)) {
2023-09-19 20:49:29 +08:00
setError({
2023-09-19 20:49:57 +08:00
type: STATUS.ERROR,
2023-09-19 20:49:29 +08:00
message:
"The file is missing necessary properties for a diagram.",
});
2023-09-19 20:49:28 +08:00
return;
}
2023-09-19 20:49:31 +08:00
}
setData(jsonObject);
if (diagramIsEmpty()) {
setError({
2023-09-19 20:49:57 +08:00
type: STATUS.OK,
2023-09-19 20:49:31 +08:00
message: "Everything looks good. You can now import.",
});
} else {
setError({
2023-09-19 20:49:57 +08:00
type: STATUS.WARNING,
2023-09-19 20:49:31 +08:00
message:
"The current diagram is not empty. Importing a new diagram will overwrite the current changes.",
});
2023-09-19 20:49:28 +08:00
}
};
reader.readAsText(f);
return {
autoRemove: false,
fileInstance: file.fileInstance,
status: "success",
shouldUpload: false,
};
}}
draggable={true}
dragMainText="Click to upload the file or drag and drop the file here"
dragSubText="Support json"
2023-09-19 20:49:31 +08:00
accept="application/json,.ddb"
2023-09-19 20:49:29 +08:00
onRemove={() =>
setError({
2023-09-19 20:49:57 +08:00
type: STATUS.NONE,
2023-09-19 20:49:29 +08:00
message: "",
})
}
onFileChange={() =>
setError({
2023-09-19 20:49:57 +08:00
type: STATUS.NONE,
2023-09-19 20:49:29 +08:00
message: "",
})
}
2023-09-19 20:49:28 +08:00
limit={1}
></Upload>
2023-09-19 20:49:57 +08:00
{error.type === STATUS.ERROR ? (
2023-09-19 20:49:28 +08:00
<Banner
type="danger"
fullMode={false}
2023-09-19 20:49:29 +08:00
description={
<div className="text-red-800">{error.message}</div>
}
/>
2023-09-19 20:49:57 +08:00
) : error.type === STATUS.OK ? (
2023-09-19 20:49:29 +08:00
<Banner
type="info"
fullMode={false}
description={<div>{error.message}</div>}
2023-09-19 20:49:20 +08:00
/>
2023-09-19 20:49:29 +08:00
) : (
2023-09-19 20:49:57 +08:00
error.type === STATUS.WARNING && (
2023-09-19 20:49:29 +08:00
<Banner
type="warning"
fullMode={false}
description={<div>{error.message}</div>}
/>
)
2023-09-19 20:49:28 +08:00
)}
</div>
) : exportData.data !== "" || exportData.data ? (
<>
{visible === MODAL.IMG ? (
<Image src={exportData.data} alt="Diagram" height={220} />
) : (
<div className="max-h-[400px] overflow-auto border border-gray-200">
<CodeMirror
value={exportData.data}
extensions={[json()]}
style={{
width: "100%",
height: "100%",
}}
/>
</div>
)}
<div className="text-sm font-semibold mt-2">Filename:</div>
<Input
value={exportData.filename}
placeholder="Filename"
suffix={<div className="p-2">{`.${exportData.extension}`}</div>}
onChange={(value) =>
setExportData((prev) => ({ ...prev, filename: value }))
}
field="filename"
/>
</>
2023-09-19 20:48:53 +08:00
) : (
<div className="text-center my-3">
2023-09-19 20:49:20 +08:00
<Spin tip="Loading..." size="large" />
2023-09-19 20:48:53 +08:00
</div>
)}
2023-09-19 20:48:26 +08:00
</Modal>
2023-09-19 20:48:38 +08:00
</div>
2023-09-19 20:46:48 +08:00
);
2023-09-19 20:48:34 +08:00
function header() {
2023-09-19 20:48:35 +08:00
return (
<nav className="flex justify-between pt-1 items-center whitespace-nowrap">
<div className="flex justify-start items-center text-slate-800">
<Link to="/">
<img
width={54}
src={icon}
alt="logo"
className="ms-8 min-w-[54px]"
/>
</Link>
<div className="ms-1 mt-1">
<div className="text-xl ms-3">Project1 / Untitled</div>
<div className="flex justify-between items-center">
<div className="flex justify-start text-md select-none me-2">
{Object.keys(menu).map((category) => (
<Dropdown
key={category}
position="bottomLeft"
style={{ width: "200px" }}
render={
<Dropdown.Menu>
{Object.keys(menu[category]).map((item, index) => {
if (menu[category][item].children.length > 0) {
return (
<Dropdown
style={{ width: "120px" }}
key={item}
position={"rightTop"}
render={
<Dropdown.Menu>
{menu[category][item].children.map(
(e, i) => (
<Dropdown.Item
key={i}
onClick={Object.values(e)[0]}
>
{Object.keys(e)[0]}
</Dropdown.Item>
)
)}
</Dropdown.Menu>
}
>
<Dropdown.Item
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
onClick={menu[category][item].function}
>
{item}
<IconChevronRight />
</Dropdown.Item>
</Dropdown>
);
}
return (
2023-09-19 20:48:34 +08:00
<Dropdown.Item
2023-09-19 20:48:35 +08:00
key={index}
2023-09-19 20:48:34 +08:00
onClick={menu[category][item].function}
>
{item}
</Dropdown.Item>
2023-09-19 20:48:35 +08:00
);
})}
</Dropdown.Menu>
}
>
<div className="px-3 py-1 hover:bg-gray-100 rounded">
{category}
</div>
</Dropdown>
))}
</div>
<Button size="small" type="tertiary">
Last saved {new Date().toISOString()}
</Button>
2023-09-19 20:48:34 +08:00
</div>
</div>
</div>
2023-09-19 20:48:35 +08:00
<div className="flex justify-around items-center text-md me-8">
<AvatarGroup maxCount={3} size="default">
<Avatar color="red" alt="Lisa LeBlanc">
LL
</Avatar>
<Avatar color="green" alt="Caroline Xiao">
CX
</Avatar>
<Avatar color="amber" alt="Rafal Matin">
RM
</Avatar>
<Avatar alt="Zank Lance">ZL</Avatar>
<Avatar alt="Youself Zhang">YZ</Avatar>
</AvatarGroup>
<Button
type="primary"
style={{
fontSize: "16px",
marginLeft: "12px",
marginRight: "12px",
}}
size="large"
icon={<IconShareStroked />}
>
Share
</Button>
<Avatar size="default" alt="Buni Zhang">
BZ
2023-09-19 20:48:34 +08:00
</Avatar>
2023-09-19 20:48:35 +08:00
</div>
</nav>
);
2023-09-19 20:48:34 +08:00
}
function layoutDropdown() {
return (
<Dropdown
position="bottomLeft"
style={{ width: "180px" }}
render={
<Dropdown.Menu>
<Dropdown.Item
icon={
2023-09-19 20:48:46 +08:00
layout.header ? (
2023-09-19 20:48:34 +08:00
<IconCheckboxTick />
) : (
<div className="px-2"></div>
)
}
2023-09-19 20:49:40 +08:00
onClick={() => invertLayout("header")}
2023-09-19 20:48:34 +08:00
>
Header
</Dropdown.Item>
2023-09-19 20:49:40 +08:00
<Dropdown.Item
icon={
layout.sidebar ? (
<IconCheckboxTick />
) : (
<div className="px-2"></div>
)
2023-09-19 20:48:34 +08:00
}
2023-09-19 20:49:40 +08:00
onClick={() => invertLayout("sidebar")}
2023-09-19 20:48:34 +08:00
>
2023-09-19 20:49:40 +08:00
Sidebar
</Dropdown.Item>
2023-09-19 20:48:34 +08:00
<Dropdown.Item
icon={
2023-09-19 20:49:40 +08:00
layout.issues ? (
2023-09-19 20:48:34 +08:00
<IconCheckboxTick />
) : (
<div className="px-2"></div>
)
}
2023-09-19 20:49:40 +08:00
onClick={() => invertLayout("issues")}
>
Issues
</Dropdown.Item>
<Dropdown.Item
icon={
layout.services ? (
<IconCheckboxTick />
) : (
<div className="px-2"></div>
)
2023-09-19 20:48:34 +08:00
}
2023-09-19 20:49:40 +08:00
onClick={() => invertLayout("services")}
2023-09-19 20:48:34 +08:00
>
Services
</Dropdown.Item>
<Dropdown.Divider />
2023-09-19 20:48:35 +08:00
<Dropdown.Item
icon={
2023-09-19 20:48:46 +08:00
layout.fullscreen ? (
2023-09-19 20:48:35 +08:00
<IconCheckboxTick />
) : (
<div className="px-2"></div>
)
}
onClick={() => {
2023-09-19 20:48:46 +08:00
if (layout.fullscreen) {
2023-09-19 20:48:35 +08:00
exitFullscreen();
} else {
enterFullscreen();
}
2023-09-19 20:49:40 +08:00
invertLayout("fullscreen");
2023-09-19 20:48:35 +08:00
}}
>
2023-09-19 20:48:34 +08:00
Fullscreen
</Dropdown.Item>
</Dropdown.Menu>
}
trigger="click"
>
2023-09-19 20:49:18 +08:00
<div className="py-1 px-2 hover:bg-slate-200 rounded flex items-center justify-center">
<div>
<i className="fa-solid fa-table-list text-xl me-1"></i>
</div>
<div>
<IconCaretdown />
</div>
2023-09-19 20:48:34 +08:00
</div>
</Dropdown>
);
}
2023-09-19 20:46:48 +08:00
}