517 lines
14 KiB
JavaScript
517 lines
14 KiB
JavaScript
import {
|
|
useState,
|
|
useEffect,
|
|
useCallback,
|
|
createContext,
|
|
useMemo,
|
|
} from "react";
|
|
import ControlPanel from "./EditorHeader/ControlPanel";
|
|
import Canvas from "./EditorCanvas/Canvas";
|
|
import { CanvasContextProvider } from "../context/CanvasContext";
|
|
import SidePanel from "./EditorSidePanel/SidePanel";
|
|
import { DB, State } from "../data/constants";
|
|
import { db } from "../data/db";
|
|
import {
|
|
useLayout,
|
|
useSettings,
|
|
useTransform,
|
|
useDiagram,
|
|
useUndoRedo,
|
|
useAreas,
|
|
useNotes,
|
|
useTypes,
|
|
useTasks,
|
|
useSaveState,
|
|
useEnums,
|
|
} from "../hooks";
|
|
import FloatingControls from "./FloatingControls";
|
|
import { Modal } from "@douyinfe/semi-ui";
|
|
import { useTranslation } from "react-i18next";
|
|
import { databases } from "../data/databases";
|
|
import { isRtl } from "../i18n/utils/rtl";
|
|
import { useSearchParams } from "react-router-dom";
|
|
import { Octokit } from "octokit";
|
|
|
|
export const IdContext = createContext({ gistId: "" });
|
|
|
|
export default function WorkSpace() {
|
|
const [id, setId] = useState(0);
|
|
const [gistId, setGistId] = useState("");
|
|
const [loadedFromGistId, setLoadedFromGistId] = useState("");
|
|
const [title, setTitle] = useState("Untitled Diagram");
|
|
const [resize, setResize] = useState(false);
|
|
const [width, setWidth] = useState(340);
|
|
const [lastSaved, setLastSaved] = useState("");
|
|
const [showSelectDbModal, setShowSelectDbModal] = useState(false);
|
|
const [selectedDb, setSelectedDb] = useState("");
|
|
const { layout } = useLayout();
|
|
const { settings } = useSettings();
|
|
const { types, setTypes } = useTypes();
|
|
const { areas, setAreas } = useAreas();
|
|
const { tasks, setTasks } = useTasks();
|
|
const { notes, setNotes } = useNotes();
|
|
const { saveState, setSaveState } = useSaveState();
|
|
const { transform, setTransform } = useTransform();
|
|
const { enums, setEnums } = useEnums();
|
|
const {
|
|
tables,
|
|
relationships,
|
|
setTables,
|
|
setRelationships,
|
|
database,
|
|
setDatabase,
|
|
} = useDiagram();
|
|
const { undoStack, redoStack, setUndoStack, setRedoStack } = useUndoRedo();
|
|
const { t, i18n } = useTranslation();
|
|
let [searchParams] = useSearchParams();
|
|
const userToken = localStorage.getItem("github_token");
|
|
const octokit = useMemo(() => {
|
|
return new Octokit({
|
|
auth: userToken ?? import.meta.env.VITE_GITHUB_ACCESS_TOKEN,
|
|
});
|
|
}, [userToken]);
|
|
const handleResize = (e) => {
|
|
if (!resize) return;
|
|
const w = isRtl(i18n.language) ? window.innerWidth - e.clientX : e.clientX;
|
|
if (w > 340) setWidth(w);
|
|
};
|
|
|
|
const save = useCallback(async () => {
|
|
const name = window.name.split(" ");
|
|
const op = name[0];
|
|
const saveAsDiagram = window.name === "" || op === "d" || op === "lt";
|
|
|
|
if (saveAsDiagram) {
|
|
if (
|
|
(id === 0 && window.name === "") ||
|
|
window.name.split(" ")[0] === "lt"
|
|
) {
|
|
await db.diagrams
|
|
.add({
|
|
database: database,
|
|
name: title,
|
|
gistId: gistId ?? "",
|
|
lastModified: new Date(),
|
|
tables: tables,
|
|
references: relationships,
|
|
notes: notes,
|
|
areas: areas,
|
|
todos: tasks,
|
|
pan: transform.pan,
|
|
zoom: transform.zoom,
|
|
loadedFromGistId: loadedFromGistId,
|
|
...(databases[database].hasEnums && { enums: enums }),
|
|
...(databases[database].hasTypes && { types: types }),
|
|
})
|
|
.then((id) => {
|
|
setId(id);
|
|
window.name = `d ${id}`;
|
|
setSaveState(State.SAVED);
|
|
setLastSaved(new Date().toLocaleString());
|
|
});
|
|
} else {
|
|
await db.diagrams
|
|
.update(id, {
|
|
database: database,
|
|
name: title,
|
|
lastModified: new Date(),
|
|
tables: tables,
|
|
references: relationships,
|
|
notes: notes,
|
|
areas: areas,
|
|
todos: tasks,
|
|
gistId: gistId ?? "",
|
|
pan: transform.pan,
|
|
zoom: transform.zoom,
|
|
loadedFromGistId: loadedFromGistId,
|
|
...(databases[database].hasEnums && { enums: enums }),
|
|
...(databases[database].hasTypes && { types: types }),
|
|
})
|
|
.then(() => {
|
|
setSaveState(State.SAVED);
|
|
setLastSaved(new Date().toLocaleString());
|
|
});
|
|
}
|
|
} else {
|
|
await db.templates
|
|
.update(id, {
|
|
database: database,
|
|
title: title,
|
|
tables: tables,
|
|
relationships: relationships,
|
|
notes: notes,
|
|
subjectAreas: areas,
|
|
todos: tasks,
|
|
pan: transform.pan,
|
|
zoom: transform.zoom,
|
|
...(databases[database].hasEnums && { enums: enums }),
|
|
...(databases[database].hasTypes && { types: types }),
|
|
})
|
|
.then(() => {
|
|
setSaveState(State.SAVED);
|
|
setLastSaved(new Date().toLocaleString());
|
|
})
|
|
.catch(() => {
|
|
setSaveState(State.ERROR);
|
|
});
|
|
}
|
|
}, [
|
|
tables,
|
|
relationships,
|
|
notes,
|
|
areas,
|
|
types,
|
|
title,
|
|
id,
|
|
tasks,
|
|
transform,
|
|
setSaveState,
|
|
database,
|
|
enums,
|
|
gistId,
|
|
loadedFromGistId,
|
|
]);
|
|
|
|
const load = useCallback(async () => {
|
|
const loadLatestDiagram = async () => {
|
|
await db.diagrams
|
|
.orderBy("lastModified")
|
|
.last()
|
|
.then((d) => {
|
|
if (d) {
|
|
if (d.database) {
|
|
setDatabase(d.database);
|
|
} else {
|
|
setDatabase(DB.GENERIC);
|
|
}
|
|
setId(d.id);
|
|
setGistId(d.gistId);
|
|
setLoadedFromGistId(d.loadedFromGistId);
|
|
setTitle(d.name);
|
|
setTables(d.tables);
|
|
setRelationships(d.references);
|
|
setNotes(d.notes);
|
|
setAreas(d.areas);
|
|
setTasks(d.todos ?? []);
|
|
setTransform({ pan: d.pan, zoom: d.zoom });
|
|
if (databases[database].hasTypes) {
|
|
setTypes(d.types ?? []);
|
|
}
|
|
if (databases[database].hasEnums) {
|
|
setEnums(d.enums ?? []);
|
|
}
|
|
window.name = `d ${d.id}`;
|
|
} else {
|
|
window.name = "";
|
|
if (selectedDb === "") setShowSelectDbModal(true);
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.log(error);
|
|
});
|
|
};
|
|
|
|
const loadDiagram = async (id) => {
|
|
await db.diagrams
|
|
.get(id)
|
|
.then((diagram) => {
|
|
if (diagram) {
|
|
if (diagram.database) {
|
|
setDatabase(diagram.database);
|
|
} else {
|
|
setDatabase(DB.GENERIC);
|
|
}
|
|
setId(diagram.id);
|
|
setGistId(diagram.gistId);
|
|
setLoadedFromGistId(diagram.loadedFromGistId);
|
|
setTitle(diagram.name);
|
|
setTables(diagram.tables);
|
|
setRelationships(diagram.references);
|
|
setAreas(diagram.areas);
|
|
setNotes(diagram.notes);
|
|
setTasks(diagram.todos ?? []);
|
|
setTransform({
|
|
pan: diagram.pan,
|
|
zoom: diagram.zoom,
|
|
});
|
|
setUndoStack([]);
|
|
setRedoStack([]);
|
|
if (databases[database].hasTypes) {
|
|
setTypes(diagram.types ?? []);
|
|
}
|
|
if (databases[database].hasEnums) {
|
|
setEnums(diagram.enums ?? []);
|
|
}
|
|
window.name = `d ${diagram.id}`;
|
|
} else {
|
|
window.name = "";
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.log(error);
|
|
});
|
|
};
|
|
|
|
const loadTemplate = async (id) => {
|
|
await db.templates
|
|
.get(id)
|
|
.then((diagram) => {
|
|
if (diagram) {
|
|
if (diagram.database) {
|
|
setDatabase(diagram.database);
|
|
} else {
|
|
setDatabase(DB.GENERIC);
|
|
}
|
|
setId(diagram.id);
|
|
setTitle(diagram.title);
|
|
setTables(diagram.tables);
|
|
setRelationships(diagram.relationships);
|
|
setAreas(diagram.subjectAreas);
|
|
setTasks(diagram.todos ?? []);
|
|
setNotes(diagram.notes);
|
|
setTransform({
|
|
zoom: 1,
|
|
pan: { x: 0, y: 0 },
|
|
});
|
|
setUndoStack([]);
|
|
setRedoStack([]);
|
|
if (databases[database].hasTypes) {
|
|
setTypes(diagram.types ?? []);
|
|
}
|
|
if (databases[database].hasEnums) {
|
|
setEnums(diagram.enums ?? []);
|
|
}
|
|
} else {
|
|
if (selectedDb === "") setShowSelectDbModal(true);
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.log(error);
|
|
if (selectedDb === "") setShowSelectDbModal(true);
|
|
});
|
|
};
|
|
|
|
if (window.name === "") {
|
|
loadLatestDiagram();
|
|
} else {
|
|
const name = window.name.split(" ");
|
|
const op = name[0];
|
|
const id = parseInt(name[1]);
|
|
switch (op) {
|
|
case "d": {
|
|
loadDiagram(id);
|
|
break;
|
|
}
|
|
case "t":
|
|
case "lt": {
|
|
loadTemplate(id);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}, [
|
|
setTransform,
|
|
setRedoStack,
|
|
setUndoStack,
|
|
setRelationships,
|
|
setTables,
|
|
setAreas,
|
|
setNotes,
|
|
setTypes,
|
|
setTasks,
|
|
setDatabase,
|
|
database,
|
|
setEnums,
|
|
selectedDb,
|
|
]);
|
|
|
|
const loadFromGist = useCallback(
|
|
async (shareId) => {
|
|
const d = await db.diagrams.get({ loadedFromGistId: shareId });
|
|
if (d) {
|
|
window.name = "d " + d.id;
|
|
} else {
|
|
window.name = "";
|
|
}
|
|
|
|
try {
|
|
const res = await octokit.request(`GET /gists/${shareId}`, {
|
|
gist_id: shareId,
|
|
headers: {
|
|
"X-GitHub-Api-Version": "2022-11-28",
|
|
},
|
|
});
|
|
const diagramSrc = res.data.files["share.json"].content;
|
|
const d = JSON.parse(diagramSrc);
|
|
setUndoStack([]);
|
|
setRedoStack([]);
|
|
setLoadedFromGistId(shareId);
|
|
setDatabase(d.database);
|
|
setTitle(d.title);
|
|
setTables(d.tables);
|
|
setRelationships(d.relationships);
|
|
setNotes(d.notes);
|
|
setAreas(d.subjectAreas);
|
|
setTransform(d.transform);
|
|
if (databases[d.database].hasTypes) {
|
|
setTypes(d.types ?? []);
|
|
}
|
|
if (databases[d.database].hasEnums) {
|
|
setEnums(d.enums ?? []);
|
|
}
|
|
} catch (e) {
|
|
console.log(e);
|
|
}
|
|
},
|
|
[
|
|
octokit,
|
|
setAreas,
|
|
setDatabase,
|
|
setEnums,
|
|
setNotes,
|
|
setRelationships,
|
|
setTables,
|
|
setTypes,
|
|
setTransform,
|
|
setRedoStack,
|
|
setUndoStack,
|
|
],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
tables?.length === 0 &&
|
|
areas?.length === 0 &&
|
|
notes?.length === 0 &&
|
|
types?.length === 0 &&
|
|
tasks?.length === 0
|
|
)
|
|
return;
|
|
|
|
if (settings.autosave) {
|
|
setSaveState(State.SAVING);
|
|
}
|
|
}, [
|
|
undoStack,
|
|
redoStack,
|
|
settings.autosave,
|
|
tables?.length,
|
|
areas?.length,
|
|
notes?.length,
|
|
types?.length,
|
|
relationships?.length,
|
|
tasks?.length,
|
|
transform.zoom,
|
|
title,
|
|
setSaveState,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (gistId && gistId !== "") {
|
|
setSaveState(State.SAVING);
|
|
}
|
|
}, [gistId, setSaveState]);
|
|
|
|
useEffect(() => {
|
|
if (saveState !== State.SAVING) return;
|
|
|
|
save();
|
|
}, [id, gistId, saveState, save]);
|
|
|
|
useEffect(() => {
|
|
document.title = "Editor | drawDB";
|
|
|
|
const shareId = searchParams.get("shareId");
|
|
if (shareId) {
|
|
loadFromGist(shareId);
|
|
} else {
|
|
load();
|
|
}
|
|
}, [load, searchParams, loadFromGist]);
|
|
|
|
return (
|
|
<div className="h-full flex flex-col overflow-hidden theme">
|
|
<IdContext.Provider value={{ gistId, setGistId }}>
|
|
<ControlPanel
|
|
diagramId={id}
|
|
setDiagramId={setId}
|
|
title={title}
|
|
setTitle={setTitle}
|
|
lastSaved={lastSaved}
|
|
setLastSaved={setLastSaved}
|
|
/>
|
|
</IdContext.Provider>
|
|
<div
|
|
className="flex h-full overflow-y-auto"
|
|
onPointerUp={(e) => e.isPrimary && setResize(false)}
|
|
onPointerLeave={(e) => e.isPrimary && setResize(false)}
|
|
onPointerMove={(e) => e.isPrimary && handleResize(e)}
|
|
onPointerDown={(e) => {
|
|
// Required for onPointerLeave to trigger when a touch pointer leaves
|
|
// https://stackoverflow.com/a/70976017/1137077
|
|
e.target.releasePointerCapture(e.pointerId);
|
|
}}
|
|
style={isRtl(i18n.language) ? { direction: "rtl" } : {}}
|
|
>
|
|
{layout.sidebar && (
|
|
<SidePanel resize={resize} setResize={setResize} width={width} />
|
|
)}
|
|
<div className="relative w-full h-full overflow-hidden">
|
|
<CanvasContextProvider className="h-full w-full">
|
|
<Canvas saveState={saveState} setSaveState={setSaveState} />
|
|
</CanvasContextProvider>
|
|
{!(layout.sidebar || layout.toolbar || layout.header) && (
|
|
<div className="fixed right-5 bottom-4">
|
|
<FloatingControls />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Modal
|
|
centered
|
|
size="medium"
|
|
closable={false}
|
|
hasCancel={false}
|
|
title={t("pick_db")}
|
|
okText={t("confirm")}
|
|
visible={showSelectDbModal}
|
|
onOk={() => {
|
|
if (selectedDb === "") return;
|
|
setDatabase(selectedDb);
|
|
setShowSelectDbModal(false);
|
|
}}
|
|
okButtonProps={{ disabled: selectedDb === "" }}
|
|
>
|
|
<div className="grid grid-cols-3 gap-4 place-content-center">
|
|
{Object.values(databases).map((x) => (
|
|
<div
|
|
key={x.name}
|
|
onClick={() => setSelectedDb(x.label)}
|
|
className={`space-y-3 py-3 px-4 rounded-md border-2 select-none ${
|
|
settings.mode === "dark"
|
|
? "bg-zinc-700 hover:bg-zinc-600"
|
|
: "bg-zinc-100 hover:bg-zinc-200"
|
|
} ${selectedDb === x.label ? "border-zinc-400" : "border-transparent"}`}
|
|
>
|
|
<div className="font-semibold">{x.name}</div>
|
|
{x.image && (
|
|
<img
|
|
src={x.image}
|
|
className="h-10"
|
|
style={{
|
|
filter:
|
|
"opacity(0.4) drop-shadow(0 0 0 white) drop-shadow(0 0 0 white)",
|
|
}}
|
|
/>
|
|
)}
|
|
<div className="text-xs">{x.description}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|