drawDB/src/components/ControlPanel.jsx

2471 lines
76 KiB
React
Raw Normal View History

2023-12-16 11:39:13 +08:00
import { useContext, 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";
import { Link } from "react-router-dom";
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:49:31 +08:00
jsonDiagramIsValid,
2023-09-19 20:49:28 +08:00
enterFullscreen,
exitFullscreen,
2023-09-19 20:49:31 +08:00
ddbDiagramIsValid,
2023-09-19 20:49:39 +08:00
dataURItoBlob,
2023-09-19 20: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,
2023-09-19 20:49:28 +08:00
} from "../utils";
2023-09-19 20:49:20 +08:00
import {
AreaContext,
LayoutContext,
NoteContext,
2023-09-19 20:50:28 +08:00
SelectContext,
2023-09-19 20:49:20 +08:00
SettingsContext,
2023-09-19 20:50:28 +08:00
TabContext,
2023-09-19 20:49:20 +08:00
TableContext,
2023-09-19 20:51:28 +08:00
TypeContext,
2023-09-19 20:49:41 +08:00
UndoRedoContext,
} from "../pages/Editor";
import { IconAddTable, IconAddArea, IconAddNote } from "./CustomIcons";
import { ObjectType, Action, Tab, State, Cardinality } from "../data/data";
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";
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";
2023-09-19 20:46:48 +08:00
export default function ControlPanel({
diagramId,
setDiagramId,
title,
setTitle,
2023-11-25 00:28:39 +08:00
setState,
lastSaved,
}) {
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,
2023-09-19 20:49:20 +08:00
filename: `diagram_${new Date().toISOString()}`,
extension: "",
});
2023-09-19 20:49:29 +08:00
const [error, setError] = useState({
2023-09-19 20:49:57 +08:00
type: STATUS.NONE,
2023-09-19 20:49:29 +08:00
message: "",
});
const [data, setData] = useState(null);
2024-02-20 15:03:08 +08:00
const { layout, setLayout, state } = useContext(LayoutContext);
2023-09-19 20:49:36 +08:00
const { settings, setSettings } = useContext(SettingsContext);
2023-09-19 20:49:57 +08:00
const {
relationships,
tables,
setTables,
addTable,
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,
} = useContext(TableContext);
2023-09-19 20:51:28 +08:00
const { types, addType, deleteType, updateType, setTypes } =
useContext(TypeContext);
2023-09-19 20:50:15 +08:00
const { notes, setNotes, updateNote, addNote, deleteNote } =
2023-09-19 20:49:57 +08:00
useContext(NoteContext);
2023-09-19 20:50:15 +08:00
const { areas, setAreas, updateArea, addArea, deleteArea } =
2023-09-19 20:49:57 +08:00
useContext(AreaContext);
2024-01-14 08:21:44 +08:00
const { undoStack, redoStack, setUndoStack, setRedoStack } =
2023-09-19 20:49:41 +08:00
useContext(UndoRedoContext);
2023-09-19 20:50:28 +08:00
const { selectedElement, setSelectedElement } = useContext(SelectContext);
const { tab, setTab } = useContext(TabContext);
2023-09-19 20:48:26 +08:00
2023-09-19 20:49:46 +08:00
const invertLayout = (component) =>
setLayout((prev) => ({ ...prev, [component]: !prev[component] }));
const diagramIsEmpty = () => {
return (
tables.length === 0 &&
relationships.length === 0 &&
notes.length === 0 &&
areas.length === 0
);
};
const overwriteDiagram = () => {
setTables(data.tables);
setRelationships(data.relationships);
setAreas(data.subjectAreas);
setNotes(data.notes);
};
const undo = () => {
if (undoStack.length === 0) return;
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) => {
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,
};
}
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) {
setSettings((prev) => ({
...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) {
setSettings((prev) => ({
...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 = () =>
setSettings((prev) => ({ ...prev, zoom: prev.zoom * 1.2 }));
const zoomOut = () =>
setSettings((prev) => ({ ...prev, zoom: prev.zoom / 1.2 }));
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 = () =>
setSettings((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();
const scaleX = canvas.width / diagram.width;
const scaleY = canvas.height / diagram.height;
const scale = Math.min(scaleX, scaleY);
const translateX = canvas.width / 2;
const translateY = canvas.height / 2;
setSettings((prev) => ({
...prev,
zoom: scale,
pan: { x: translateX, y: translateY },
}));
};
2023-09-19 20:50:28 +08:00
const edit = () => {
if (selectedElement.element === ObjectType.TABLE) {
if (!layout.sidebar) {
setSelectedElement({
element: ObjectType.TABLE,
id: selectedElement.id,
openDialogue: true,
openCollapse: false,
});
} else {
setTab(Tab.tables);
setSelectedElement({
element: ObjectType.TABLE,
id: selectedElement.id,
openDialogue: false,
openCollapse: true,
});
if (tab !== Tab.tables) return;
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) {
setTab(Tab.subject_areas);
if (tab !== Tab.subject_areas) return;
document
.getElementById(`scroll_area_${selectedElement.id}`)
.scrollIntoView({ behavior: "smooth" });
} else {
setSelectedElement({
element: ObjectType.AREA,
id: selectedElement.id,
openDialogue: true,
openCollapse: false,
});
}
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) {
setTab(Tab.notes);
if (tab !== Tab.notes) return;
document
.getElementById(`scroll_note_${selectedElement.id}`)
.scrollIntoView({ behavior: "smooth" });
} else {
setSelectedElement({
element: ObjectType.NOTE,
id: selectedElement.id,
openDialogue: true,
openCollapse: false,
});
}
}
};
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();
};
2023-11-25 00:28:39 +08:00
const save = () => setState(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);
setTypes(diagram.types);
2023-10-28 01:18:28 +08:00
setRelationships(diagram.references);
setAreas(diagram.areas);
setNotes(diagram.notes);
2023-10-28 07:11:31 +08:00
setUndoStack([]);
setRedoStack([]);
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": {
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,
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",
2023-11-02 22:45:11 +08:00
filename: 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-02-13 03:05:21 +08:00
function: () => {},
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 = [];
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;
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);
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
}
} 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"
) {
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;
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-02-13 03:05:21 +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) => {
if (t.name === startTable) {
startTableId = t.id;
return;
}
if (t.name === endTable) {
endTableId = t.id;
}
2024-02-13 03:05:21 +08:00
});
if (startTableId === -1 || endTableId === -1) return;
2024-02-13 03:05:21 +08:00
tables[startTableId].fields.forEach((f) => {
if (f.name === startField) {
startFieldId = f.id;
return;
}
if (f.name === endField) {
endFieldId = f.id;
}
2024-02-13 03:05:21 +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-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
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-21 15:05:43 +08:00
console.log(tables);
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) {
setSettings((prev) => ({ ...prev, pan: { x: 0, y: 0 } }));
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:
db.diagrams.add({
name: saveAsTitle,
lastModified: new Date(),
tables: tables,
references: relationships,
types: types,
notes: notes,
areas: areas,
});
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}
></Upload>
{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"
></Select>
<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>}
/>
) : (
<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">
<i className="bi bi-file-earmark-text text-[16px] me-1 opacity-60"></i>
{d.name}
</td>
<td className="py-1">
{d.lastModified.toLocaleDateString() +
" " +
d.lastModified.toLocaleTimeString()}
</td>
<td className="py-1">{sizeStr}</td>
</tr>
);
})}
</tbody>
</table>
</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: "",
filename: `diagram_${new Date().toISOString()}`,
}));
2023-09-19 20:49:29 +08:00
setError({
2023-09-19 20:49:57 +08:00
type: STATUS.NONE,
2023-09-19 20:49:29 +08:00
message: "",
});
setData(null);
2023-09-19 20:48:27 +08:00
}}
2023-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">
<i className="block fa-regular fa-circle fa-xs"></i>
<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={() => {
setSettings((prev) => ({ ...prev, zoom: e }));
}}
>
{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) =>
setSettings((prev) => ({
...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">
<div className="w-[40px]">{Math.floor(settings.zoom * 100)}%</div>
<div>
<IconCaretdown />
</div>
</div>
</Dropdown>
<Tooltip content="Zoom in" position="bottom">
<button
className="py-1 px-2 hover-2 rounded text-lg"
onClick={() =>
setSettings((prev) => ({ ...prev, zoom: prev.zoom * 1.2 }))
}
>
<i className="fa-solid fa-magnifying-glass-plus"></i>
</button>
</Tooltip>
<Tooltip content="Zoom out" position="bottom">
<button
className="py-1 px-2 hover-2 rounded text-lg"
onClick={() =>
setSettings((prev) => ({ ...prev, zoom: prev.zoom / 1.2 }))
}
>
<i className="fa-solid fa-magnifying-glass-minus"></i>
</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
className="py-1 px-2 hover-2 rounded text-xl"
onClick={() => setSidesheet(SIDESHEET.TODO)}
>
2024-01-14 08:05:07 +08:00
<i className="fa-regular fa-calendar-check"></i>
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() {
switch (state) {
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={
state === State.LOADING || state === State.SAVING ? (
<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={
2023-09-19 20:48:46 +08:00
layout.header ? (
2023-09-19 20:48:34 +08:00
<IconCheckboxTick />
) : (
<div className="px-2"></div>
)
}
2023-09-19 20:49:40 +08:00
onClick={() => invertLayout("header")}
2023-09-19 20:48:34 +08:00
>
Header
</Dropdown.Item>
2023-09-19 20:49:40 +08:00
<Dropdown.Item
icon={
layout.sidebar ? (
<IconCheckboxTick />
) : (
<div className="px-2"></div>
)
2023-09-19 20:48:34 +08:00
}
2023-09-19 20:49:40 +08:00
onClick={() => invertLayout("sidebar")}
2023-09-19 20:48:34 +08:00
>
2023-09-19 20:49:40 +08:00
Sidebar
</Dropdown.Item>
2023-09-19 20:48:34 +08:00
<Dropdown.Item
icon={
2023-09-19 20:49:40 +08:00
layout.issues ? (
2023-09-19 20:48:34 +08:00
<IconCheckboxTick />
) : (
<div className="px-2"></div>
)
}
2023-09-19 20:49:40 +08:00
onClick={() => invertLayout("issues")}
>
Issues
</Dropdown.Item>
2023-09-19 20:48:34 +08:00
<Dropdown.Divider />
2023-09-19 20:48:35 +08:00
<Dropdown.Item
icon={
2023-09-19 20:48:46 +08:00
layout.fullscreen ? (
2023-09-19 20:48:35 +08:00
<IconCheckboxTick />
) : (
<div className="px-2"></div>
)
}
onClick={() => {
2023-09-19 20:48:46 +08:00
if (layout.fullscreen) {
2023-09-19 20:48:35 +08:00
exitFullscreen();
} else {
enterFullscreen();
}
2023-09-19 20:49:40 +08:00
invertLayout("fullscreen");
2023-09-19 20:48:35 +08:00
}}
>
2023-09-19 20:48:34 +08:00
Fullscreen
</Dropdown.Item>
</Dropdown.Menu>
}
trigger="click"
>
2023-09-19 20: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
}