2024-03-15 22:00:23 +08:00
|
|
|
import { useState } from "react";
|
2023-09-19 20:47:16 +08:00
|
|
|
import {
|
2023-09-19 20:48:20 +08:00
|
|
|
IconCaretdown,
|
|
|
|
IconChevronRight,
|
|
|
|
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:51:08 +08:00
|
|
|
IconRowsStroked,
|
2023-10-22 00:45:13 +08:00
|
|
|
IconEdit,
|
2023-09-19 20:48:20 +08:00
|
|
|
} from "@douyinfe/semi-icons";
|
2024-03-15 02:30:22 +08:00
|
|
|
import { Link, useNavigate } from "react-router-dom";
|
2023-09-19 20:48:20 +08:00
|
|
|
import icon from "../assets/icon_dark_64.png";
|
|
|
|
import {
|
|
|
|
Button,
|
|
|
|
Divider,
|
|
|
|
Dropdown,
|
2023-09-19 20:49:36 +08:00
|
|
|
InputNumber,
|
2023-09-19 20:51:18 +08:00
|
|
|
Tooltip,
|
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,
|
2024-01-14 08:05:07 +08:00
|
|
|
SideSheet,
|
|
|
|
List,
|
2024-01-21 15:05:43 +08:00
|
|
|
Select,
|
|
|
|
Checkbox,
|
2023-09-19 20:48:20 +08:00
|
|
|
} from "@douyinfe/semi-ui";
|
2024-01-14 08:05:07 +08:00
|
|
|
import timeLine from "../assets/process.png";
|
|
|
|
import timeLineDark from "../assets/process_dark.png";
|
|
|
|
import todo from "../assets/calendar.png";
|
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:51:26 +08:00
|
|
|
jsonToMySQL,
|
|
|
|
jsonToPostgreSQL,
|
2024-02-05 19:40:59 +08:00
|
|
|
jsonToSQLite,
|
2024-02-06 06:42:11 +08:00
|
|
|
jsonToMariaDB,
|
2024-02-13 03:05:21 +08:00
|
|
|
jsonToSQLServer,
|
2024-03-11 08:45:44 +08:00
|
|
|
} from "../utils/toSQL";
|
2023-10-19 16:36:46 +08:00
|
|
|
import { IconAddTable, IconAddArea, IconAddNote } from "./CustomIcons";
|
2024-03-15 22:37:22 +08:00
|
|
|
import { ObjectType, Action, Tab, State, Cardinality } from "../data/constants";
|
2023-09-19 20:49:24 +08:00
|
|
|
import jsPDF from "jspdf";
|
2023-09-19 20:50:22 +08:00
|
|
|
import { useHotkeys } from "react-hotkeys-hook";
|
2023-09-19 20:50:32 +08:00
|
|
|
import { Validator } from "jsonschema";
|
2023-10-19 16:36:46 +08:00
|
|
|
import { areaSchema, noteSchema, tableSchema } from "../data/schemas";
|
2024-02-13 03:05:21 +08:00
|
|
|
import Editor from "@monaco-editor/react";
|
2023-10-26 21:34:50 +08:00
|
|
|
import { db } from "../data/db";
|
2023-10-27 01:26:13 +08:00
|
|
|
import { useLiveQuery } from "dexie-react-hooks";
|
2024-01-18 18:45:58 +08:00
|
|
|
import { Parser } from "node-sql-parser";
|
2024-01-14 08:05:07 +08:00
|
|
|
import Todo from "./Todo";
|
2024-02-16 22:27:09 +08:00
|
|
|
import { Thumbnail } from "./Thumbnail";
|
2024-03-10 01:42:09 +08:00
|
|
|
import useLayout from "../hooks/useLayout";
|
2024-03-10 04:39:46 +08:00
|
|
|
import useSettings from "../hooks/useSettings";
|
2024-03-11 01:24:19 +08:00
|
|
|
import useTransform from "../hooks/useTransform";
|
2024-03-11 05:55:23 +08:00
|
|
|
import useTables from "../hooks/useTables";
|
|
|
|
import useUndoRedo from "../hooks/useUndoRedo";
|
|
|
|
import useSelect from "../hooks/useSelect";
|
2024-03-11 08:45:44 +08:00
|
|
|
import { enterFullscreen, exitFullscreen } from "../utils/fullscreen";
|
|
|
|
import { ddbDiagramIsValid, jsonDiagramIsValid } from "../utils/validateSchema";
|
|
|
|
import { dataURItoBlob } from "../utils/utils";
|
2024-03-12 05:59:04 +08:00
|
|
|
import useAreas from "../hooks/useAreas";
|
2024-03-13 05:36:49 +08:00
|
|
|
import useNotes from "../hooks/useNotes";
|
2024-03-13 07:27:42 +08:00
|
|
|
import useTypes from "../hooks/useTypes";
|
2024-03-15 22:00:23 +08:00
|
|
|
import useSaveState from "../hooks/useSaveState";
|
2023-09-19 20:46:48 +08:00
|
|
|
|
2023-11-23 03:32:17 +08:00
|
|
|
export default function ControlPanel({
|
|
|
|
diagramId,
|
|
|
|
setDiagramId,
|
|
|
|
title,
|
|
|
|
setTitle,
|
2023-11-25 00:28:39 +08:00
|
|
|
lastSaved,
|
2023-11-23 03:32:17 +08:00
|
|
|
}) {
|
2024-02-16 22:27:09 +08:00
|
|
|
const defaultTemplates = useLiveQuery(() => db.templates.toArray());
|
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-10-22 00:45:13 +08:00
|
|
|
RENAME: 4,
|
2023-10-27 01:26:13 +08:00
|
|
|
OPEN: 5,
|
2023-10-28 07:11:31 +08:00
|
|
|
SAVEAS: 6,
|
2023-11-02 19:31:26 +08:00
|
|
|
NEW: 7,
|
2024-01-18 18:45:58 +08:00
|
|
|
IMPORT_SRC: 8,
|
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,
|
|
|
|
};
|
2024-01-14 08:05:07 +08:00
|
|
|
const SIDESHEET = {
|
|
|
|
NONE: 0,
|
|
|
|
TODO: 1,
|
|
|
|
TIMELINE: 2,
|
|
|
|
};
|
2023-10-27 01:26:13 +08:00
|
|
|
const diagrams = useLiveQuery(() => db.diagrams.toArray());
|
2023-09-19 20:49:20 +08:00
|
|
|
const [visible, setVisible] = useState(MODAL.NONE);
|
2024-01-14 08:05:07 +08:00
|
|
|
const [sidesheet, setSidesheet] = useState(SIDESHEET.NONE);
|
2023-10-22 00:45:13 +08:00
|
|
|
const [prevTitle, setPrevTitle] = useState(title);
|
2023-10-28 07:11:31 +08:00
|
|
|
const [saveAsTitle, setSaveAsTitle] = useState(title);
|
|
|
|
const [selectedDiagramId, setSelectedDiagramId] = useState(0);
|
2023-11-02 19:31:26 +08:00
|
|
|
const [selectedTemplateId, setSelectedTemplateId] = useState(-1);
|
2023-10-22 00:45:13 +08:00
|
|
|
const [showEditName, setShowEditName] = useState(false);
|
2023-09-19 20:49:20 +08:00
|
|
|
const [exportData, setExportData] = useState({
|
2023-09-19 20:50:39 +08:00
|
|
|
data: null,
|
2024-02-28 23:07:39 +08:00
|
|
|
filename: `${title}_${new Date().toISOString()}`,
|
2023-09-19 20:49:20 +08:00
|
|
|
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);
|
2024-03-15 22:00:23 +08:00
|
|
|
const { saveState, setSaveState } = useSaveState();
|
2024-03-10 02:35:04 +08:00
|
|
|
const { layout, setLayout } = useLayout();
|
2024-03-10 04:39:46 +08:00
|
|
|
const { settings, setSettings } = useSettings();
|
2023-09-19 20:49:57 +08:00
|
|
|
const {
|
|
|
|
relationships,
|
|
|
|
tables,
|
|
|
|
setTables,
|
|
|
|
addTable,
|
2023-09-19 20:50:15 +08:00
|
|
|
updateTable,
|
2023-09-19 20:49:57 +08:00
|
|
|
deleteTable,
|
2023-09-19 20:50:04 +08:00
|
|
|
updateField,
|
2023-09-19 20:49:57 +08:00
|
|
|
setRelationships,
|
|
|
|
addRelationship,
|
|
|
|
deleteRelationship,
|
2024-03-11 05:55:23 +08:00
|
|
|
} = useTables();
|
2024-03-13 07:27:42 +08:00
|
|
|
const { types, addType, deleteType, updateType, setTypes } = useTypes();
|
|
|
|
const { notes, setNotes, updateNote, addNote, deleteNote } = useNotes();
|
2024-03-12 05:59:04 +08:00
|
|
|
const { areas, setAreas, updateArea, addArea, deleteArea } = useAreas();
|
2024-03-11 05:55:23 +08:00
|
|
|
const { undoStack, redoStack, setUndoStack, setRedoStack } = useUndoRedo();
|
|
|
|
const { selectedElement, setSelectedElement } = useSelect();
|
2024-03-11 01:24:19 +08:00
|
|
|
const { transform, setTransform } = useTransform();
|
2024-03-15 02:30:22 +08:00
|
|
|
const navigate = useNavigate();
|
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);
|
2024-02-29 05:03:40 +08:00
|
|
|
if (data.title) {
|
|
|
|
setTitle(data.title);
|
|
|
|
}
|
2023-09-19 20:49:46 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
const undo = () => {
|
|
|
|
if (undoStack.length === 0) return;
|
2024-01-12 10:10:49 +08:00
|
|
|
const a = undoStack[undoStack.length - 1];
|
2024-02-13 03:05:21 +08:00
|
|
|
setUndoStack((prev) => prev.filter((e, i) => i !== prev.length - 1));
|
2023-09-19 20:49:46 +08:00
|
|
|
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:51:28 +08:00
|
|
|
} else if (a.element === ObjectType.TYPE) {
|
|
|
|
deleteType(types.length - 1, 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 },
|
|
|
|
]);
|
2023-09-19 20:50:15 +08:00
|
|
|
updateTable(a.id, { x: a.x, y: a.y }, true);
|
2023-09-19 20:49:46 +08:00
|
|
|
} else if (a.element === ObjectType.AREA) {
|
|
|
|
setRedoStack((prev) => [
|
|
|
|
...prev,
|
|
|
|
{ ...a, x: areas[a.id].x, y: areas[a.id].y },
|
|
|
|
]);
|
2023-09-19 20:50:15 +08:00
|
|
|
updateArea(a.id, { x: a.x, y: a.y });
|
2023-09-19 20:49:46 +08:00
|
|
|
} else if (a.element === ObjectType.NOTE) {
|
|
|
|
setRedoStack((prev) => [
|
|
|
|
...prev,
|
|
|
|
{ ...a, x: notes[a.id].x, y: notes[a.id].y },
|
|
|
|
]);
|
2023-09-19 20:50:15 +08:00
|
|
|
updateNote(a.id, { x: a.x, y: a.y });
|
2023-09-19 20:49:46 +08:00
|
|
|
}
|
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:51:28 +08:00
|
|
|
} else if (a.element === ObjectType.TYPE) {
|
|
|
|
addType(false, { id: a.id, ...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) {
|
2023-09-19 20:50:18 +08:00
|
|
|
updateArea(a.aid, a.undo);
|
2023-09-19 20:50:09 +08:00
|
|
|
} else if (a.element === ObjectType.NOTE) {
|
2023-09-19 20:50:15 +08:00
|
|
|
updateNote(a.nid, a.undo);
|
2023-09-19 20:50:04 +08:00
|
|
|
} else if (a.element === ObjectType.TABLE) {
|
|
|
|
if (a.component === "field") {
|
2023-09-19 20:50:15 +08:00
|
|
|
updateField(a.tid, a.fid, a.undo);
|
2023-09-19 20:50:04 +08:00
|
|
|
} else if (a.component === "field_delete") {
|
2023-12-27 10:45:23 +08:00
|
|
|
setRelationships((prev) => {
|
|
|
|
return prev.map((e) => {
|
2024-01-23 14:57:21 +08:00
|
|
|
if (e.startTableId === a.tid && e.startFieldId >= a.data.id) {
|
2023-12-27 10:45:23 +08:00
|
|
|
return {
|
|
|
|
...e,
|
|
|
|
startFieldId: e.startFieldId + 1,
|
|
|
|
startX: tables[a.tid].x + 15,
|
|
|
|
startY: tables[a.tid].y + (e.startFieldId + 1) * 36 + 50 + 19,
|
|
|
|
};
|
|
|
|
}
|
2024-01-23 14:57:21 +08:00
|
|
|
if (e.endTableId === a.tid && e.endFieldId >= a.data.id) {
|
2023-12-27 10:45:23 +08:00
|
|
|
return {
|
|
|
|
...e,
|
|
|
|
endFieldId: e.endFieldId + 1,
|
|
|
|
endX: tables[a.tid].x + 15,
|
|
|
|
endY: tables[a.tid].y + (e.endFieldId + 1) * 36 + 50 + 19,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return e;
|
|
|
|
});
|
|
|
|
});
|
2023-09-19 20:50:04 +08:00
|
|
|
setTables((prev) =>
|
2023-12-16 11:39:13 +08:00
|
|
|
prev.map((t) => {
|
2023-09-19 20:50:04 +08:00
|
|
|
if (t.id === a.tid) {
|
|
|
|
const temp = t.fields.slice();
|
|
|
|
temp.splice(a.data.id, 0, a.data);
|
|
|
|
return { ...t, fields: temp.map((t, i) => ({ ...t, id: i })) };
|
|
|
|
}
|
|
|
|
return t;
|
|
|
|
})
|
|
|
|
);
|
|
|
|
} else if (a.component === "field_add") {
|
2023-09-19 20:50:16 +08:00
|
|
|
updateTable(a.tid, {
|
|
|
|
fields: tables[a.tid].fields
|
|
|
|
.filter((e) => e.id !== tables[a.tid].fields.length - 1)
|
|
|
|
.map((t, i) => ({ ...t, id: i })),
|
|
|
|
});
|
2023-09-19 20:50:04 +08:00
|
|
|
} else if (a.component === "index_add") {
|
2023-09-19 20:50:46 +08:00
|
|
|
updateTable(a.tid, {
|
|
|
|
indices: tables[a.tid].indices
|
|
|
|
.filter((e) => e.id !== tables[a.tid].indices.length - 1)
|
|
|
|
.map((t, i) => ({ ...t, id: i })),
|
|
|
|
});
|
2023-09-19 20:50:04 +08:00
|
|
|
} else if (a.component === "index") {
|
2023-09-19 20:50:15 +08:00
|
|
|
updateTable(a.tid, {
|
|
|
|
indices: tables[a.tid].indices.map((index) =>
|
|
|
|
index.id === a.iid
|
|
|
|
? {
|
2024-02-13 03:05:21 +08:00
|
|
|
...index,
|
|
|
|
...a.undo,
|
|
|
|
}
|
2023-09-19 20:50:15 +08:00
|
|
|
: index
|
|
|
|
),
|
|
|
|
});
|
2023-09-19 20:50:04 +08:00
|
|
|
} else if (a.component === "index_delete") {
|
|
|
|
setTables((prev) =>
|
2023-12-16 11:39:13 +08:00
|
|
|
prev.map((table) => {
|
2023-09-19 20:50:04 +08:00
|
|
|
if (table.id === a.tid) {
|
|
|
|
const temp = table.indices.slice();
|
|
|
|
temp.splice(a.data.id, 0, a.data);
|
|
|
|
return {
|
|
|
|
...table,
|
|
|
|
indices: temp.map((t, i) => ({ ...t, id: i })),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return table;
|
|
|
|
})
|
|
|
|
);
|
2023-09-19 20:50:12 +08:00
|
|
|
} else if (a.component === "self") {
|
2023-09-19 20:50:15 +08:00
|
|
|
updateTable(a.tid, a.undo);
|
2023-09-19 20:50:02 +08:00
|
|
|
}
|
2023-09-19 20:50:46 +08:00
|
|
|
} else if (a.element === ObjectType.RELATIONSHIP) {
|
|
|
|
setRelationships((prev) =>
|
|
|
|
prev.map((e, idx) => (idx === a.rid ? { ...e, ...a.undo } : e))
|
|
|
|
);
|
2023-09-19 20:51:28 +08:00
|
|
|
} else if (a.element === ObjectType.TYPE) {
|
|
|
|
if (a.component === "field_add") {
|
|
|
|
updateType(a.tid, {
|
|
|
|
fields: types[a.tid].fields.filter(
|
|
|
|
(e, i) => i !== types[a.tid].fields.length - 1
|
|
|
|
),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (a.component === "field") {
|
|
|
|
updateType(a.tid, {
|
|
|
|
fields: types[a.tid].fields.map((e, i) =>
|
|
|
|
i === a.fid ? { ...e, ...a.undo } : e
|
|
|
|
),
|
|
|
|
});
|
|
|
|
} else if (a.component === "field_delete") {
|
|
|
|
setTypes((prev) =>
|
|
|
|
prev.map((t, i) => {
|
|
|
|
if (i === a.tid) {
|
|
|
|
const temp = t.fields.slice();
|
|
|
|
temp.splice(a.fid, 0, a.data);
|
|
|
|
return { ...t, fields: temp };
|
|
|
|
}
|
|
|
|
return t;
|
|
|
|
})
|
|
|
|
);
|
|
|
|
} else if (a.component === "self") {
|
|
|
|
updateType(a.tid, a.undo);
|
|
|
|
}
|
2023-09-19 20:50:00 +08:00
|
|
|
}
|
|
|
|
setRedoStack((prev) => [...prev, a]);
|
|
|
|
} else if (a.action === Action.PAN) {
|
2024-03-10 02:35:04 +08:00
|
|
|
setTransform((prev) => ({
|
2023-09-19 20:50:00 +08:00
|
|
|
...prev,
|
2023-09-19 20:50:15 +08:00
|
|
|
pan: a.undo,
|
2023-09-19 20:50:00 +08:00
|
|
|
}));
|
|
|
|
setRedoStack((prev) => [...prev, a]);
|
2023-09-19 20:49:46 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const redo = () => {
|
|
|
|
if (redoStack.length === 0) return;
|
2024-01-12 10:10:49 +08:00
|
|
|
const a = redoStack[redoStack.length - 1];
|
2024-02-13 03:05:21 +08:00
|
|
|
setRedoStack((prev) => prev.filter((e, i) => i !== prev.length - 1));
|
2023-09-19 20:49:46 +08:00
|
|
|
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:51:28 +08:00
|
|
|
} else if (a.element === ObjectType.TYPE) {
|
|
|
|
addType(false);
|
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 },
|
|
|
|
]);
|
2023-09-19 20:50:15 +08:00
|
|
|
updateTable(a.id, { x: a.x, y: a.y }, true);
|
2023-09-19 20:49:46 +08:00
|
|
|
} else if (a.element === ObjectType.AREA) {
|
|
|
|
setUndoStack((prev) => [
|
|
|
|
...prev,
|
|
|
|
{ ...a, x: areas[a.id].x, y: areas[a.id].y },
|
|
|
|
]);
|
2023-09-19 20:50:15 +08:00
|
|
|
updateArea(a.id, { x: a.x, y: a.y });
|
2023-09-19 20:49:46 +08:00
|
|
|
} else if (a.element === ObjectType.NOTE) {
|
|
|
|
setUndoStack((prev) => [
|
|
|
|
...prev,
|
|
|
|
{ ...a, x: notes[a.id].x, y: notes[a.id].y },
|
|
|
|
]);
|
2023-09-19 20:50:15 +08:00
|
|
|
updateNote(a.id, { x: a.x, y: a.y });
|
2023-09-19 20:49:46 +08:00
|
|
|
}
|
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:51:28 +08:00
|
|
|
} else if (a.element === ObjectType.TYPE) {
|
|
|
|
deleteType(a.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) {
|
2023-09-19 20:50:18 +08:00
|
|
|
updateArea(a.aid, a.redo);
|
2023-09-19 20:50:09 +08:00
|
|
|
} else if (a.element === ObjectType.NOTE) {
|
2023-09-19 20:50:15 +08:00
|
|
|
updateNote(a.nid, a.redo);
|
2023-09-19 20:50:04 +08:00
|
|
|
} else if (a.element === ObjectType.TABLE) {
|
|
|
|
if (a.component === "field") {
|
2023-09-19 20:50:15 +08:00
|
|
|
updateField(a.tid, a.fid, a.redo);
|
2023-09-19 20:50:04 +08:00
|
|
|
} else if (a.component === "field_delete") {
|
2023-12-27 10:45:23 +08:00
|
|
|
setRelationships((prev) => {
|
|
|
|
return prev.map((e) => {
|
|
|
|
if (e.startTableId === a.tid && e.startFieldId > a.data.id) {
|
|
|
|
return {
|
|
|
|
...e,
|
|
|
|
startFieldId: e.startFieldId - 1,
|
|
|
|
startX: tables[a.tid].x + 15,
|
|
|
|
startY: tables[a.tid].y + (e.startFieldId - 1) * 36 + 50 + 19,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (e.endTableId === a.tid && e.endFieldId > a.data.id) {
|
|
|
|
return {
|
|
|
|
...e,
|
|
|
|
endFieldId: e.endFieldId - 1,
|
|
|
|
endX: tables[a.tid].x + 15,
|
|
|
|
endY: tables[a.tid].y + (e.endFieldId - 1) * 36 + 50 + 19,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return e;
|
|
|
|
});
|
|
|
|
});
|
2023-09-19 20:50:15 +08:00
|
|
|
updateTable(a.tid, {
|
|
|
|
fields: tables[a.tid].fields
|
|
|
|
.filter((field) => field.id !== a.data.id)
|
|
|
|
.map((e, i) => ({ ...e, id: i })),
|
|
|
|
});
|
2023-09-19 20:50:04 +08:00
|
|
|
} else if (a.component === "field_add") {
|
2023-09-19 20:50:15 +08:00
|
|
|
updateTable(a.tid, {
|
|
|
|
fields: [
|
|
|
|
...tables[a.tid].fields,
|
|
|
|
{
|
|
|
|
name: "",
|
|
|
|
type: "",
|
|
|
|
default: "",
|
|
|
|
check: "",
|
|
|
|
primary: false,
|
|
|
|
unique: false,
|
|
|
|
notNull: false,
|
|
|
|
increment: false,
|
|
|
|
comment: "",
|
|
|
|
id: tables[a.tid].fields.length,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
2023-09-19 20:50:04 +08:00
|
|
|
} else if (a.component === "index_add") {
|
|
|
|
setTables((prev) =>
|
|
|
|
prev.map((table) => {
|
|
|
|
if (table.id === a.tid) {
|
|
|
|
return {
|
|
|
|
...table,
|
|
|
|
indices: [
|
|
|
|
...table.indices,
|
|
|
|
{
|
|
|
|
id: table.indices.length,
|
|
|
|
name: `index_${table.indices.length}`,
|
|
|
|
fields: [],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return table;
|
|
|
|
})
|
|
|
|
);
|
|
|
|
} else if (a.component === "index") {
|
2023-09-19 20:50:15 +08:00
|
|
|
updateTable(a.tid, {
|
|
|
|
indices: tables[a.tid].indices.map((index) =>
|
|
|
|
index.id === a.iid
|
|
|
|
? {
|
2024-02-13 03:05:21 +08:00
|
|
|
...index,
|
|
|
|
...a.redo,
|
|
|
|
}
|
2023-09-19 20:50:15 +08:00
|
|
|
: index
|
|
|
|
),
|
|
|
|
});
|
2023-09-19 20:50:04 +08:00
|
|
|
} else if (a.component === "index_delete") {
|
2023-09-19 20:50:15 +08:00
|
|
|
updateTable(a.tid, {
|
|
|
|
indices: tables[a.tid].indices
|
|
|
|
.filter((e) => e.id !== a.data.id)
|
|
|
|
.map((t, i) => ({ ...t, id: i })),
|
|
|
|
});
|
2023-09-19 20:50:12 +08:00
|
|
|
} else if (a.component === "self") {
|
2023-09-19 20:50:52 +08:00
|
|
|
updateTable(a.tid, a.redo, false);
|
2023-09-19 20:50:02 +08:00
|
|
|
}
|
2023-09-19 20:50:46 +08:00
|
|
|
} else if (a.element === ObjectType.RELATIONSHIP) {
|
|
|
|
setRelationships((prev) =>
|
|
|
|
prev.map((e, idx) => (idx === a.rid ? { ...e, ...a.redo } : e))
|
|
|
|
);
|
2023-09-19 20:51:28 +08:00
|
|
|
} else if (a.element === ObjectType.TYPE) {
|
|
|
|
if (a.component === "field_add") {
|
|
|
|
updateType(a.tid, {
|
|
|
|
fields: [
|
|
|
|
...types[a.tid].fields,
|
|
|
|
{
|
|
|
|
name: "",
|
|
|
|
type: "",
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
} else if (a.component === "field") {
|
|
|
|
updateType(a.tid, {
|
|
|
|
fields: types[a.tid].fields.map((e, i) =>
|
|
|
|
i === a.fid ? { ...e, ...a.redo } : e
|
|
|
|
),
|
|
|
|
});
|
|
|
|
} else if (a.component === "field_delete") {
|
|
|
|
updateType(a.tid, {
|
|
|
|
fields: types[a.tid].fields.filter((field, i) => i !== a.fid),
|
|
|
|
});
|
|
|
|
} else if (a.component === "self") {
|
|
|
|
updateType(a.tid, a.redo);
|
|
|
|
}
|
2023-09-19 20:50:00 +08:00
|
|
|
}
|
|
|
|
setUndoStack((prev) => [...prev, a]);
|
|
|
|
} else if (a.action === Action.PAN) {
|
2024-03-10 02:35:04 +08:00
|
|
|
setTransform((prev) => ({
|
2023-09-19 20:50:00 +08:00
|
|
|
...prev,
|
2023-09-19 20:50:15 +08:00
|
|
|
pan: a.redo,
|
2023-09-19 20:50:00 +08:00
|
|
|
}));
|
|
|
|
setUndoStack((prev) => [...prev, a]);
|
2023-09-19 20:49:46 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-09-19 20:50:22 +08:00
|
|
|
const fileImport = () => setVisible(MODAL.IMPORT);
|
|
|
|
const viewGrid = () =>
|
|
|
|
setSettings((prev) => ({ ...prev, showGrid: !prev.showGrid }));
|
|
|
|
const zoomIn = () =>
|
2024-03-10 02:35:04 +08:00
|
|
|
setTransform((prev) => ({ ...prev, zoom: prev.zoom * 1.2 }));
|
2023-09-19 20:50:22 +08:00
|
|
|
const zoomOut = () =>
|
2024-03-10 02:35:04 +08:00
|
|
|
setTransform((prev) => ({ ...prev, zoom: prev.zoom / 1.2 }));
|
2023-09-19 20:50:22 +08:00
|
|
|
const viewStrictMode = () => {
|
|
|
|
setSettings((prev) => ({ ...prev, strictMode: !prev.strictMode }));
|
|
|
|
Toast.success(`Stict mode is ${settings.strictMode ? "on" : "off"}.`);
|
|
|
|
};
|
|
|
|
const viewFieldSummary = () => {
|
|
|
|
setSettings((prev) => ({
|
|
|
|
...prev,
|
|
|
|
showFieldSummary: !prev.showFieldSummary,
|
|
|
|
}));
|
|
|
|
Toast.success(
|
|
|
|
`Field summary is ${settings.showFieldSummary ? "off" : "on"}.`
|
|
|
|
);
|
|
|
|
};
|
|
|
|
const copyAsImage = () => {
|
|
|
|
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.");
|
|
|
|
})
|
2023-12-16 11:39:13 +08:00
|
|
|
.catch(() => {
|
2023-09-19 20:50:22 +08:00
|
|
|
Toast.error("Could not copy to clipboard.");
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
const resetView = () =>
|
2024-03-10 02:35:04 +08:00
|
|
|
setTransform((prev) => ({ ...prev, zoom: 1, pan: { x: 0, y: 0 } }));
|
2023-09-19 20:50:24 +08:00
|
|
|
const fitWindow = () => {
|
|
|
|
const diagram = document.getElementById("diagram").getBoundingClientRect();
|
|
|
|
const canvas = document.getElementById("canvas").getBoundingClientRect();
|
2024-02-20 17:32:19 +08:00
|
|
|
console.log(diagram);
|
|
|
|
console.log(canvas);
|
2023-09-19 20:50:24 +08:00
|
|
|
|
|
|
|
const scaleX = canvas.width / diagram.width;
|
|
|
|
const scaleY = canvas.height / diagram.height;
|
|
|
|
|
|
|
|
const scale = Math.min(scaleX, scaleY);
|
|
|
|
|
2024-02-20 17:32:19 +08:00
|
|
|
const translateX = canvas.left;
|
|
|
|
const translateY = canvas.top;
|
2023-09-19 20:50:24 +08:00
|
|
|
|
2024-03-10 02:35:04 +08:00
|
|
|
setTransform((prev) => ({
|
2023-09-19 20:50:24 +08:00
|
|
|
...prev,
|
2024-02-20 17:32:19 +08:00
|
|
|
zoom: scale - 0.01,
|
2023-09-19 20:50:24 +08:00
|
|
|
pan: { x: translateX, y: translateY },
|
|
|
|
}));
|
|
|
|
};
|
2023-09-19 20:50:28 +08:00
|
|
|
const edit = () => {
|
|
|
|
if (selectedElement.element === ObjectType.TABLE) {
|
|
|
|
if (!layout.sidebar) {
|
2024-03-14 02:39:16 +08:00
|
|
|
setSelectedElement((prev) => ({
|
|
|
|
...prev,
|
|
|
|
open: true,
|
|
|
|
}));
|
2023-09-19 20:50:28 +08:00
|
|
|
} else {
|
2024-03-14 02:39:16 +08:00
|
|
|
setSelectedElement((prev) => ({
|
|
|
|
...prev,
|
|
|
|
open: true,
|
2024-03-15 22:37:22 +08:00
|
|
|
currentTab: Tab.TABLES,
|
2024-03-14 02:39:16 +08:00
|
|
|
}));
|
2024-03-15 22:37:22 +08:00
|
|
|
if (selectedElement.currentTab !== Tab.TABLES) return;
|
2023-09-19 20:50:28 +08:00
|
|
|
document
|
|
|
|
.getElementById(`scroll_table_${selectedElement.id}`)
|
|
|
|
.scrollIntoView({ behavior: "smooth" });
|
|
|
|
}
|
2023-09-19 20:50:30 +08:00
|
|
|
} else if (selectedElement.element === ObjectType.AREA) {
|
2023-09-19 20:50:28 +08:00
|
|
|
if (layout.sidebar) {
|
2024-03-14 02:39:16 +08:00
|
|
|
setSelectedElement((prev) => ({
|
|
|
|
...prev,
|
2024-03-15 22:37:22 +08:00
|
|
|
currentTab: Tab.AREAS,
|
2024-03-14 02:39:16 +08:00
|
|
|
}));
|
2024-03-15 22:37:22 +08:00
|
|
|
if (selectedElement.currentTab !== Tab.AREAS) return;
|
2023-09-19 20:50:28 +08:00
|
|
|
document
|
|
|
|
.getElementById(`scroll_area_${selectedElement.id}`)
|
|
|
|
.scrollIntoView({ behavior: "smooth" });
|
|
|
|
} else {
|
2024-03-14 02:39:16 +08:00
|
|
|
setSelectedElement((prev) => ({
|
|
|
|
...prev,
|
|
|
|
open: true,
|
|
|
|
editFromToolbar: true,
|
|
|
|
}));
|
2023-09-19 20:50:28 +08:00
|
|
|
}
|
2023-09-19 20:50:30 +08:00
|
|
|
} else if (selectedElement.element === ObjectType.NOTE) {
|
2023-09-19 20:50:28 +08:00
|
|
|
if (layout.sidebar) {
|
2024-03-14 02:39:16 +08:00
|
|
|
setSelectedElement((prev) => ({
|
|
|
|
...prev,
|
2024-03-15 22:37:22 +08:00
|
|
|
currentTab: Tab.NOTES,
|
2024-03-14 02:39:16 +08:00
|
|
|
open: false,
|
|
|
|
}));
|
2024-03-15 22:37:22 +08:00
|
|
|
if (selectedElement.currentTab !== Tab.NOTES) return;
|
2023-09-19 20:50:28 +08:00
|
|
|
document
|
|
|
|
.getElementById(`scroll_note_${selectedElement.id}`)
|
|
|
|
.scrollIntoView({ behavior: "smooth" });
|
|
|
|
} else {
|
2024-03-14 02:39:16 +08:00
|
|
|
setSelectedElement((prev) => ({
|
|
|
|
...prev,
|
|
|
|
open: true,
|
|
|
|
editFromToolbar: true,
|
|
|
|
}));
|
2023-09-19 20:50:28 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2023-09-19 20:50:30 +08:00
|
|
|
const del = () => {
|
|
|
|
switch (selectedElement.element) {
|
|
|
|
case ObjectType.TABLE:
|
|
|
|
deleteTable(selectedElement.id, true);
|
|
|
|
break;
|
|
|
|
case ObjectType.NOTE:
|
|
|
|
deleteNote(selectedElement.id, true);
|
|
|
|
break;
|
|
|
|
case ObjectType.AREA:
|
|
|
|
deleteArea(selectedElement.id, true);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
const duplicate = () => {
|
|
|
|
switch (selectedElement.element) {
|
|
|
|
case ObjectType.TABLE:
|
|
|
|
addTable(true, {
|
|
|
|
...tables[selectedElement.id],
|
|
|
|
x: tables[selectedElement.id].x + 20,
|
|
|
|
y: tables[selectedElement.id].y + 20,
|
|
|
|
id: tables.length,
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case ObjectType.NOTE:
|
|
|
|
addNote(true, {
|
|
|
|
...notes[selectedElement.id],
|
|
|
|
x: notes[selectedElement.id].x + 20,
|
|
|
|
y: notes[selectedElement.id].y + 20,
|
|
|
|
id: notes.length,
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case ObjectType.AREA:
|
|
|
|
addArea(true, {
|
|
|
|
...areas[selectedElement.id],
|
|
|
|
x: areas[selectedElement.id].x + 20,
|
|
|
|
y: areas[selectedElement.id].y + 20,
|
|
|
|
id: areas.length,
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
};
|
2023-09-19 20:50:32 +08:00
|
|
|
const copy = () => {
|
|
|
|
switch (selectedElement.element) {
|
|
|
|
case ObjectType.TABLE:
|
|
|
|
navigator.clipboard
|
|
|
|
.writeText(JSON.stringify({ ...tables[selectedElement.id] }))
|
2023-12-16 11:39:13 +08:00
|
|
|
.catch(() => {
|
2023-09-19 20:50:32 +08:00
|
|
|
Toast.error("Could not copy");
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case ObjectType.NOTE:
|
|
|
|
navigator.clipboard
|
|
|
|
.writeText(JSON.stringify({ ...notes[selectedElement.id] }))
|
2023-12-16 11:39:13 +08:00
|
|
|
.catch(() => {
|
2023-09-19 20:50:32 +08:00
|
|
|
Toast.error("Could not copy");
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case ObjectType.AREA:
|
|
|
|
navigator.clipboard
|
|
|
|
.writeText(JSON.stringify({ ...areas[selectedElement.id] }))
|
2023-12-16 11:39:13 +08:00
|
|
|
.catch(() => {
|
2023-09-19 20:50:32 +08:00
|
|
|
Toast.error("Could not copy");
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
const paste = () => {
|
|
|
|
navigator.clipboard.readText().then((text) => {
|
|
|
|
let obj = null;
|
|
|
|
try {
|
|
|
|
obj = JSON.parse(text);
|
|
|
|
} catch (error) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const v = new Validator();
|
|
|
|
if (v.validate(obj, tableSchema).valid) {
|
|
|
|
addTable(true, {
|
|
|
|
...obj,
|
|
|
|
x: obj.x + 20,
|
|
|
|
y: obj.y + 20,
|
|
|
|
id: tables.length,
|
|
|
|
});
|
|
|
|
} else if (v.validate(obj, areaSchema).valid) {
|
|
|
|
addArea(true, {
|
|
|
|
...obj,
|
|
|
|
x: obj.x + 20,
|
|
|
|
y: obj.y + 20,
|
|
|
|
id: areas.length,
|
|
|
|
});
|
|
|
|
} else if (v.validate(obj, noteSchema)) {
|
|
|
|
addNote(true, {
|
|
|
|
...obj,
|
|
|
|
x: obj.x + 20,
|
|
|
|
y: obj.y + 20,
|
|
|
|
id: notes.length,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
const cut = () => {
|
|
|
|
copy();
|
|
|
|
del();
|
|
|
|
};
|
2024-03-15 22:00:23 +08:00
|
|
|
const save = () => setSaveState(State.SAVING);
|
2023-10-27 01:26:13 +08:00
|
|
|
const open = () => setVisible(MODAL.OPEN);
|
2023-10-28 07:11:31 +08:00
|
|
|
const saveDiagramAs = () => setVisible(MODAL.SAVEAS);
|
|
|
|
const loadDiagram = async (id) => {
|
|
|
|
await db.diagrams
|
2023-10-28 01:18:28 +08:00
|
|
|
.get(id)
|
|
|
|
.then((diagram) => {
|
|
|
|
if (diagram) {
|
2023-10-28 07:11:31 +08:00
|
|
|
setDiagramId(diagram.id);
|
2023-10-28 01:18:28 +08:00
|
|
|
setTitle(diagram.name);
|
|
|
|
setTables(diagram.tables);
|
2023-11-23 03:32:17 +08:00
|
|
|
setTypes(diagram.types);
|
2023-10-28 01:18:28 +08:00
|
|
|
setRelationships(diagram.references);
|
|
|
|
setAreas(diagram.areas);
|
|
|
|
setNotes(diagram.notes);
|
2024-03-10 02:35:04 +08:00
|
|
|
setTransform({
|
|
|
|
pan: diagram.pan,
|
|
|
|
zoom: diagram.zoom,
|
|
|
|
});
|
2023-10-28 07:11:31 +08:00
|
|
|
setUndoStack([]);
|
|
|
|
setRedoStack([]);
|
2023-11-23 03:32:17 +08:00
|
|
|
window.name = `d ${diagram.id}`;
|
2023-10-28 01:18:28 +08:00
|
|
|
} else {
|
|
|
|
Toast.error("Oops! Something went wrong.");
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
Toast.error("Oops! Couldn't load diagram.");
|
|
|
|
});
|
|
|
|
};
|
2023-11-02 19:31:26 +08:00
|
|
|
const createNewDiagram = (id) => {
|
2023-12-24 09:06:49 +08:00
|
|
|
const newWindow = window.open("/editor");
|
|
|
|
newWindow.name = "lt " + id;
|
2023-11-02 19:31:26 +08:00
|
|
|
};
|
2023-09-19 20:50:22 +08:00
|
|
|
|
2023-09-19 20:48:26 +08:00
|
|
|
const menu = {
|
|
|
|
File: {
|
|
|
|
New: {
|
2023-11-02 19:31:26 +08:00
|
|
|
function: () => setVisible(MODAL.NEW),
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
|
|
|
"New window": {
|
2023-11-29 05:08:45 +08:00
|
|
|
function: () => {
|
|
|
|
const newWindow = window.open("/editor", "_blank");
|
|
|
|
newWindow.name = window.name;
|
|
|
|
},
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
2023-10-22 14:01:43 +08:00
|
|
|
Open: {
|
2023-10-27 01:26:13 +08:00
|
|
|
function: open,
|
|
|
|
shortcut: "Ctrl+O",
|
2023-10-22 14:01:43 +08:00
|
|
|
},
|
2023-09-19 20:48:26 +08:00
|
|
|
Save: {
|
2023-10-26 21:34:50 +08:00
|
|
|
function: save,
|
|
|
|
shortcut: "Ctrl+S",
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
|
|
|
"Save as": {
|
2023-10-28 07:11:31 +08:00
|
|
|
function: saveDiagramAs,
|
|
|
|
shortcut: "Ctrl+Shift+S",
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
2023-12-19 10:36:10 +08:00
|
|
|
"Save as template": {
|
|
|
|
function: () => {
|
|
|
|
db.templates
|
|
|
|
.add({
|
|
|
|
title: title,
|
|
|
|
tables: tables,
|
|
|
|
relationships: relationships,
|
|
|
|
types: types,
|
|
|
|
notes: notes,
|
|
|
|
subjectAreas: areas,
|
|
|
|
custom: 1,
|
|
|
|
})
|
|
|
|
.then(() => {
|
|
|
|
Toast.success("Template saved!");
|
|
|
|
});
|
|
|
|
},
|
|
|
|
},
|
2023-09-19 20:48:26 +08:00
|
|
|
Rename: {
|
2023-10-22 00:45:13 +08:00
|
|
|
function: () => {
|
|
|
|
setVisible(MODAL.RENAME);
|
|
|
|
setPrevTitle(title);
|
|
|
|
},
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
2023-10-28 07:11:31 +08:00
|
|
|
"Delete diagram": {
|
|
|
|
function: async () => {
|
|
|
|
await db.diagrams
|
|
|
|
.delete(diagramId)
|
|
|
|
.then(() => {
|
|
|
|
setDiagramId(0);
|
|
|
|
setTitle("Untitled diagram");
|
|
|
|
setTables([]);
|
|
|
|
setRelationships([]);
|
|
|
|
setAreas([]);
|
|
|
|
setNotes([]);
|
2023-11-02 22:45:11 +08:00
|
|
|
setTypes([]);
|
2023-10-28 07:11:31 +08:00
|
|
|
setUndoStack([]);
|
|
|
|
setRedoStack([]);
|
|
|
|
})
|
2023-12-16 11:39:13 +08:00
|
|
|
.catch(() => Toast.error("Oops! Something went wrong."));
|
2023-10-28 07:11:31 +08:00
|
|
|
},
|
|
|
|
},
|
2024-01-18 18:45:58 +08:00
|
|
|
"Import diagram": {
|
2023-09-19 20:50:22 +08:00
|
|
|
function: fileImport,
|
|
|
|
shortcut: "Ctrl+I",
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
2024-01-18 18:45:58 +08:00
|
|
|
"Import from source": {
|
2024-01-21 15:05:43 +08:00
|
|
|
function: () => {
|
|
|
|
setData({ src: "", overwrite: true, dbms: "MySQL" });
|
2024-02-13 03:05:21 +08:00
|
|
|
setVisible(MODAL.IMPORT_SRC);
|
|
|
|
},
|
2024-01-18 18:45:58 +08:00
|
|
|
},
|
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:51:29 +08:00
|
|
|
types: types,
|
2024-02-29 05:03:40 +08:00
|
|
|
title: title,
|
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",
|
2024-02-29 05:03:40 +08:00
|
|
|
title: title,
|
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,
|
2023-09-19 20:51:36 +08:00
|
|
|
types: types,
|
2023-09-19 20:49:30 +08:00
|
|
|
},
|
|
|
|
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
|
|
|
],
|
2024-02-13 03:05:21 +08:00
|
|
|
function: () => {},
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
|
|
|
"Export source": {
|
|
|
|
children: [
|
2023-09-19 20:50:39 +08:00
|
|
|
{
|
|
|
|
MySQL: () => {
|
|
|
|
setVisible(MODAL.CODE);
|
2023-09-19 20:51:26 +08:00
|
|
|
const src = jsonToMySQL({
|
|
|
|
tables: tables,
|
|
|
|
references: relationships,
|
2023-09-19 20:51:36 +08:00
|
|
|
types: types,
|
2023-09-19 20:51:26 +08:00
|
|
|
});
|
|
|
|
setExportData((prev) => ({
|
|
|
|
...prev,
|
|
|
|
data: src,
|
|
|
|
extension: "sql",
|
|
|
|
}));
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
PostgreSQL: () => {
|
|
|
|
setVisible(MODAL.CODE);
|
|
|
|
const src = jsonToPostgreSQL({
|
2023-09-19 20:50:39 +08:00
|
|
|
tables: tables,
|
|
|
|
references: relationships,
|
2023-09-19 20:51:36 +08:00
|
|
|
types: types,
|
2023-09-19 20:50:39 +08:00
|
|
|
});
|
|
|
|
setExportData((prev) => ({
|
|
|
|
...prev,
|
|
|
|
data: src,
|
|
|
|
extension: "sql",
|
|
|
|
}));
|
|
|
|
},
|
|
|
|
},
|
2024-01-31 23:00:15 +08:00
|
|
|
{
|
|
|
|
SQLite: () => {
|
|
|
|
setVisible(MODAL.CODE);
|
|
|
|
const src = jsonToSQLite({
|
|
|
|
tables: tables,
|
|
|
|
references: relationships,
|
|
|
|
types: types,
|
|
|
|
});
|
|
|
|
setExportData((prev) => ({
|
|
|
|
...prev,
|
|
|
|
data: src,
|
|
|
|
extension: "sql",
|
|
|
|
}));
|
|
|
|
},
|
|
|
|
},
|
2024-02-05 19:40:59 +08:00
|
|
|
{
|
|
|
|
MariaDB: () => {
|
|
|
|
setVisible(MODAL.CODE);
|
|
|
|
const src = jsonToMariaDB({
|
|
|
|
tables: tables,
|
|
|
|
references: relationships,
|
|
|
|
types: types,
|
|
|
|
});
|
|
|
|
setExportData((prev) => ({
|
|
|
|
...prev,
|
|
|
|
data: src,
|
|
|
|
extension: "sql",
|
|
|
|
}));
|
|
|
|
},
|
|
|
|
},
|
2024-02-06 06:42:11 +08:00
|
|
|
{
|
2024-02-16 22:27:09 +08:00
|
|
|
MSSQL: () => {
|
2024-02-06 06:42:11 +08:00
|
|
|
setVisible(MODAL.CODE);
|
|
|
|
const src = jsonToSQLServer({
|
|
|
|
tables: tables,
|
|
|
|
references: relationships,
|
|
|
|
types: types,
|
|
|
|
});
|
|
|
|
setExportData((prev) => ({
|
|
|
|
...prev,
|
|
|
|
data: src,
|
|
|
|
extension: "sql",
|
|
|
|
}));
|
2024-02-13 03:05:21 +08:00
|
|
|
},
|
2024-02-06 06:42:11 +08:00
|
|
|
},
|
2023-09-19 20:48:26 +08:00
|
|
|
],
|
2024-02-13 03:05:21 +08:00
|
|
|
function: () => {},
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
2023-10-28 07:11:31 +08:00
|
|
|
Exit: {
|
2024-03-15 02:30:22 +08:00
|
|
|
function: () => {
|
|
|
|
save();
|
2024-03-15 22:00:23 +08:00
|
|
|
if (saveState === State.SAVED) navigate("/");
|
2024-03-15 02:30:22 +08:00
|
|
|
},
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
|
|
|
},
|
|
|
|
Edit: {
|
|
|
|
Undo: {
|
2023-09-19 20:49:46 +08:00
|
|
|
function: undo,
|
2023-09-19 20:50:22 +08:00
|
|
|
shortcut: "Ctrl+Z",
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
|
|
|
Redo: {
|
2023-09-19 20:49:46 +08:00
|
|
|
function: redo,
|
2023-09-19 20:50:22 +08:00
|
|
|
shortcut: "Ctrl+Y",
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
2023-09-19 20:49:39 +08:00
|
|
|
Clear: {
|
|
|
|
function: () => {
|
|
|
|
setTables([]);
|
|
|
|
setRelationships([]);
|
|
|
|
setAreas([]);
|
|
|
|
setNotes([]);
|
2023-09-19 20:50:22 +08:00
|
|
|
setUndoStack([]);
|
|
|
|
setRedoStack([]);
|
2023-09-19 20:49:39 +08:00
|
|
|
},
|
|
|
|
},
|
2023-09-19 20:49:46 +08:00
|
|
|
Edit: {
|
2023-09-19 20:50:28 +08:00
|
|
|
function: edit,
|
|
|
|
shortcut: "Ctrl+E",
|
2023-09-19 20:49:46 +08:00
|
|
|
},
|
2023-09-19 20:48:26 +08:00
|
|
|
Cut: {
|
2023-09-19 20:50:32 +08:00
|
|
|
function: cut,
|
2023-09-19 20:50:28 +08:00
|
|
|
shortcut: "Ctrl+X",
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
|
|
|
Copy: {
|
2023-09-19 20:50:32 +08:00
|
|
|
function: copy,
|
2023-09-19 20:50:28 +08:00
|
|
|
shortcut: "Ctrl+C",
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
2023-09-19 20:49:39 +08:00
|
|
|
Paste: {
|
2023-09-19 20:50:32 +08:00
|
|
|
function: paste,
|
2023-09-19 20:50:28 +08:00
|
|
|
shortcut: "Ctrl+V",
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
2023-09-19 20:49:46 +08:00
|
|
|
Duplicate: {
|
2023-09-19 20:50:30 +08:00
|
|
|
function: duplicate,
|
2023-09-19 20:50:28 +08:00
|
|
|
shortcut: "Ctrl+D",
|
2023-09-19 20:49:46 +08:00
|
|
|
},
|
|
|
|
Delete: {
|
2023-09-19 20:50:30 +08:00
|
|
|
function: del,
|
2023-09-19 20:50:28 +08:00
|
|
|
shortcut: "Del",
|
2023-09-19 20:49:46 +08:00
|
|
|
},
|
2023-09-19 20:49:39 +08:00
|
|
|
"Copy as image": {
|
2023-09-19 20:50:22 +08:00
|
|
|
function: copyAsImage,
|
|
|
|
shortcut: "Ctrl+Alt+C",
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
|
|
|
},
|
|
|
|
View: {
|
2023-09-19 20:49:46 +08:00
|
|
|
Header: {
|
|
|
|
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: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: {
|
|
|
|
function: () =>
|
|
|
|
setLayout((prev) => ({ ...prev, issues: !prev.issues })),
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
|
|
|
"Strict mode": {
|
2023-09-19 20:50:22 +08:00
|
|
|
function: viewStrictMode,
|
|
|
|
shortcut: "Ctrl+Shift+M",
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
2024-02-03 19:48:07 +08:00
|
|
|
"Presentation mode": {
|
|
|
|
function: () => {
|
2024-02-13 03:05:21 +08:00
|
|
|
setLayout((prev) => ({
|
2024-02-03 19:48:07 +08:00
|
|
|
...prev,
|
2024-02-05 19:40:59 +08:00
|
|
|
header: false,
|
2024-02-03 19:48:07 +08:00
|
|
|
sidebar: false,
|
|
|
|
toolbar: false,
|
|
|
|
}));
|
|
|
|
enterFullscreen();
|
2024-02-13 03:05:21 +08:00
|
|
|
},
|
2024-02-03 19:48:07 +08:00
|
|
|
},
|
2023-09-19 20:49:16 +08:00
|
|
|
"Field summary": {
|
2023-09-19 20:50:22 +08:00
|
|
|
function: viewFieldSummary,
|
|
|
|
shortcut: "Ctrl+Shift+F",
|
2023-09-19 20:49:16 +08:00
|
|
|
},
|
2023-09-19 20:48:26 +08:00
|
|
|
"Reset view": {
|
2023-09-19 20:50:22 +08:00
|
|
|
function: resetView,
|
|
|
|
shortcut: "Ctrl+R",
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
2023-12-30 04:38:14 +08:00
|
|
|
"Show grid": {
|
2023-09-19 20:50:22 +08:00
|
|
|
function: viewGrid,
|
|
|
|
shortcut: "Ctrl+Shift+G",
|
2023-09-19 20:49:46 +08:00
|
|
|
},
|
2023-12-30 04:38:14 +08:00
|
|
|
"Show cardinality": {
|
|
|
|
function: () =>
|
|
|
|
setSettings((prev) => ({
|
|
|
|
...prev,
|
|
|
|
showCardinality: !prev.showCardinality,
|
|
|
|
})),
|
|
|
|
},
|
2023-09-19 20:48:26 +08:00
|
|
|
Theme: {
|
2023-09-19 20:51:08 +08:00
|
|
|
children: [
|
|
|
|
{
|
|
|
|
Light: () => {
|
|
|
|
const body = document.body;
|
|
|
|
if (body.hasAttribute("theme-mode")) {
|
|
|
|
body.setAttribute("theme-mode", "light");
|
|
|
|
}
|
2023-09-19 20:51:43 +08:00
|
|
|
localStorage.setItem("theme", "light");
|
2023-09-19 20:51:08 +08:00
|
|
|
setSettings((prev) => ({ ...prev, mode: "light" }));
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Dark: () => {
|
|
|
|
const body = document.body;
|
|
|
|
if (body.hasAttribute("theme-mode")) {
|
|
|
|
body.setAttribute("theme-mode", "dark");
|
|
|
|
}
|
2023-09-19 20:51:43 +08:00
|
|
|
localStorage.setItem("theme", "dark");
|
2023-09-19 20:51:08 +08:00
|
|
|
setSettings((prev) => ({ ...prev, mode: "dark" }));
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
2024-02-13 03:05:21 +08:00
|
|
|
function: () => {},
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
|
|
|
"Zoom in": {
|
2023-09-19 20:50:22 +08:00
|
|
|
function: zoomIn,
|
|
|
|
shortcut: "Ctrl+Up/Wheel",
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
|
|
|
"Zoom out": {
|
2023-09-19 20:50:22 +08:00
|
|
|
function: zoomOut,
|
|
|
|
shortcut: "Ctrl+Down/Wheel",
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
|
|
|
Fullscreen: {
|
2023-09-19 20:48:35 +08:00
|
|
|
function: enterFullscreen,
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
|
|
|
},
|
2024-01-14 08:43:22 +08:00
|
|
|
Settings: {
|
2024-02-19 20:14:32 +08:00
|
|
|
"Show timeline": {
|
|
|
|
function: () => setSidesheet(SIDESHEET.TIMELINE),
|
|
|
|
},
|
2024-01-29 15:53:05 +08:00
|
|
|
Autosave: {
|
|
|
|
function: () =>
|
|
|
|
setSettings((prev) => {
|
2024-02-13 03:05:21 +08:00
|
|
|
Toast.success(`Autosave is ${settings.autosave ? "off" : "on"}`);
|
2024-01-29 15:53:05 +08:00
|
|
|
return { ...prev, autosave: !prev.autosave };
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
Panning: {
|
|
|
|
function: () =>
|
|
|
|
setSettings((prev) => {
|
|
|
|
Toast.success(`Panning is ${settings.panning ? "off" : "on"}`);
|
|
|
|
return { ...prev, panning: !prev.panning };
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
"Flush storage": {
|
|
|
|
function: async () => {
|
|
|
|
db.delete()
|
|
|
|
.then(() => {
|
|
|
|
Toast.success("Storage flushed");
|
|
|
|
window.location.reload(false);
|
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
Toast.error("Oops! Something went wrong.");
|
|
|
|
});
|
|
|
|
},
|
2024-01-14 08:43:22 +08:00
|
|
|
},
|
|
|
|
},
|
2023-09-19 20:48:26 +08:00
|
|
|
Help: {
|
|
|
|
Shortcuts: {
|
2023-09-19 20:51:44 +08:00
|
|
|
function: () => window.open("/shortcuts", "_blank"),
|
|
|
|
shortcut: "Ctrl+H",
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
|
|
|
"Ask us on discord": {
|
2024-02-20 04:40:24 +08:00
|
|
|
function: () => window.open("https://discord.gg/CUr9s9KH6X", "_blank"),
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
2023-09-19 20:50:39 +08:00
|
|
|
"Report a bug": {
|
2023-09-19 20:51:49 +08:00
|
|
|
function: () => window.open("/bug_report", "_blank"),
|
2023-09-19 20:50:39 +08:00
|
|
|
},
|
2023-10-13 04:17:23 +08:00
|
|
|
"Give feedback": {
|
|
|
|
function: () => window.open("/survey", "_blank"),
|
2023-09-19 20:48:26 +08:00
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2023-09-19 20:50:22 +08:00
|
|
|
useHotkeys("ctrl+i, meta+i", fileImport, { preventDefault: true });
|
|
|
|
useHotkeys("ctrl+z, meta+z", undo, { preventDefault: true });
|
|
|
|
useHotkeys("ctrl+y, meta+y", redo, { preventDefault: true });
|
2023-10-27 01:26:13 +08:00
|
|
|
useHotkeys("ctrl+s, meta+s", save, { preventDefault: true });
|
|
|
|
useHotkeys("ctrl+o, meta+o", open, { preventDefault: true });
|
2023-09-19 20:50:28 +08:00
|
|
|
useHotkeys("ctrl+e, meta+e", edit, { preventDefault: true });
|
2023-09-19 20:50:30 +08:00
|
|
|
useHotkeys("ctrl+d, meta+d", duplicate, { preventDefault: true });
|
2023-09-19 20:50:32 +08:00
|
|
|
useHotkeys("ctrl+c, meta+c", copy, { preventDefault: true });
|
|
|
|
useHotkeys("ctrl+v, meta+v", paste, { preventDefault: true });
|
|
|
|
useHotkeys("ctrl+x, meta+x", cut, { preventDefault: true });
|
2023-09-19 20:50:30 +08:00
|
|
|
useHotkeys("delete", del, { preventDefault: true });
|
2023-09-19 20:50:22 +08:00
|
|
|
useHotkeys("ctrl+shift+g, meta+shift+g", viewGrid, { preventDefault: true });
|
|
|
|
useHotkeys("ctrl+up, meta+up", zoomIn, { preventDefault: true });
|
|
|
|
useHotkeys("ctrl+down, meta+down", zoomOut, { preventDefault: true });
|
|
|
|
useHotkeys("ctrl+shift+m, meta+shift+m", viewStrictMode, {
|
|
|
|
preventDefault: true,
|
|
|
|
});
|
|
|
|
useHotkeys("ctrl+shift+f, meta+shift+f", viewFieldSummary, {
|
|
|
|
preventDefault: true,
|
|
|
|
});
|
2023-10-28 07:11:31 +08:00
|
|
|
useHotkeys("ctrl+shift+s, meta+shift+s", saveDiagramAs, {
|
|
|
|
preventDefault: true,
|
|
|
|
});
|
2023-09-19 20:50:22 +08:00
|
|
|
useHotkeys("ctrl+alt+c, meta+alt+c", copyAsImage, { preventDefault: true });
|
|
|
|
useHotkeys("ctrl+r, meta+r", resetView, { preventDefault: true });
|
2023-09-19 20:51:44 +08:00
|
|
|
useHotkeys("ctrl+h, meta+h", () => window.open("/shortcuts", "_blank"), {
|
|
|
|
preventDefault: true,
|
|
|
|
});
|
2023-09-19 20:50:24 +08:00
|
|
|
useHotkeys("ctrl+alt+w, meta+alt+w", fitWindow, { preventDefault: true });
|
2023-09-19 20:50:22 +08:00
|
|
|
|
2023-10-22 00:45:13 +08:00
|
|
|
const getModalTitle = () => {
|
|
|
|
switch (visible) {
|
|
|
|
case MODAL.IMPORT:
|
2024-01-18 18:45:58 +08:00
|
|
|
case MODAL.IMPORT_SRC:
|
2023-10-22 00:45:13 +08:00
|
|
|
return "Import diagram";
|
|
|
|
case MODAL.CODE:
|
2023-11-01 19:35:57 +08:00
|
|
|
return "Export source";
|
2023-10-22 00:45:13 +08:00
|
|
|
case MODAL.IMG:
|
|
|
|
return "Export image";
|
|
|
|
case MODAL.RENAME:
|
|
|
|
return "Rename diagram";
|
2023-10-27 01:26:13 +08:00
|
|
|
case MODAL.OPEN:
|
|
|
|
return "Open diagram";
|
2023-10-28 07:11:31 +08:00
|
|
|
case MODAL.SAVEAS:
|
|
|
|
return "Save as";
|
2023-11-02 19:31:26 +08:00
|
|
|
case MODAL.NEW:
|
2024-02-16 22:27:09 +08:00
|
|
|
return "Create new diagram";
|
2023-10-22 00:45:13 +08:00
|
|
|
default:
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const getOkText = () => {
|
|
|
|
switch (visible) {
|
|
|
|
case MODAL.IMPORT:
|
2024-01-18 18:45:58 +08:00
|
|
|
case MODAL.IMPORT_SRC:
|
2023-10-22 00:45:13 +08:00
|
|
|
return "Import";
|
|
|
|
case MODAL.CODE:
|
|
|
|
case MODAL.IMG:
|
|
|
|
return "Export";
|
|
|
|
case MODAL.RENAME:
|
|
|
|
return "Rename";
|
2023-10-27 01:26:13 +08:00
|
|
|
case MODAL.OPEN:
|
|
|
|
return "Open";
|
2023-10-28 07:11:31 +08:00
|
|
|
case MODAL.SAVEAS:
|
|
|
|
return "Save as";
|
2023-11-02 19:31:26 +08:00
|
|
|
case MODAL.NEW:
|
|
|
|
return "Create";
|
2023-10-22 00:45:13 +08:00
|
|
|
default:
|
2023-10-28 07:11:31 +08:00
|
|
|
return "Confirm";
|
2023-10-22 00:45:13 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-01-21 15:05:43 +08:00
|
|
|
const parseSQLAndLoadDiagram = () => {
|
|
|
|
const parser = new Parser();
|
|
|
|
let ast = null;
|
|
|
|
try {
|
2024-02-13 03:05:21 +08:00
|
|
|
console.log(data.dbms);
|
2024-01-21 15:05:43 +08:00
|
|
|
ast = parser.astify(data.src, { database: data.dbms });
|
|
|
|
} catch (err) {
|
2024-02-13 03:05:21 +08:00
|
|
|
Toast.error(
|
|
|
|
"Could not parse the sql file. Make sure there are no syntax errors."
|
|
|
|
);
|
2024-01-21 15:05:43 +08:00
|
|
|
console.log(err);
|
|
|
|
return;
|
|
|
|
}
|
2024-01-21 17:22:45 +08:00
|
|
|
|
2024-01-21 15:05:43 +08:00
|
|
|
const tables = [];
|
2024-01-22 22:35:01 +08:00
|
|
|
const relationships = [];
|
2024-01-23 16:15:28 +08:00
|
|
|
const inlineForeignKeys = [];
|
2024-01-21 15:05:43 +08:00
|
|
|
|
2024-02-13 03:05:21 +08:00
|
|
|
ast.forEach((e) => {
|
2024-01-21 17:22:45 +08:00
|
|
|
if (e.type === "create") {
|
|
|
|
if (e.keyword === "table") {
|
|
|
|
const table = {};
|
|
|
|
table.name = e.table[0].table;
|
|
|
|
table.comment = "";
|
|
|
|
table.color = "#175e7a";
|
|
|
|
table.fields = [];
|
|
|
|
table.indices = [];
|
|
|
|
table.x = 0;
|
|
|
|
table.y = 0;
|
|
|
|
e.create_definitions.forEach((d) => {
|
|
|
|
if (d.resource === "column") {
|
|
|
|
const field = {};
|
|
|
|
field.name = d.column.column;
|
|
|
|
field.type = d.definition.dataType;
|
|
|
|
field.comment = "";
|
|
|
|
field.unique = false;
|
|
|
|
if (d.unique) field.unique = true;
|
2024-01-23 14:57:21 +08:00
|
|
|
field.increment = false;
|
|
|
|
if (d.auto_increment) field.increment = true;
|
2024-01-21 17:22:45 +08:00
|
|
|
field.notNull = false;
|
|
|
|
if (d.nullable) field.notNull = true;
|
|
|
|
field.primary = false;
|
|
|
|
if (d.primary_key) field.primary = true;
|
|
|
|
field.default = "";
|
|
|
|
if (d.default_val) field.default = d.default_val.value.value;
|
|
|
|
if (d.definition["length"]) field.size = d.definition["length"];
|
|
|
|
field.check = "";
|
|
|
|
if (d.check) {
|
|
|
|
let check = "";
|
|
|
|
if (d.check.definition[0].left.column) {
|
|
|
|
let value = d.check.definition[0].right.value;
|
2024-02-13 03:05:21 +08:00
|
|
|
if (
|
|
|
|
d.check.definition[0].right.type ===
|
|
|
|
"double_quote_string" ||
|
|
|
|
d.check.definition[0].right.type === "single_quote_string"
|
|
|
|
)
|
|
|
|
value = "'" + value + "'";
|
|
|
|
check =
|
|
|
|
d.check.definition[0].left.column +
|
|
|
|
" " +
|
|
|
|
d.check.definition[0].operator +
|
|
|
|
" " +
|
|
|
|
value;
|
2024-01-21 17:22:45 +08:00
|
|
|
} else {
|
|
|
|
let value = d.check.definition[0].right.value;
|
2024-02-13 03:05:21 +08:00
|
|
|
if (
|
|
|
|
d.check.definition[0].left.type === "double_quote_string" ||
|
|
|
|
d.check.definition[0].left.type === "single_quote_string"
|
|
|
|
)
|
|
|
|
value = "'" + value + "'";
|
|
|
|
check =
|
|
|
|
value +
|
|
|
|
" " +
|
|
|
|
d.check.definition[0].operator +
|
|
|
|
" " +
|
|
|
|
d.check.definition[0].right.column;
|
2024-01-21 17:22:45 +08:00
|
|
|
}
|
|
|
|
field.check = check;
|
|
|
|
}
|
2024-01-21 15:05:43 +08:00
|
|
|
|
2024-01-21 17:22:45 +08:00
|
|
|
table.fields.push(field);
|
|
|
|
} else if (d.resource === "constraint") {
|
|
|
|
if (d.constraint_type === "primary key") {
|
2024-02-13 03:05:21 +08:00
|
|
|
d.definition.forEach((c) => {
|
2024-01-21 17:22:45 +08:00
|
|
|
table.fields.forEach((f) => {
|
|
|
|
if (f.name === c.column && !f.primary) {
|
|
|
|
f.primary = true;
|
|
|
|
}
|
2024-02-13 03:05:21 +08:00
|
|
|
});
|
2024-01-21 17:22:45 +08:00
|
|
|
});
|
2024-01-23 16:15:28 +08:00
|
|
|
} else if (d.constraint_type === "FOREIGN KEY") {
|
2024-02-13 03:05:21 +08:00
|
|
|
inlineForeignKeys.push({ ...d, startTable: e.table[0].table });
|
2024-01-21 15:05:43 +08:00
|
|
|
}
|
|
|
|
}
|
2024-01-21 17:22:45 +08:00
|
|
|
});
|
|
|
|
tables.push(table);
|
2024-01-22 22:35:01 +08:00
|
|
|
tables.forEach((e, i) => {
|
|
|
|
e.id = i;
|
|
|
|
e.fields.forEach((f, j) => {
|
|
|
|
f.id = j;
|
2024-02-13 03:05:21 +08:00
|
|
|
});
|
|
|
|
});
|
|
|
|
} else if (e.keyword === "index") {
|
2024-01-21 17:22:45 +08:00
|
|
|
const index = {};
|
|
|
|
index.name = e.index;
|
|
|
|
index.unique = false;
|
|
|
|
if (e.index_type === "unique") index.unique = true;
|
|
|
|
index.fields = [];
|
2024-02-13 03:05:21 +08:00
|
|
|
e.index_columns.forEach((f) => index.fields.push(f.column));
|
2024-01-21 15:05:43 +08:00
|
|
|
|
2024-01-21 17:22:45 +08:00
|
|
|
let found = -1;
|
|
|
|
tables.forEach((t, i) => {
|
|
|
|
if (found !== -1) return;
|
|
|
|
if (t.name === e.table.table) {
|
|
|
|
t.indices.push(index);
|
|
|
|
found = i;
|
2024-01-21 15:05:43 +08:00
|
|
|
}
|
2024-01-21 17:22:45 +08:00
|
|
|
});
|
2024-01-21 15:05:43 +08:00
|
|
|
|
2024-02-13 03:05:21 +08:00
|
|
|
if (found !== -1) tables[found].indices.forEach((i, j) => (i.id = j));
|
2024-01-21 17:22:45 +08:00
|
|
|
}
|
2024-01-22 22:35:01 +08:00
|
|
|
} else if (e.type === "alter") {
|
2024-02-13 03:05:21 +08:00
|
|
|
if (
|
|
|
|
e.expr[0].action === "add" &&
|
|
|
|
e.expr[0].create_definitions.constraint_type === "FOREIGN KEY"
|
|
|
|
) {
|
2024-01-22 22:35:01 +08:00
|
|
|
const relationship = {};
|
|
|
|
const startTable = e.table[0].table;
|
|
|
|
const startField = e.expr[0].create_definitions.definition[0].column;
|
2024-02-13 03:05:21 +08:00
|
|
|
const endTable =
|
|
|
|
e.expr[0].create_definitions.reference_definition.table[0].table;
|
|
|
|
const endField =
|
|
|
|
e.expr[0].create_definitions.reference_definition.definition[0]
|
|
|
|
.column;
|
2024-01-22 22:35:01 +08:00
|
|
|
let updateConstraint = "No action";
|
|
|
|
let deleteConstraint = "No action";
|
2024-02-13 03:05:21 +08:00
|
|
|
e.expr[0].create_definitions.reference_definition.on_action.forEach(
|
|
|
|
(c) => {
|
|
|
|
if (c.type === "on update") {
|
|
|
|
updateConstraint = c.value.value;
|
|
|
|
updateConstraint =
|
|
|
|
updateConstraint[0].toUpperCase() +
|
|
|
|
updateConstraint.substring(1);
|
|
|
|
} else if (c.type === "on delete") {
|
|
|
|
deleteConstraint = c.value.value;
|
|
|
|
deleteConstraint =
|
|
|
|
deleteConstraint[0].toUpperCase() +
|
|
|
|
deleteConstraint.substring(1);
|
|
|
|
}
|
2024-01-22 22:35:01 +08:00
|
|
|
}
|
2024-02-13 03:05:21 +08:00
|
|
|
);
|
2024-01-22 22:35:01 +08:00
|
|
|
|
|
|
|
let startTableId = -1;
|
|
|
|
let startFieldId = -1;
|
|
|
|
let endTableId = -1;
|
|
|
|
let endFieldId = -1;
|
|
|
|
|
2024-02-13 03:05:21 +08:00
|
|
|
tables.forEach((t) => {
|
2024-01-22 22:35:01 +08:00
|
|
|
if (t.name === startTable) {
|
|
|
|
startTableId = t.id;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (t.name === endTable) {
|
|
|
|
endTableId = t.id;
|
|
|
|
}
|
2024-02-13 03:05:21 +08:00
|
|
|
});
|
2024-01-22 22:35:01 +08:00
|
|
|
|
|
|
|
if (startTableId === -1 || endTableId === -1) return;
|
|
|
|
|
2024-02-13 03:05:21 +08:00
|
|
|
tables[startTableId].fields.forEach((f) => {
|
2024-01-22 22:35:01 +08:00
|
|
|
if (f.name === startField) {
|
|
|
|
startFieldId = f.id;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (f.name === endField) {
|
|
|
|
endFieldId = f.id;
|
|
|
|
}
|
2024-02-13 03:05:21 +08:00
|
|
|
});
|
2024-01-22 22:35:01 +08:00
|
|
|
|
|
|
|
if (startFieldId === -1 || endFieldId === -1) return;
|
|
|
|
|
|
|
|
const startX = tables[startTableId].x + 15;
|
|
|
|
const startY = tables[startTableId].y + startFieldId * 36 + 69;
|
|
|
|
const endX = tables[endTableId].x + 15;
|
|
|
|
const endY = tables[endTableId].y + endFieldId * 36 + 69;
|
|
|
|
|
|
|
|
relationship.mandetory = false;
|
|
|
|
|
|
|
|
relationship.name = startTable + "_" + startField + "_fk";
|
|
|
|
relationship.startTableId = startTableId;
|
|
|
|
relationship.startFieldId = startFieldId;
|
|
|
|
relationship.endTableId = endTableId;
|
|
|
|
relationship.endFieldId = endFieldId;
|
|
|
|
relationship.updateConstraint = updateConstraint;
|
|
|
|
relationship.deleteConstraint = deleteConstraint;
|
|
|
|
relationship.cardinality = Cardinality.ONE_TO_ONE;
|
|
|
|
relationship.startX = startX;
|
|
|
|
relationship.startY = startY;
|
|
|
|
relationship.endX = endX;
|
|
|
|
relationship.endY = endY;
|
|
|
|
relationships.push(relationship);
|
|
|
|
|
2024-02-13 03:05:21 +08:00
|
|
|
relationships.forEach((r, i) => (r.id = i));
|
2024-01-22 22:35:01 +08:00
|
|
|
}
|
2024-01-21 15:05:43 +08:00
|
|
|
}
|
2024-02-13 03:05:21 +08:00
|
|
|
});
|
2024-01-21 15:05:43 +08:00
|
|
|
|
2024-01-23 16:15:28 +08:00
|
|
|
inlineForeignKeys.forEach((fk) => {
|
|
|
|
const relationship = {};
|
|
|
|
const startTable = fk.startTable;
|
|
|
|
const startField = fk.definition[0].column;
|
|
|
|
const endTable = fk.reference_definition.table[0].table;
|
|
|
|
const endField = fk.reference_definition.definition[0].column;
|
|
|
|
let updateConstraint = "No action";
|
|
|
|
let deleteConstraint = "No action";
|
2024-02-13 03:05:21 +08:00
|
|
|
fk.reference_definition.on_action.forEach((c) => {
|
2024-01-23 16:15:28 +08:00
|
|
|
if (c.type === "on update") {
|
|
|
|
updateConstraint = c.value.value;
|
2024-02-13 03:05:21 +08:00
|
|
|
updateConstraint =
|
|
|
|
updateConstraint[0].toUpperCase() + updateConstraint.substring(1);
|
2024-01-23 16:15:28 +08:00
|
|
|
} else if (c.type === "on delete") {
|
|
|
|
deleteConstraint = c.value.value;
|
2024-02-13 03:05:21 +08:00
|
|
|
deleteConstraint =
|
|
|
|
deleteConstraint[0].toUpperCase() + deleteConstraint.substring(1);
|
2024-01-23 16:15:28 +08:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
let startTableId = -1;
|
|
|
|
let startFieldId = -1;
|
|
|
|
let endTableId = -1;
|
|
|
|
let endFieldId = -1;
|
|
|
|
|
2024-02-13 03:05:21 +08:00
|
|
|
tables.forEach((t) => {
|
2024-01-23 16:15:28 +08:00
|
|
|
if (t.name === startTable) {
|
|
|
|
startTableId = t.id;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (t.name === endTable) {
|
|
|
|
endTableId = t.id;
|
|
|
|
}
|
2024-02-13 03:05:21 +08:00
|
|
|
});
|
2024-01-23 16:15:28 +08:00
|
|
|
|
|
|
|
if (startTableId === -1 || endTableId === -1) return;
|
|
|
|
|
2024-02-13 03:05:21 +08:00
|
|
|
tables[startTableId].fields.forEach((f) => {
|
2024-01-23 16:15:28 +08:00
|
|
|
if (f.name === startField) {
|
|
|
|
startFieldId = f.id;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (f.name === endField) {
|
|
|
|
endFieldId = f.id;
|
|
|
|
}
|
2024-02-13 03:05:21 +08:00
|
|
|
});
|
2024-01-23 16:15:28 +08:00
|
|
|
|
|
|
|
if (startFieldId === -1 || endFieldId === -1) return;
|
|
|
|
|
|
|
|
const startX = tables[startTableId].x + 15;
|
|
|
|
const startY = tables[startTableId].y + startFieldId * 36 + 69;
|
|
|
|
const endX = tables[endTableId].x + 15;
|
|
|
|
const endY = tables[endTableId].y + endFieldId * 36 + 69;
|
|
|
|
|
|
|
|
relationship.name = startTable + "_" + startField + "_fk";
|
|
|
|
relationship.startTableId = startTableId;
|
|
|
|
relationship.startFieldId = startFieldId;
|
|
|
|
relationship.endTableId = endTableId;
|
|
|
|
relationship.endFieldId = endFieldId;
|
|
|
|
relationship.updateConstraint = updateConstraint;
|
|
|
|
relationship.deleteConstraint = deleteConstraint;
|
|
|
|
relationship.cardinality = Cardinality.ONE_TO_ONE;
|
|
|
|
relationship.startX = startX;
|
|
|
|
relationship.startY = startY;
|
|
|
|
relationship.endX = endX;
|
|
|
|
relationship.endY = endY;
|
|
|
|
relationships.push(relationship);
|
|
|
|
});
|
2024-01-29 15:53:05 +08:00
|
|
|
|
2024-02-13 03:05:21 +08:00
|
|
|
relationships.forEach((r, i) => (r.id = i));
|
2024-01-23 16:15:28 +08:00
|
|
|
|
2024-01-22 22:35:01 +08:00
|
|
|
if (data.overwrite) {
|
|
|
|
setTables(tables);
|
|
|
|
setRelationships(relationships);
|
|
|
|
setNotes([]);
|
|
|
|
setAreas([]);
|
|
|
|
setTypes([]);
|
|
|
|
setUndoStack([]);
|
|
|
|
setRedoStack([]);
|
|
|
|
} else {
|
2024-02-13 03:05:21 +08:00
|
|
|
setTables((prev) => [...prev, ...tables]);
|
|
|
|
setRelationships((prev) => [...prev, ...relationships]);
|
2024-01-22 22:35:01 +08:00
|
|
|
}
|
2024-01-21 15:05:43 +08:00
|
|
|
|
|
|
|
console.log(tables);
|
2024-01-22 22:35:01 +08:00
|
|
|
console.log(relationships);
|
2024-02-13 03:05:21 +08:00
|
|
|
};
|
2024-01-21 15:05:43 +08:00
|
|
|
|
2024-01-14 02:36:13 +08:00
|
|
|
const getModalOnOk = async () => {
|
2023-10-22 00:45:13 +08:00
|
|
|
switch (visible) {
|
|
|
|
case MODAL.IMG:
|
|
|
|
saveAs(
|
|
|
|
exportData.data,
|
|
|
|
`${exportData.filename}.${exportData.extension}`
|
|
|
|
);
|
|
|
|
return;
|
2023-12-16 11:39:13 +08:00
|
|
|
case MODAL.CODE: {
|
2023-10-22 00:45:13 +08:00
|
|
|
const blob = new Blob([exportData.data], {
|
|
|
|
type: "application/json",
|
|
|
|
});
|
|
|
|
saveAs(blob, `${exportData.filename}.${exportData.extension}`);
|
|
|
|
return;
|
2023-12-16 11:39:13 +08:00
|
|
|
}
|
2023-10-22 00:45:13 +08:00
|
|
|
case MODAL.IMPORT:
|
|
|
|
if (error.type !== STATUS.ERROR) {
|
2024-03-10 02:35:04 +08:00
|
|
|
setTransform((prev) => ({ ...prev, pan: { x: 0, y: 0 } }));
|
2023-10-22 00:45:13 +08:00
|
|
|
overwriteDiagram();
|
|
|
|
setData(null);
|
|
|
|
setVisible(MODAL.NONE);
|
|
|
|
setUndoStack([]);
|
|
|
|
setRedoStack([]);
|
|
|
|
}
|
|
|
|
return;
|
2024-01-18 18:45:58 +08:00
|
|
|
case MODAL.IMPORT_SRC:
|
2024-01-21 15:05:43 +08:00
|
|
|
parseSQLAndLoadDiagram();
|
2024-02-13 03:05:21 +08:00
|
|
|
setVisible(MODAL.NONE);
|
2024-01-18 18:45:58 +08:00
|
|
|
return;
|
2023-10-28 01:18:28 +08:00
|
|
|
case MODAL.OPEN:
|
|
|
|
if (selectedDiagramId === 0) return;
|
|
|
|
loadDiagram(selectedDiagramId);
|
|
|
|
setVisible(MODAL.NONE);
|
|
|
|
return;
|
2023-11-02 19:31:26 +08:00
|
|
|
case MODAL.RENAME:
|
|
|
|
setPrevTitle(title);
|
|
|
|
setVisible(MODAL.NONE);
|
|
|
|
return;
|
2023-10-28 07:11:31 +08:00
|
|
|
case MODAL.SAVEAS:
|
2024-02-29 08:48:24 +08:00
|
|
|
setTitle(saveAsTitle);
|
2023-10-28 07:11:31 +08:00
|
|
|
setVisible(MODAL.NONE);
|
|
|
|
return;
|
2023-11-02 19:31:26 +08:00
|
|
|
case MODAL.NEW:
|
|
|
|
setVisible(MODAL.NONE);
|
|
|
|
createNewDiagram(selectedTemplateId);
|
|
|
|
return;
|
2023-10-22 00:45:13 +08:00
|
|
|
default:
|
|
|
|
setVisible(MODAL.NONE);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const importModalBody = () => {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<Upload
|
|
|
|
action="#"
|
|
|
|
beforeUpload={({ file, fileList }) => {
|
|
|
|
const f = fileList[0].fileInstance;
|
|
|
|
if (!f) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const reader = new FileReader();
|
2023-12-20 08:57:29 +08:00
|
|
|
reader.onload = async (e) => {
|
2023-10-22 00:45:13 +08:00
|
|
|
let jsonObject = null;
|
|
|
|
try {
|
2023-12-20 08:57:29 +08:00
|
|
|
jsonObject = JSON.parse(e.target.result);
|
2023-10-22 00:45:13 +08:00
|
|
|
} catch (error) {
|
|
|
|
setError({
|
|
|
|
type: STATUS.ERROR,
|
|
|
|
message: "The file contains an error.",
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (f.type === "application/json") {
|
|
|
|
if (!jsonDiagramIsValid(jsonObject)) {
|
|
|
|
setError({
|
|
|
|
type: STATUS.ERROR,
|
|
|
|
message:
|
|
|
|
"The file is missing necessary properties for a diagram.",
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} else if (f.name.split(".").pop() === "ddb") {
|
|
|
|
if (!ddbDiagramIsValid(jsonObject)) {
|
|
|
|
setError({
|
|
|
|
type: STATUS.ERROR,
|
|
|
|
message:
|
|
|
|
"The file is missing necessary properties for a diagram.",
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setData(jsonObject);
|
|
|
|
if (diagramIsEmpty()) {
|
|
|
|
setError({
|
|
|
|
type: STATUS.OK,
|
|
|
|
message: "Everything looks good. You can now import.",
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
setError({
|
|
|
|
type: STATUS.WARNING,
|
|
|
|
message:
|
|
|
|
"The current diagram is not empty. Importing a new diagram will overwrite the current changes.",
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
reader.readAsText(f);
|
|
|
|
|
|
|
|
return {
|
|
|
|
autoRemove: false,
|
|
|
|
fileInstance: file.fileInstance,
|
|
|
|
status: "success",
|
|
|
|
shouldUpload: false,
|
|
|
|
};
|
|
|
|
}}
|
|
|
|
draggable={true}
|
|
|
|
dragMainText="Drag and drop the file here or click to upload."
|
|
|
|
dragSubText="Support json and ddb"
|
|
|
|
accept="application/json,.ddb"
|
|
|
|
onRemove={() =>
|
|
|
|
setError({
|
|
|
|
type: STATUS.NONE,
|
|
|
|
message: "",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
onFileChange={() =>
|
|
|
|
setError({
|
|
|
|
type: STATUS.NONE,
|
|
|
|
message: "",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
limit={1}
|
2024-03-16 08:23:10 +08:00
|
|
|
/>
|
2023-10-22 00:45:13 +08:00
|
|
|
{error.type === STATUS.ERROR ? (
|
|
|
|
<Banner
|
|
|
|
type="danger"
|
|
|
|
fullMode={false}
|
2024-01-23 14:38:59 +08:00
|
|
|
description={<div>{error.message}</div>}
|
2023-10-22 00:45:13 +08:00
|
|
|
/>
|
|
|
|
) : error.type === STATUS.OK ? (
|
|
|
|
<Banner
|
|
|
|
type="info"
|
|
|
|
fullMode={false}
|
|
|
|
description={<div>{error.message}</div>}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
error.type === STATUS.WARNING && (
|
|
|
|
<Banner
|
|
|
|
type="warning"
|
|
|
|
fullMode={false}
|
|
|
|
description={<div>{error.message}</div>}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2024-01-18 18:45:58 +08:00
|
|
|
const importSrcModalBody = () => {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<Upload
|
|
|
|
action="#"
|
|
|
|
beforeUpload={({ file, fileList }) => {
|
|
|
|
const f = fileList[0].fileInstance;
|
|
|
|
if (!f) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = async (e) => {
|
2024-02-13 03:05:21 +08:00
|
|
|
setData((prev) => ({ ...prev, src: e.target.result }));
|
2024-01-18 18:45:58 +08:00
|
|
|
};
|
|
|
|
reader.readAsText(f);
|
|
|
|
|
|
|
|
return {
|
|
|
|
autoRemove: false,
|
|
|
|
fileInstance: file.fileInstance,
|
|
|
|
status: "success",
|
|
|
|
shouldUpload: false,
|
|
|
|
};
|
|
|
|
}}
|
|
|
|
draggable={true}
|
|
|
|
dragMainText="Drag and drop the file here or click to upload."
|
|
|
|
dragSubText="Upload an sql file to autogenerate your tables and columns."
|
|
|
|
accept=".sql"
|
2024-01-21 15:05:43 +08:00
|
|
|
onRemove={() => {
|
2024-01-18 18:45:58 +08:00
|
|
|
setError({
|
|
|
|
type: STATUS.NONE,
|
|
|
|
message: "",
|
2024-01-21 15:05:43 +08:00
|
|
|
});
|
2024-02-13 03:05:21 +08:00
|
|
|
setData((prev) => ({ ...prev, src: "" }));
|
|
|
|
}}
|
2024-01-18 18:45:58 +08:00
|
|
|
onFileChange={() =>
|
|
|
|
setError({
|
|
|
|
type: STATUS.NONE,
|
|
|
|
message: "",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
limit={1}
|
|
|
|
></Upload>
|
2024-01-21 15:05:43 +08:00
|
|
|
<div className="my-2">
|
|
|
|
<div className="text-sm font-semibold mb-1">Select DBMS</div>
|
2024-02-13 03:05:21 +08:00
|
|
|
<Select
|
|
|
|
defaultValue="MySQL"
|
2024-01-21 15:05:43 +08:00
|
|
|
optionList={[
|
2024-02-13 03:05:21 +08:00
|
|
|
{ value: "MySQL", label: "MySQL" },
|
|
|
|
{ value: "Postgresql", label: "PostgreSQL" },
|
2024-01-21 15:05:43 +08:00
|
|
|
]}
|
2024-02-13 03:05:21 +08:00
|
|
|
onChange={(e) => setData((prev) => ({ ...prev, dbms: e }))}
|
|
|
|
className="w-full"
|
2024-03-16 08:23:10 +08:00
|
|
|
/>
|
2024-02-13 03:05:21 +08:00
|
|
|
<Checkbox
|
|
|
|
aria-label="overwrite checkbox"
|
2024-01-21 15:05:43 +08:00
|
|
|
checked={data.overwrite}
|
|
|
|
defaultChecked
|
2024-02-13 03:05:21 +08:00
|
|
|
onChange={(e) =>
|
|
|
|
setData((prev) => ({ ...prev, overwrite: e.target.checked }))
|
|
|
|
}
|
|
|
|
className="my-2"
|
|
|
|
>
|
2024-01-21 15:05:43 +08:00
|
|
|
Overwrite existing diagram
|
|
|
|
</Checkbox>
|
|
|
|
</div>
|
2024-01-18 18:45:58 +08:00
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2023-11-02 19:31:26 +08:00
|
|
|
const newModalBody = () => (
|
|
|
|
<div className="h-[360px] grid grid-cols-3 gap-2 overflow-auto px-1">
|
2024-02-16 22:27:09 +08:00
|
|
|
<div onClick={() => setSelectedTemplateId(0)}>
|
2023-11-02 19:31:26 +08:00
|
|
|
<div
|
2024-02-16 22:27:09 +08:00
|
|
|
className={`rounded-md h-[180px] border-2 hover:border-dashed ${
|
|
|
|
selectedTemplateId === 0 ? "border-blue-400" : "border-zinc-100"
|
|
|
|
}`}
|
2023-11-02 19:31:26 +08:00
|
|
|
>
|
2024-02-16 22:27:09 +08:00
|
|
|
<Thumbnail i={0} diagram={{}} zoom={0.24} />
|
2023-10-27 01:26:13 +08:00
|
|
|
</div>
|
2023-11-02 19:31:26 +08:00
|
|
|
<div className="text-center mt-1">Blank</div>
|
|
|
|
</div>
|
2024-02-16 22:27:09 +08:00
|
|
|
{defaultTemplates.map((temp, i) => (
|
|
|
|
<div key={i} onClick={() => setSelectedTemplateId(temp.id)}>
|
2023-11-02 19:31:26 +08:00
|
|
|
<div
|
2024-02-16 22:27:09 +08:00
|
|
|
className={`rounded-md h-[180px] border-2 hover:border-dashed ${
|
|
|
|
selectedTemplateId === temp.id
|
|
|
|
? "border-blue-400"
|
|
|
|
: "border-zinc-100"
|
|
|
|
}`}
|
2023-11-02 19:31:26 +08:00
|
|
|
>
|
2024-02-16 22:27:09 +08:00
|
|
|
<Thumbnail i={temp.id} diagram={temp} zoom={0.24} />
|
2023-11-02 19:31:26 +08:00
|
|
|
</div>
|
2024-02-16 22:27:09 +08:00
|
|
|
<div className="text-center mt-1">{temp.title}</div>
|
2023-11-02 19:31:26 +08:00
|
|
|
</div>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
|
|
|
|
const getModalBody = () => {
|
|
|
|
switch (visible) {
|
|
|
|
case MODAL.IMPORT:
|
|
|
|
return importModalBody();
|
2024-01-18 18:45:58 +08:00
|
|
|
case MODAL.IMPORT_SRC:
|
|
|
|
return importSrcModalBody();
|
2023-11-02 19:31:26 +08:00
|
|
|
case MODAL.NEW:
|
|
|
|
return newModalBody();
|
|
|
|
case MODAL.RENAME:
|
|
|
|
return (
|
2023-10-22 00:45:13 +08:00
|
|
|
<Input
|
2023-11-02 19:31:26 +08:00
|
|
|
placeholder="Diagram name"
|
|
|
|
value={title}
|
|
|
|
onChange={(v) => setTitle(v)}
|
2023-10-22 00:45:13 +08:00
|
|
|
/>
|
2023-11-02 19:31:26 +08:00
|
|
|
);
|
|
|
|
case MODAL.OPEN:
|
|
|
|
return (
|
|
|
|
<div>
|
2023-12-27 10:45:23 +08:00
|
|
|
{diagrams?.length === 0 ? (
|
2023-11-02 19:31:26 +08:00
|
|
|
<Banner
|
|
|
|
fullMode={false}
|
|
|
|
type="info"
|
|
|
|
bordered
|
|
|
|
icon={null}
|
|
|
|
closeIcon={null}
|
|
|
|
description={<div>You have no saved diagrams.</div>}
|
|
|
|
/>
|
|
|
|
) : (
|
2023-11-23 03:32:17 +08:00
|
|
|
<div className="max-h-[360px]">
|
2023-11-02 19:31:26 +08:00
|
|
|
<table className="w-full text-left border-separate border-spacing-x-0">
|
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<th>Name</th>
|
|
|
|
<th>Last Modified</th>
|
|
|
|
<th>Size</th>
|
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
<tbody>
|
|
|
|
{diagrams?.map((d) => {
|
|
|
|
const size = JSON.stringify(d).length;
|
|
|
|
let sizeStr;
|
|
|
|
if (size >= 1024 && size < 1024 * 1024)
|
|
|
|
sizeStr = (size / 1024).toFixed(1) + "KB";
|
|
|
|
else if (size >= 1024 * 1024)
|
|
|
|
sizeStr = (size / (1024 * 1024)).toFixed(1) + "MB";
|
|
|
|
else sizeStr = size + "B";
|
|
|
|
return (
|
|
|
|
<tr
|
|
|
|
key={d.id}
|
2024-02-13 03:05:21 +08:00
|
|
|
className={`${
|
|
|
|
selectedDiagramId === d.id
|
|
|
|
? "bg-blue-300 bg-opacity-30"
|
|
|
|
: "hover-1"
|
|
|
|
}`}
|
2023-11-02 19:31:26 +08:00
|
|
|
onClick={() => {
|
|
|
|
setSelectedDiagramId(d.id);
|
|
|
|
}}
|
|
|
|
onDoubleClick={() => {
|
|
|
|
loadDiagram(d.id);
|
2023-12-25 05:26:14 +08:00
|
|
|
window.name = "d " + d.id;
|
2023-11-02 19:31:26 +08:00
|
|
|
setVisible(MODAL.NONE);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<td className="py-1">
|
2024-03-16 08:23:10 +08:00
|
|
|
<i className="bi bi-file-earmark-text text-[16px] me-1 opacity-60" />
|
2023-11-02 19:31:26 +08:00
|
|
|
{d.name}
|
|
|
|
</td>
|
|
|
|
<td className="py-1">
|
|
|
|
{d.lastModified.toLocaleDateString() +
|
|
|
|
" " +
|
|
|
|
d.lastModified.toLocaleTimeString()}
|
|
|
|
</td>
|
|
|
|
<td className="py-1">{sizeStr}</td>
|
|
|
|
</tr>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</tbody>
|
|
|
|
</table>
|
2023-11-23 03:32:17 +08:00
|
|
|
</div>
|
2023-11-02 19:31:26 +08:00
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
case MODAL.SAVEAS:
|
|
|
|
return (
|
|
|
|
<Input
|
|
|
|
placeholder="Diagram name"
|
|
|
|
value={saveAsTitle}
|
|
|
|
onChange={(v) => setSaveAsTitle(v)}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
case MODAL.CODE:
|
|
|
|
case MODAL.IMG:
|
|
|
|
if (exportData.data !== "" || exportData.data) {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{visible === MODAL.IMG ? (
|
|
|
|
<Image src={exportData.data} alt="Diagram" height={280} />
|
|
|
|
) : (
|
|
|
|
<Editor
|
|
|
|
height="360px"
|
|
|
|
value={exportData.data}
|
|
|
|
language={exportData.extension}
|
|
|
|
options={{ readOnly: true }}
|
|
|
|
theme={settings.mode === "light" ? "light" : "vs-dark"}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
<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"
|
|
|
|
/>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return (
|
|
|
|
<div className="text-center my-3">
|
|
|
|
<Spin tip="Loading..." size="large" />
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return <></>;
|
2023-10-22 00:45:13 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-09-19 20:46:48 +08:00
|
|
|
return (
|
2023-09-19 20:51:18 +08:00
|
|
|
<>
|
2023-09-19 20:48:46 +08:00
|
|
|
{layout.header && header()}
|
2024-02-03 19:48:07 +08:00
|
|
|
{layout.toolbar && toolbar()}
|
2023-09-19 20:48:26 +08:00
|
|
|
<Modal
|
2023-10-22 00:45:13 +08:00
|
|
|
title={getModalTitle()}
|
2023-09-19 20:49:20 +08:00
|
|
|
visible={visible !== MODAL.NONE}
|
2023-10-22 00:45:13 +08:00
|
|
|
onOk={getModalOnOk}
|
2023-09-19 20:48:27 +08:00
|
|
|
afterClose={() => {
|
2023-12-16 11:39:13 +08:00
|
|
|
setExportData(() => ({
|
2023-09-19 20:49:20 +08:00
|
|
|
data: "",
|
|
|
|
extension: "",
|
2024-02-28 23:07:39 +08:00
|
|
|
filename: `${title}_${new Date().toISOString()}`,
|
2023-09-19 20:49:20 +08:00
|
|
|
}));
|
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-10-22 00:45:13 +08:00
|
|
|
onCancel={() => {
|
|
|
|
if (visible === MODAL.RENAME) setTitle(prevTitle);
|
|
|
|
setVisible(MODAL.NONE);
|
|
|
|
}}
|
2023-09-19 20:48:26 +08:00
|
|
|
centered
|
|
|
|
closeOnEsc={true}
|
2023-10-22 00:45:13 +08:00
|
|
|
okText={getOkText()}
|
2023-09-19 20:49:50 +08:00
|
|
|
okButtonProps={{
|
2024-02-13 03:05:21 +08:00
|
|
|
disabled:
|
|
|
|
(error && error.type && error.type === STATUS.ERROR) ||
|
2023-09-19 20:49:50 +08:00
|
|
|
(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) &&
|
2023-10-28 01:18:28 +08:00
|
|
|
!exportData.data) ||
|
2023-10-28 07:11:31 +08:00
|
|
|
(visible === MODAL.RENAME && title === "") ||
|
2024-01-21 15:05:43 +08:00
|
|
|
(visible === MODAL.SAVEAS && saveAsTitle === "") ||
|
|
|
|
(visible === MODAL.IMPORT_SRC && data.src === ""),
|
2023-09-19 20:49:50 +08:00
|
|
|
}}
|
2023-09-19 20:48:26 +08:00
|
|
|
cancelText="Cancel"
|
2024-02-16 22:27:09 +08:00
|
|
|
width={visible === MODAL.NEW ? 740 : 600}
|
2023-09-19 20:48:26 +08:00
|
|
|
>
|
2023-10-22 00:45:13 +08:00
|
|
|
{getModalBody()}
|
2023-09-19 20:48:26 +08:00
|
|
|
</Modal>
|
2024-01-14 08:05:07 +08:00
|
|
|
<SideSheet
|
|
|
|
visible={sidesheet !== SIDESHEET.NONE}
|
|
|
|
onCancel={() => {
|
|
|
|
setSidesheet(SIDESHEET.NONE);
|
|
|
|
}}
|
|
|
|
width={340}
|
|
|
|
title={getTitle(sidesheet)}
|
|
|
|
style={{ paddingBottom: "16px" }}
|
|
|
|
bodyStyle={{ padding: "0px" }}
|
|
|
|
>
|
|
|
|
{getContent(sidesheet)}
|
|
|
|
</SideSheet>
|
2023-09-19 20:51:18 +08:00
|
|
|
</>
|
2023-09-19 20:46:48 +08:00
|
|
|
);
|
2023-09-19 20:48:34 +08:00
|
|
|
|
2024-01-14 08:05:07 +08:00
|
|
|
function getTitle(type) {
|
|
|
|
switch (type) {
|
|
|
|
case SIDESHEET.TIMELINE:
|
|
|
|
return (
|
|
|
|
<div className="flex items-center">
|
|
|
|
<img
|
|
|
|
src={settings.mode === "light" ? timeLine : timeLineDark}
|
|
|
|
className="w-7"
|
|
|
|
alt="chat icon"
|
|
|
|
/>
|
|
|
|
<div className="ms-3 text-lg">Timeline</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
case SIDESHEET.TODO:
|
|
|
|
return (
|
|
|
|
<div className="flex items-center">
|
|
|
|
<img src={todo} className="w-7" alt="todo icon" />
|
|
|
|
<div className="ms-3 text-lg">To-do list</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function getContent(type) {
|
|
|
|
switch (type) {
|
|
|
|
case SIDESHEET.TIMELINE:
|
|
|
|
return renderTimeline();
|
|
|
|
case SIDESHEET.TODO:
|
|
|
|
return <Todo />;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function renderTimeline() {
|
|
|
|
if (undoStack.length > 0) {
|
|
|
|
return (
|
|
|
|
<List className="sidesheet-theme">
|
|
|
|
{[...undoStack].reverse().map((e, i) => (
|
|
|
|
<List.Item
|
|
|
|
key={i}
|
|
|
|
style={{ padding: "4px 18px 4px 18px" }}
|
|
|
|
className="hover-1"
|
|
|
|
>
|
|
|
|
<div className="flex items-center py-1 w-full">
|
2024-03-16 08:23:10 +08:00
|
|
|
<i className="block fa-regular fa-circle fa-xs" />
|
2024-01-14 08:05:07 +08:00
|
|
|
<div className="ms-2">{e.message}</div>
|
|
|
|
</div>
|
|
|
|
</List.Item>
|
|
|
|
))}
|
|
|
|
</List>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return (
|
|
|
|
<div className="m-5 sidesheet-theme">
|
|
|
|
No activity was recorded. You have not added anything to your diagram
|
|
|
|
yet.
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-19 20:51:18 +08:00
|
|
|
function toolbar() {
|
|
|
|
return (
|
2024-02-03 19:48:07 +08:00
|
|
|
<div className="py-1.5 px-5 flex justify-between items-center rounded-xl my-1 sm:mx-1 xl:mx-6 select-none overflow-hidden toolbar-theme">
|
2023-09-19 20:51:18 +08:00
|
|
|
<div className="flex justify-start items-center">
|
|
|
|
{layoutDropdown()}
|
|
|
|
<Divider layout="vertical" margin="8px" />
|
|
|
|
<Dropdown
|
|
|
|
style={{ width: "240px" }}
|
|
|
|
position="bottomLeft"
|
|
|
|
render={
|
|
|
|
<Dropdown.Menu>
|
|
|
|
<Dropdown.Item
|
|
|
|
onClick={fitWindow}
|
|
|
|
style={{ display: "flex", justifyContent: "space-between" }}
|
|
|
|
>
|
|
|
|
<div>Fit window / Reset</div>
|
|
|
|
<div className="text-gray-400">Ctrl+Alt+W</div>
|
|
|
|
</Dropdown.Item>
|
|
|
|
<Dropdown.Divider />
|
|
|
|
{[0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0].map((e, i) => (
|
|
|
|
<Dropdown.Item
|
|
|
|
key={i}
|
|
|
|
onClick={() => {
|
2024-03-10 02:35:04 +08:00
|
|
|
setTransform((prev) => ({ ...prev, zoom: e }));
|
2023-09-19 20:51:18 +08:00
|
|
|
}}
|
|
|
|
>
|
|
|
|
{Math.floor(e * 100)}%
|
|
|
|
</Dropdown.Item>
|
|
|
|
))}
|
|
|
|
<Dropdown.Divider />
|
|
|
|
<Dropdown.Item>
|
|
|
|
<InputNumber
|
|
|
|
field="zoom"
|
|
|
|
label="Custom zoom"
|
|
|
|
placeholder="Zoom"
|
|
|
|
suffix={<div className="p-1">%</div>}
|
|
|
|
onChange={(v) =>
|
2024-03-10 02:35:04 +08:00
|
|
|
setTransform((prev) => ({
|
2023-09-19 20:51:18 +08:00
|
|
|
...prev,
|
|
|
|
zoom: parseFloat(v) * 0.01,
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</Dropdown.Item>
|
|
|
|
</Dropdown.Menu>
|
|
|
|
}
|
|
|
|
trigger="click"
|
|
|
|
>
|
|
|
|
<div className="py-1 px-2 hover-2 rounded flex items-center justify-center">
|
2024-03-10 02:35:04 +08:00
|
|
|
<div className="w-[40px]">
|
|
|
|
{Math.floor(transform.zoom * 100)}%
|
|
|
|
</div>
|
2023-09-19 20:51:18 +08:00
|
|
|
<div>
|
|
|
|
<IconCaretdown />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</Dropdown>
|
|
|
|
<Tooltip content="Zoom in" position="bottom">
|
|
|
|
<button
|
|
|
|
className="py-1 px-2 hover-2 rounded text-lg"
|
|
|
|
onClick={() =>
|
2024-03-10 02:35:04 +08:00
|
|
|
setTransform((prev) => ({ ...prev, zoom: prev.zoom * 1.2 }))
|
2023-09-19 20:51:18 +08:00
|
|
|
}
|
|
|
|
>
|
2024-03-16 08:23:10 +08:00
|
|
|
<i className="fa-solid fa-magnifying-glass-plus" />
|
2023-09-19 20:51:18 +08:00
|
|
|
</button>
|
|
|
|
</Tooltip>
|
|
|
|
<Tooltip content="Zoom out" position="bottom">
|
|
|
|
<button
|
|
|
|
className="py-1 px-2 hover-2 rounded text-lg"
|
|
|
|
onClick={() =>
|
2024-03-10 02:35:04 +08:00
|
|
|
setTransform((prev) => ({ ...prev, zoom: prev.zoom / 1.2 }))
|
2023-09-19 20:51:18 +08:00
|
|
|
}
|
|
|
|
>
|
2024-03-16 08:23:10 +08:00
|
|
|
<i className="fa-solid fa-magnifying-glass-minus" />
|
2023-09-19 20:51:18 +08:00
|
|
|
</button>
|
|
|
|
</Tooltip>
|
|
|
|
<Divider layout="vertical" margin="8px" />
|
|
|
|
<Tooltip content="Undo" position="bottom">
|
|
|
|
<button
|
|
|
|
className="py-1 px-2 hover-2 rounded flex items-center"
|
|
|
|
onClick={undo}
|
|
|
|
>
|
|
|
|
<IconUndo
|
|
|
|
size="large"
|
|
|
|
style={{ color: undoStack.length === 0 ? "#9598a6" : "" }}
|
|
|
|
/>
|
|
|
|
</button>
|
|
|
|
</Tooltip>
|
|
|
|
<Tooltip content="Redo" position="bottom">
|
|
|
|
<button
|
|
|
|
className="py-1 px-2 hover-2 rounded flex items-center"
|
|
|
|
onClick={redo}
|
|
|
|
>
|
|
|
|
<IconRedo
|
|
|
|
size="large"
|
|
|
|
style={{ color: redoStack.length === 0 ? "#9598a6" : "" }}
|
|
|
|
/>
|
|
|
|
</button>
|
|
|
|
</Tooltip>
|
|
|
|
<Divider layout="vertical" margin="8px" />
|
|
|
|
<Tooltip content="Add table" position="bottom">
|
|
|
|
<button
|
|
|
|
className="flex items-center py-1 px-2 hover-2 rounded"
|
|
|
|
onClick={() => addTable()}
|
|
|
|
>
|
|
|
|
<IconAddTable theme={settings.mode} />
|
|
|
|
</button>
|
|
|
|
</Tooltip>
|
|
|
|
<Tooltip content="Add subject area" position="bottom">
|
|
|
|
<button
|
|
|
|
className="py-1 px-2 hover-2 rounded flex items-center"
|
|
|
|
onClick={() => addArea()}
|
|
|
|
>
|
|
|
|
<IconAddArea theme={settings.mode} />
|
|
|
|
</button>
|
|
|
|
</Tooltip>
|
|
|
|
<Tooltip content="Add note" position="bottom">
|
|
|
|
<button
|
|
|
|
className="py-1 px-2 hover-2 rounded flex items-center"
|
|
|
|
onClick={() => addNote()}
|
|
|
|
>
|
|
|
|
<IconAddNote theme={settings.mode} />
|
|
|
|
</button>
|
|
|
|
</Tooltip>
|
|
|
|
<Divider layout="vertical" margin="8px" />
|
|
|
|
<Tooltip content="Save" position="bottom">
|
2023-10-26 21:34:50 +08:00
|
|
|
<button
|
|
|
|
className="py-1 px-2 hover-2 rounded flex items-center"
|
|
|
|
onClick={save}
|
|
|
|
>
|
2023-09-19 20:51:18 +08:00
|
|
|
<IconSaveStroked size="extra-large" />
|
|
|
|
</button>
|
|
|
|
</Tooltip>
|
2024-01-14 08:05:07 +08:00
|
|
|
<Tooltip content="To-do" position="bottom">
|
2024-02-13 03:05:21 +08:00
|
|
|
<button
|
2024-03-01 01:06:08 +08:00
|
|
|
className="py-1 px-2 hover-2 rounded text-xl -mt-0.5"
|
2024-02-13 03:05:21 +08:00
|
|
|
onClick={() => setSidesheet(SIDESHEET.TODO)}
|
|
|
|
>
|
2024-03-16 08:23:10 +08:00
|
|
|
<i className="fa-regular fa-calendar-check" />
|
2023-09-19 20:51:18 +08:00
|
|
|
</button>
|
|
|
|
</Tooltip>
|
|
|
|
</div>
|
|
|
|
<button
|
|
|
|
onClick={() => invertLayout("header")}
|
|
|
|
className="flex items-center"
|
|
|
|
>
|
|
|
|
{layout.header ? <IconChevronUp /> : <IconChevronDown />}
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-11-25 00:28:39 +08:00
|
|
|
function getState() {
|
2024-03-15 22:00:23 +08:00
|
|
|
switch (saveState) {
|
2023-11-25 00:28:39 +08:00
|
|
|
case State.NONE:
|
2023-11-25 01:13:49 +08:00
|
|
|
return "No changes";
|
2023-11-25 00:28:39 +08:00
|
|
|
case State.LOADING:
|
|
|
|
return "Loading . . .";
|
|
|
|
case State.SAVED:
|
|
|
|
return `Last saved ${lastSaved}`;
|
|
|
|
case State.SAVING:
|
|
|
|
return "Saving . . .";
|
2023-12-25 05:26:14 +08:00
|
|
|
case State.ERROR:
|
|
|
|
return "Failed to save";
|
2023-11-25 00:28:39 +08:00
|
|
|
default:
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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">
|
2023-09-19 20:51:08 +08:00
|
|
|
<div className="flex justify-start items-center">
|
2023-09-19 20:48:35 +08:00
|
|
|
<Link to="/">
|
|
|
|
<img
|
|
|
|
width={54}
|
|
|
|
src={icon}
|
|
|
|
alt="logo"
|
|
|
|
className="ms-8 min-w-[54px]"
|
|
|
|
/>
|
|
|
|
</Link>
|
|
|
|
<div className="ms-1 mt-1">
|
2023-10-22 00:45:13 +08:00
|
|
|
<div className="flex items-center">
|
|
|
|
<div
|
|
|
|
className="text-xl ms-3 me-1"
|
|
|
|
onMouseEnter={() => setShowEditName(true)}
|
|
|
|
onMouseLeave={() => setShowEditName(false)}
|
|
|
|
onClick={() => setVisible(MODAL.RENAME)}
|
|
|
|
>
|
2023-12-25 05:26:14 +08:00
|
|
|
{window.name.split(" ")[0] === "t" ? "Templates/" : "Diagrams/"}
|
2023-10-22 00:45:13 +08:00
|
|
|
{title}
|
|
|
|
</div>
|
|
|
|
{(showEditName || visible === MODAL.RENAME) && <IconEdit />}
|
|
|
|
</div>
|
2023-09-19 20:48:35 +08:00
|
|
|
<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"
|
2023-09-19 20:50:22 +08:00
|
|
|
style={{ width: "220px" }}
|
2023-09-19 20:48:35 +08:00
|
|
|
render={
|
|
|
|
<Dropdown.Menu>
|
|
|
|
{Object.keys(menu[category]).map((item, index) => {
|
2023-09-19 20:50:22 +08:00
|
|
|
if (menu[category][item].children) {
|
2023-09-19 20:48:35 +08:00
|
|
|
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}
|
2023-09-19 20:50:22 +08:00
|
|
|
style={
|
|
|
|
menu[category][item].shortcut && {
|
|
|
|
display: "flex",
|
|
|
|
justifyContent: "space-between",
|
|
|
|
alignItems: "center",
|
|
|
|
}
|
|
|
|
}
|
2023-09-19 20:48:34 +08:00
|
|
|
>
|
2023-09-19 20:50:22 +08:00
|
|
|
{menu[category][item].shortcut ? (
|
|
|
|
<>
|
|
|
|
<div>{item}</div>
|
|
|
|
<div className="text-gray-400">
|
|
|
|
{menu[category][item].shortcut}
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
item
|
|
|
|
)}
|
2023-09-19 20:48:34 +08:00
|
|
|
</Dropdown.Item>
|
2023-09-19 20:48:35 +08:00
|
|
|
);
|
|
|
|
})}
|
|
|
|
</Dropdown.Menu>
|
|
|
|
}
|
|
|
|
>
|
2023-09-19 20:51:08 +08:00
|
|
|
<div className="px-3 py-1 hover-2 rounded">{category}</div>
|
2023-09-19 20:48:35 +08:00
|
|
|
</Dropdown>
|
|
|
|
))}
|
|
|
|
</div>
|
2023-11-25 00:28:39 +08:00
|
|
|
<Button
|
|
|
|
size="small"
|
|
|
|
type="tertiary"
|
|
|
|
icon={
|
2024-03-15 22:00:23 +08:00
|
|
|
saveState === State.LOADING || saveState === State.SAVING ? (
|
2023-11-25 00:28:39 +08:00
|
|
|
<Spin size="small" />
|
|
|
|
) : null
|
|
|
|
}
|
|
|
|
>
|
|
|
|
{getState()}
|
2023-09-19 20:48:35 +08:00
|
|
|
</Button>
|
2023-09-19 20:48:34 +08:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
2023-09-19 20:48:35 +08:00
|
|
|
</nav>
|
|
|
|
);
|
2023-09-19 20:48:34 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
function layoutDropdown() {
|
|
|
|
return (
|
|
|
|
<Dropdown
|
|
|
|
position="bottomLeft"
|
|
|
|
style={{ width: "180px" }}
|
|
|
|
render={
|
|
|
|
<Dropdown.Menu>
|
|
|
|
<Dropdown.Item
|
|
|
|
icon={
|
2024-03-16 08:23:10 +08:00
|
|
|
layout.header ? <IconCheckboxTick /> : <div className="px-2" />
|
2023-09-19 20:48:34 +08:00
|
|
|
}
|
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={
|
2024-03-16 08:23:10 +08:00
|
|
|
layout.sidebar ? <IconCheckboxTick /> : <div className="px-2" />
|
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={
|
2024-03-16 08:23:10 +08:00
|
|
|
layout.issues ? <IconCheckboxTick /> : <div className="px-2" />
|
2023-09-19 20:48:34 +08:00
|
|
|
}
|
2023-09-19 20:49:40 +08:00
|
|
|
onClick={() => invertLayout("issues")}
|
|
|
|
>
|
|
|
|
Issues
|
|
|
|
</Dropdown.Item>
|
2023-09-19 20:48:34 +08:00
|
|
|
<Dropdown.Divider />
|
2023-09-19 20:48:35 +08:00
|
|
|
<Dropdown.Item
|
2024-03-16 08:23:10 +08:00
|
|
|
icon={<div className="px-2" />}
|
2023-09-19 20:48:35 +08:00
|
|
|
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:51:08 +08:00
|
|
|
<div className="py-1 px-2 hover-2 rounded flex items-center justify-center">
|
|
|
|
<IconRowsStroked size="extra-large" />
|
2023-09-19 20:49:18 +08:00
|
|
|
<div>
|
|
|
|
<IconCaretdown />
|
|
|
|
</div>
|
2023-09-19 20:48:34 +08:00
|
|
|
</div>
|
|
|
|
</Dropdown>
|
|
|
|
);
|
|
|
|
}
|
2023-09-19 20:46:48 +08:00
|
|
|
}
|