Some mobile browsers (e.g. chrome) uses collapsing url bars (the
bar collapses when you scroll). In such cases, `100vh` typically
refers to the full height of the viewport when the url bar is
collapsed (see also `svh`, `lvh` and `dvh`, e.g. at
<https://ishadeed.com/article/new-viewport-units/#the-small-large-and-dynamic-viewport-units>
). This meant that on my tablet, the editor would extend below the
visible page until I scrolled it into view.
This commit re-uses a fix from some of my other projects
(specifically
<5c7e788d40/src/styles/globals.css (L14-L28)
>
) where the root element is set to fill 100% height. This avoids
dealing with viewport units altogether. On my tablet, this means
that the url bar is visible and that the editor does not extend
below the visible page.
413 lines
11 KiB
JavaScript
413 lines
11 KiB
JavaScript
import { useState, useEffect, useCallback } from "react";
|
|
import ControlPanel from "./EditorHeader/ControlPanel";
|
|
import Canvas from "./EditorCanvas/Canvas";
|
|
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";
|
|
|
|
export default function WorkSpace() {
|
|
const [id, setId] = useState(0);
|
|
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 } = useTranslation();
|
|
|
|
const handleResize = (e) => {
|
|
if (!resize) return;
|
|
const w = 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,
|
|
lastModified: new Date(),
|
|
tables: tables,
|
|
references: relationships,
|
|
notes: notes,
|
|
areas: areas,
|
|
todos: tasks,
|
|
pan: transform.pan,
|
|
zoom: transform.zoom,
|
|
...(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,
|
|
pan: transform.pan,
|
|
zoom: transform.zoom,
|
|
...(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,
|
|
]);
|
|
|
|
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);
|
|
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);
|
|
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,
|
|
]);
|
|
|
|
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 (saveState !== State.SAVING) return;
|
|
|
|
save();
|
|
}, [id, saveState, save]);
|
|
|
|
useEffect(() => {
|
|
document.title = "Editor | drawDB";
|
|
|
|
load();
|
|
}, [load]);
|
|
|
|
return (
|
|
<div className="h-full flex flex-col overflow-hidden theme">
|
|
<ControlPanel
|
|
diagramId={id}
|
|
setDiagramId={setId}
|
|
title={title}
|
|
setTitle={setTitle}
|
|
lastSaved={lastSaved}
|
|
setLastSaved={setLastSaved}
|
|
/>
|
|
<div
|
|
className="flex h-full overflow-y-auto"
|
|
onMouseUp={() => setResize(false)}
|
|
onMouseLeave={() => setResize(false)}
|
|
onMouseMove={handleResize}
|
|
>
|
|
{layout.sidebar && (
|
|
<SidePanel resize={resize} setResize={setResize} width={width} />
|
|
)}
|
|
<div className="relative w-full h-full overflow-hidden">
|
|
<Canvas saveState={saveState} setSaveState={setSaveState} />
|
|
{!(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>
|
|
);
|
|
}
|