Move controlpanel modal and sidesheet into folders
This commit is contained in:
parent
1576b3fb96
commit
173b02daa2
File diff suppressed because it is too large
Load Diff
71
src/components/EditorHeader/LayoutDropdown.jsx
Normal file
71
src/components/EditorHeader/LayoutDropdown.jsx
Normal file
@ -0,0 +1,71 @@
|
||||
import {
|
||||
IconCaretdown,
|
||||
IconCheckboxTick,
|
||||
IconRowsStroked,
|
||||
} from "@douyinfe/semi-icons";
|
||||
import { Dropdown } from "@douyinfe/semi-ui";
|
||||
import { useLayout } from "../../hooks";
|
||||
import { enterFullscreen, exitFullscreen } from "../../utils/fullscreen";
|
||||
|
||||
export default function LayoutDropdown() {
|
||||
const { layout, setLayout } = useLayout();
|
||||
const invertLayout = (component) =>
|
||||
setLayout((prev) => ({ ...prev, [component]: !prev[component] }));
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
position="bottomLeft"
|
||||
style={{ width: "180px" }}
|
||||
render={
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
icon={
|
||||
layout.header ? <IconCheckboxTick /> : <div className="px-2" />
|
||||
}
|
||||
onClick={() => invertLayout("header")}
|
||||
>
|
||||
Header
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
icon={
|
||||
layout.sidebar ? <IconCheckboxTick /> : <div className="px-2" />
|
||||
}
|
||||
onClick={() => invertLayout("sidebar")}
|
||||
>
|
||||
Sidebar
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
icon={
|
||||
layout.issues ? <IconCheckboxTick /> : <div className="px-2" />
|
||||
}
|
||||
onClick={() => invertLayout("issues")}
|
||||
>
|
||||
Issues
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
icon={<div className="px-2" />}
|
||||
onClick={() => {
|
||||
if (layout.fullscreen) {
|
||||
exitFullscreen();
|
||||
} else {
|
||||
enterFullscreen();
|
||||
}
|
||||
invertLayout("fullscreen");
|
||||
}}
|
||||
>
|
||||
Fullscreen
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
}
|
||||
trigger="click"
|
||||
>
|
||||
<div className="py-1 px-2 hover-2 rounded flex items-center justify-center">
|
||||
<IconRowsStroked size="extra-large" />
|
||||
<div>
|
||||
<IconCaretdown />
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
127
src/components/EditorHeader/Modal/ImportDiagram.jsx
Normal file
127
src/components/EditorHeader/Modal/ImportDiagram.jsx
Normal file
@ -0,0 +1,127 @@
|
||||
import {
|
||||
ddbDiagramIsValid,
|
||||
jsonDiagramIsValid,
|
||||
} from "../../../utils/validateSchema";
|
||||
import { Upload, Banner } from "@douyinfe/semi-ui";
|
||||
import { STATUS } from "../../../data/constants";
|
||||
import { useAreas, useNotes, useTables } from "../../../hooks";
|
||||
|
||||
export default function ImportDiagram({ setImportData, error, setError }) {
|
||||
const { areas } = useAreas();
|
||||
const { notes } = useNotes();
|
||||
const { tables, relationships } = useTables();
|
||||
|
||||
const diagramIsEmpty = () => {
|
||||
return (
|
||||
tables.length === 0 &&
|
||||
relationships.length === 0 &&
|
||||
notes.length === 0 &&
|
||||
areas.length === 0
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Upload
|
||||
action="#"
|
||||
beforeUpload={({ file, fileList }) => {
|
||||
const f = fileList[0].fileInstance;
|
||||
if (!f) {
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
let jsonObject = null;
|
||||
try {
|
||||
jsonObject = JSON.parse(e.target.result);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
setImportData(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}
|
||||
/>
|
||||
{error.type === STATUS.ERROR ? (
|
||||
<Banner
|
||||
type="danger"
|
||||
fullMode={false}
|
||||
description={<div>{error.message}</div>}
|
||||
/>
|
||||
) : 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>}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
66
src/components/EditorHeader/Modal/ImportSource.jsx
Normal file
66
src/components/EditorHeader/Modal/ImportSource.jsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { Upload, Checkbox } from "@douyinfe/semi-ui";
|
||||
import { STATUS } from "../../../data/constants";
|
||||
|
||||
export default function ImportSource({ importData, setImportData, setError }) {
|
||||
return (
|
||||
<div>
|
||||
<Upload
|
||||
action="#"
|
||||
beforeUpload={({ file, fileList }) => {
|
||||
const f = fileList[0].fileInstance;
|
||||
if (!f) {
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
setImportData((prev) => ({ ...prev, src: e.target.result }));
|
||||
};
|
||||
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"
|
||||
onRemove={() => {
|
||||
setError({
|
||||
type: STATUS.NONE,
|
||||
message: "",
|
||||
});
|
||||
setImportData((prev) => ({ ...prev, src: "" }));
|
||||
}}
|
||||
onFileChange={() =>
|
||||
setError({
|
||||
type: STATUS.NONE,
|
||||
message: "",
|
||||
})
|
||||
}
|
||||
limit={1}
|
||||
/>
|
||||
<div>
|
||||
<div className="text-xs mb-3 mt-1 opacity-80">
|
||||
* For the time being loading only MySQL scripts is supported.
|
||||
</div>
|
||||
<Checkbox
|
||||
aria-label="overwrite checkbox"
|
||||
checked={importData.overwrite}
|
||||
defaultChecked
|
||||
onChange={(e) =>
|
||||
setImportData((prev) => ({
|
||||
...prev,
|
||||
overwrite: e.target.checked,
|
||||
}))
|
||||
}
|
||||
>
|
||||
Overwrite existing diagram
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
312
src/components/EditorHeader/Modal/Modal.jsx
Normal file
312
src/components/EditorHeader/Modal/Modal.jsx
Normal file
@ -0,0 +1,312 @@
|
||||
import {
|
||||
Spin,
|
||||
Input,
|
||||
Image,
|
||||
Toast,
|
||||
Modal as SemiUIModal,
|
||||
} from "@douyinfe/semi-ui";
|
||||
import { MODAL, STATUS } from "../../../data/constants";
|
||||
import { useState } from "react";
|
||||
import { db } from "../../../data/db";
|
||||
import {
|
||||
useAreas,
|
||||
useNotes,
|
||||
useSettings,
|
||||
useTables,
|
||||
useTransform,
|
||||
useTypes,
|
||||
useUndoRedo,
|
||||
} from "../../../hooks";
|
||||
import { saveAs } from "file-saver";
|
||||
import { Parser } from "node-sql-parser";
|
||||
import { astToDiagram } from "../../../utils/astToDiagram";
|
||||
import { getModalTitle, getOkText } from "../../../utils/modalTitles";
|
||||
import Rename from "./Rename";
|
||||
import Open from "./Open";
|
||||
import New from "./New";
|
||||
import ImportDiagram from "./ImportDiagram";
|
||||
import ImportSource from "./ImportSource";
|
||||
import Editor from "@monaco-editor/react";
|
||||
|
||||
export default function Modal({
|
||||
modal,
|
||||
setModal,
|
||||
title,
|
||||
setTitle,
|
||||
prevTitle,
|
||||
setPrevTitle,
|
||||
setDiagramId,
|
||||
exportData,
|
||||
setExportData,
|
||||
}) {
|
||||
const { setTables, setRelationships } = useTables();
|
||||
const { setNotes } = useNotes();
|
||||
const { setAreas } = useAreas();
|
||||
const { setTypes } = useTypes();
|
||||
const { settings } = useSettings();
|
||||
const { setTransform } = useTransform();
|
||||
const { setUndoStack, setRedoStack } = useUndoRedo();
|
||||
const [importSource, setImportSource] = useState({
|
||||
src: "",
|
||||
overwrite: true,
|
||||
dbms: "MySQL",
|
||||
});
|
||||
const [importData, setImportData] = useState(null);
|
||||
const [error, setError] = useState({
|
||||
type: STATUS.NONE,
|
||||
message: "",
|
||||
});
|
||||
const [selectedTemplateId, setSelectedTemplateId] = useState(-1);
|
||||
const [selectedDiagramId, setSelectedDiagramId] = useState(0);
|
||||
const [saveAsTitle, setSaveAsTitle] = useState(title);
|
||||
|
||||
const overwriteDiagram = () => {
|
||||
setTables(importData.tables);
|
||||
setRelationships(importData.relationships);
|
||||
setAreas(importData.subjectAreas);
|
||||
setNotes(importData.notes);
|
||||
if (importData.title) {
|
||||
setTitle(importData.title);
|
||||
}
|
||||
};
|
||||
|
||||
const loadDiagram = async (id) => {
|
||||
await db.diagrams
|
||||
.get(id)
|
||||
.then((diagram) => {
|
||||
if (diagram) {
|
||||
setDiagramId(diagram.id);
|
||||
setTitle(diagram.name);
|
||||
setTables(diagram.tables);
|
||||
setTypes(diagram.types);
|
||||
setRelationships(diagram.references);
|
||||
setAreas(diagram.areas);
|
||||
setNotes(diagram.notes);
|
||||
setTransform({
|
||||
pan: diagram.pan,
|
||||
zoom: diagram.zoom,
|
||||
});
|
||||
setUndoStack([]);
|
||||
setRedoStack([]);
|
||||
window.name = `d ${diagram.id}`;
|
||||
} else {
|
||||
Toast.error("Oops! Something went wrong.");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
Toast.error("Oops! Couldn't load diagram.");
|
||||
});
|
||||
};
|
||||
|
||||
const parseSQLAndLoadDiagram = () => {
|
||||
const parser = new Parser();
|
||||
let ast = null;
|
||||
try {
|
||||
ast = parser.astify(importData.src, { database: "MySQL" });
|
||||
} catch (err) {
|
||||
Toast.error(
|
||||
"Could not parse the sql file. Make sure there are no syntax errors."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const d = astToDiagram(ast);
|
||||
if (importData.overwrite) {
|
||||
setTables(d.tables);
|
||||
setRelationships(d.relationships);
|
||||
setNotes([]);
|
||||
setAreas([]);
|
||||
setTypes([]);
|
||||
setUndoStack([]);
|
||||
setRedoStack([]);
|
||||
} else {
|
||||
setTables((prev) => [...prev, ...d.tables]);
|
||||
setRelationships((prev) => [...prev, ...d.relationships]);
|
||||
}
|
||||
};
|
||||
|
||||
const createNewDiagram = (id) => {
|
||||
const newWindow = window.open("/editor");
|
||||
newWindow.name = "lt " + id;
|
||||
};
|
||||
|
||||
const getModalOnOk = async () => {
|
||||
switch (modal) {
|
||||
case MODAL.IMG:
|
||||
saveAs(
|
||||
exportData.data,
|
||||
`${exportData.filename}.${exportData.extension}`
|
||||
);
|
||||
return;
|
||||
case MODAL.CODE: {
|
||||
const blob = new Blob([exportData.data], {
|
||||
type: "application/json",
|
||||
});
|
||||
saveAs(blob, `${exportData.filename}.${exportData.extension}`);
|
||||
return;
|
||||
}
|
||||
case MODAL.IMPORT:
|
||||
if (error.type !== STATUS.ERROR) {
|
||||
setTransform((prev) => ({ ...prev, pan: { x: 0, y: 0 } }));
|
||||
overwriteDiagram();
|
||||
setImportData(null);
|
||||
setModal(MODAL.NONE);
|
||||
setUndoStack([]);
|
||||
setRedoStack([]);
|
||||
}
|
||||
return;
|
||||
case MODAL.IMPORT_SRC:
|
||||
parseSQLAndLoadDiagram();
|
||||
setModal(MODAL.NONE);
|
||||
return;
|
||||
case MODAL.OPEN:
|
||||
if (selectedDiagramId === 0) return;
|
||||
loadDiagram(selectedDiagramId);
|
||||
setModal(MODAL.NONE);
|
||||
return;
|
||||
case MODAL.RENAME:
|
||||
setPrevTitle(title);
|
||||
setModal(MODAL.NONE);
|
||||
return;
|
||||
case MODAL.SAVEAS:
|
||||
setTitle(saveAsTitle);
|
||||
setModal(MODAL.NONE);
|
||||
return;
|
||||
case MODAL.NEW:
|
||||
setModal(MODAL.NONE);
|
||||
createNewDiagram(selectedTemplateId);
|
||||
return;
|
||||
default:
|
||||
setModal(MODAL.NONE);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const getModalBody = () => {
|
||||
switch (modal) {
|
||||
case MODAL.IMPORT:
|
||||
return (
|
||||
<ImportDiagram
|
||||
setImportData={setImportData}
|
||||
error={error}
|
||||
setError={setError}
|
||||
/>
|
||||
);
|
||||
case MODAL.IMPORT_SRC:
|
||||
return (
|
||||
<ImportSource
|
||||
importData={importSource}
|
||||
setImportData={setImportSource}
|
||||
setError={setError}
|
||||
/>
|
||||
);
|
||||
case MODAL.NEW:
|
||||
return (
|
||||
<New
|
||||
selectedTemplateId={selectedTemplateId}
|
||||
setSelectedTemplateId={setSelectedTemplateId}
|
||||
/>
|
||||
);
|
||||
case MODAL.RENAME:
|
||||
return <Rename title={title} setTitle={setTitle} />;
|
||||
case MODAL.OPEN:
|
||||
return (
|
||||
<Open
|
||||
selectedDiagramId={selectedDiagramId}
|
||||
setSelectedDiagramId={setSelectedDiagramId}
|
||||
/>
|
||||
);
|
||||
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 (
|
||||
<>
|
||||
{modal === 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 <></>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SemiUIModal
|
||||
title={getModalTitle(modal)}
|
||||
visible={modal !== MODAL.NONE}
|
||||
onOk={getModalOnOk}
|
||||
afterClose={() => {
|
||||
setExportData(() => ({
|
||||
data: "",
|
||||
extension: "",
|
||||
filename: `${title}_${new Date().toISOString()}`,
|
||||
}));
|
||||
setError({
|
||||
type: STATUS.NONE,
|
||||
message: "",
|
||||
});
|
||||
setImportData(null);
|
||||
setImportSource({
|
||||
src: "",
|
||||
overwrite: true,
|
||||
dbms: "MySQL",
|
||||
});
|
||||
}}
|
||||
onCancel={() => {
|
||||
if (modal === MODAL.RENAME) setTitle(prevTitle);
|
||||
setModal(MODAL.NONE);
|
||||
}}
|
||||
centered
|
||||
closeOnEsc={true}
|
||||
okText={getOkText(modal)}
|
||||
okButtonProps={{
|
||||
disabled:
|
||||
(error && error?.type === STATUS.ERROR) ||
|
||||
(modal === MODAL.IMPORT &&
|
||||
(error.type === STATUS.ERROR || !importData)) ||
|
||||
(modal === MODAL.RENAME && title === "") ||
|
||||
((modal === MODAL.IMG || modal === MODAL.CODE) && !exportData.data) ||
|
||||
(modal === MODAL.SAVEAS && saveAsTitle === "") ||
|
||||
(modal === MODAL.IMPORT_SRC && importSource.src === ""),
|
||||
}}
|
||||
cancelText="Cancel"
|
||||
width={modal === MODAL.NEW ? 740 : 600}
|
||||
>
|
||||
{getModalBody()}
|
||||
</SemiUIModal>
|
||||
);
|
||||
}
|
43
src/components/EditorHeader/Modal/New.jsx
Normal file
43
src/components/EditorHeader/Modal/New.jsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { db } from "../../../data/db";
|
||||
import { useSettings } from "../../../hooks";
|
||||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
import Thumbnail from "../../Thumbnail";
|
||||
|
||||
export default function New({ selectedTemplateId, setSelectedTemplateId }) {
|
||||
const { settings } = useSettings();
|
||||
const templates = useLiveQuery(() => db.templates.toArray());
|
||||
|
||||
return (
|
||||
<div className="h-[360px] grid grid-cols-3 gap-2 overflow-auto px-1">
|
||||
<div onClick={() => setSelectedTemplateId(0)}>
|
||||
<div
|
||||
className={`rounded-md h-[180px] border-2 hover:border-dashed ${
|
||||
selectedTemplateId === 0 ? "border-blue-400" : "border-zinc-400"
|
||||
}`}
|
||||
>
|
||||
<Thumbnail i={0} diagram={{}} zoom={0.24} theme={settings.mode} />
|
||||
</div>
|
||||
<div className="text-center mt-1">Blank</div>
|
||||
</div>
|
||||
{templates?.map((temp, i) => (
|
||||
<div key={i} onClick={() => setSelectedTemplateId(temp.id)}>
|
||||
<div
|
||||
className={`rounded-md h-[180px] border-2 hover:border-dashed ${
|
||||
selectedTemplateId === temp.id
|
||||
? "border-blue-400"
|
||||
: "border-zinc-400"
|
||||
}`}
|
||||
>
|
||||
<Thumbnail
|
||||
i={temp.id}
|
||||
diagram={temp}
|
||||
zoom={0.24}
|
||||
theme={settings.mode}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center mt-1">{temp.title}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
73
src/components/EditorHeader/Modal/Open.jsx
Normal file
73
src/components/EditorHeader/Modal/Open.jsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { db } from "../../../data/db";
|
||||
import { Banner } from "@douyinfe/semi-ui";
|
||||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
|
||||
export default function Open({ selectedDiagramId, setSelectedDiagramId }) {
|
||||
const diagrams = useLiveQuery(() => db.diagrams.toArray());
|
||||
|
||||
const getDiagramSize = (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 sizeStr;
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
{diagrams?.length === 0 ? (
|
||||
<Banner
|
||||
fullMode={false}
|
||||
type="info"
|
||||
bordered
|
||||
icon={null}
|
||||
closeIcon={null}
|
||||
description={<div>You have no saved diagrams.</div>}
|
||||
/>
|
||||
) : (
|
||||
<div className="max-h-[360px]">
|
||||
<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) => {
|
||||
return (
|
||||
<tr
|
||||
key={d.id}
|
||||
className={`${
|
||||
selectedDiagramId === d.id
|
||||
? "bg-blue-300 bg-opacity-30"
|
||||
: "hover-1"
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedDiagramId(d.id);
|
||||
}}
|
||||
>
|
||||
<td className="py-1">
|
||||
<i className="bi bi-file-earmark-text text-[16px] me-1 opacity-60" />
|
||||
{d.name}
|
||||
</td>
|
||||
<td className="py-1">
|
||||
{d.lastModified.toLocaleDateString() +
|
||||
" " +
|
||||
d.lastModified.toLocaleTimeString()}
|
||||
</td>
|
||||
<td className="py-1">{getDiagramSize(d)}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
11
src/components/EditorHeader/Modal/Rename.jsx
Normal file
11
src/components/EditorHeader/Modal/Rename.jsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { Input } from "@douyinfe/semi-ui";
|
||||
|
||||
export default function Rename({ title, setTitle }) {
|
||||
return (
|
||||
<Input
|
||||
placeholder="Diagram name"
|
||||
value={title}
|
||||
onChange={(v) => setTitle(v)}
|
||||
/>
|
||||
);
|
||||
}
|
61
src/components/EditorHeader/SideSheet/Sidesheet.jsx
Normal file
61
src/components/EditorHeader/SideSheet/Sidesheet.jsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { SideSheet as SemiUISideSheet } from "@douyinfe/semi-ui";
|
||||
import { SIDESHEET } from "../../../data/constants";
|
||||
import { useSettings } from "../../../hooks";
|
||||
import timeLine from "../../../assets/process.png";
|
||||
import timeLineDark from "../../../assets/process_dark.png";
|
||||
import todo from "../../../assets/calendar.png";
|
||||
import Timeline from "./Timeline";
|
||||
import Todo from "./Todo";
|
||||
|
||||
export default function Sidesheet({ type, onClose }) {
|
||||
const { settings } = useSettings();
|
||||
|
||||
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 <Timeline />;
|
||||
case SIDESHEET.TODO:
|
||||
return <Todo />;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SemiUISideSheet
|
||||
visible={type !== SIDESHEET.NONE}
|
||||
onCancel={onClose}
|
||||
width={340}
|
||||
title={getTitle(type)}
|
||||
style={{ paddingBottom: "16px" }}
|
||||
bodyStyle={{ padding: "0px" }}
|
||||
>
|
||||
{getContent(type)}
|
||||
</SemiUISideSheet>
|
||||
);
|
||||
}
|
32
src/components/EditorHeader/SideSheet/Timeline.jsx
Normal file
32
src/components/EditorHeader/SideSheet/Timeline.jsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { useUndoRedo } from "../../../hooks";
|
||||
import { List } from "@douyinfe/semi-ui";
|
||||
|
||||
export default function Timeline() {
|
||||
const { undoStack } = useUndoRedo();
|
||||
|
||||
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" />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
@ -19,8 +19,8 @@ import {
|
||||
IconDeleteStroked,
|
||||
IconCaretdown,
|
||||
} from "@douyinfe/semi-icons";
|
||||
import { State } from "../../data/constants";
|
||||
import { useTasks, useSaveState } from "../../hooks";
|
||||
import { State } from "../../../data/constants";
|
||||
import { useTasks, useSaveState } from "../../../hooks";
|
||||
|
||||
const Priority = {
|
||||
NONE: 0,
|
@ -99,3 +99,28 @@ export const State = {
|
||||
LOADING: 3,
|
||||
ERROR: 4,
|
||||
};
|
||||
|
||||
export const MODAL = {
|
||||
NONE: 0,
|
||||
IMG: 1,
|
||||
CODE: 2,
|
||||
IMPORT: 3,
|
||||
RENAME: 4,
|
||||
OPEN: 5,
|
||||
SAVEAS: 6,
|
||||
NEW: 7,
|
||||
IMPORT_SRC: 8,
|
||||
};
|
||||
|
||||
export const STATUS = {
|
||||
NONE: 0,
|
||||
WARNING: 1,
|
||||
ERROR: 2,
|
||||
OK: 3,
|
||||
};
|
||||
|
||||
export const SIDESHEET = {
|
||||
NONE: 0,
|
||||
TODO: 1,
|
||||
TIMELINE: 2,
|
||||
};
|
252
src/utils/astToDiagram.js
Normal file
252
src/utils/astToDiagram.js
Normal file
@ -0,0 +1,252 @@
|
||||
import { Cardinality } from "../data/constants";
|
||||
|
||||
export function astToDiagram(ast) {
|
||||
const tables = [];
|
||||
const relationships = [];
|
||||
const inlineForeignKeys = [];
|
||||
|
||||
ast.forEach((e) => {
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
} else {
|
||||
let value = d.check.definition[0].right.value;
|
||||
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;
|
||||
}
|
||||
field.check = check;
|
||||
}
|
||||
|
||||
table.fields.push(field);
|
||||
} else if (d.resource === "constraint") {
|
||||
if (d.constraint_type === "primary key") {
|
||||
d.definition.forEach((c) => {
|
||||
table.fields.forEach((f) => {
|
||||
if (f.name === c.column && !f.primary) {
|
||||
f.primary = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (d.constraint_type === "FOREIGN KEY") {
|
||||
inlineForeignKeys.push({ ...d, startTable: e.table[0].table });
|
||||
}
|
||||
}
|
||||
});
|
||||
tables.push(table);
|
||||
tables.forEach((e, i) => {
|
||||
e.id = i;
|
||||
e.fields.forEach((f, j) => {
|
||||
f.id = j;
|
||||
});
|
||||
});
|
||||
} else if (e.keyword === "index") {
|
||||
const index = {};
|
||||
index.name = e.index;
|
||||
index.unique = false;
|
||||
if (e.index_type === "unique") index.unique = true;
|
||||
index.fields = [];
|
||||
e.index_columns.forEach((f) => index.fields.push(f.column));
|
||||
|
||||
let found = -1;
|
||||
tables.forEach((t, i) => {
|
||||
if (found !== -1) return;
|
||||
if (t.name === e.table.table) {
|
||||
t.indices.push(index);
|
||||
found = i;
|
||||
}
|
||||
});
|
||||
|
||||
if (found !== -1) tables[found].indices.forEach((i, j) => (i.id = j));
|
||||
}
|
||||
} else if (e.type === "alter") {
|
||||
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;
|
||||
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";
|
||||
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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let startTableId = -1;
|
||||
let startFieldId = -1;
|
||||
let endTableId = -1;
|
||||
let endFieldId = -1;
|
||||
|
||||
tables.forEach((t) => {
|
||||
if (t.name === startTable) {
|
||||
startTableId = t.id;
|
||||
return;
|
||||
}
|
||||
|
||||
if (t.name === endTable) {
|
||||
endTableId = t.id;
|
||||
}
|
||||
});
|
||||
|
||||
if (startTableId === -1 || endTableId === -1) return;
|
||||
|
||||
tables[startTableId].fields.forEach((f) => {
|
||||
if (f.name === startField) {
|
||||
startFieldId = f.id;
|
||||
return;
|
||||
}
|
||||
|
||||
if (f.name === endField) {
|
||||
endFieldId = f.id;
|
||||
}
|
||||
});
|
||||
|
||||
if (startFieldId === -1 || endFieldId === -1) return;
|
||||
|
||||
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;
|
||||
relationships.push(relationship);
|
||||
|
||||
relationships.forEach((r, i) => (r.id = i));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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";
|
||||
fk.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);
|
||||
}
|
||||
});
|
||||
|
||||
let startTableId = -1;
|
||||
let startFieldId = -1;
|
||||
let endTableId = -1;
|
||||
let endFieldId = -1;
|
||||
|
||||
tables.forEach((t) => {
|
||||
if (t.name === startTable) {
|
||||
startTableId = t.id;
|
||||
return;
|
||||
}
|
||||
|
||||
if (t.name === endTable) {
|
||||
endTableId = t.id;
|
||||
}
|
||||
});
|
||||
|
||||
if (startTableId === -1 || endTableId === -1) return;
|
||||
|
||||
tables[startTableId].fields.forEach((f) => {
|
||||
if (f.name === startField) {
|
||||
startFieldId = f.id;
|
||||
return;
|
||||
}
|
||||
|
||||
if (f.name === endField) {
|
||||
endFieldId = f.id;
|
||||
}
|
||||
});
|
||||
|
||||
if (startFieldId === -1 || endFieldId === -1) return;
|
||||
|
||||
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;
|
||||
relationships.push(relationship);
|
||||
});
|
||||
|
||||
relationships.forEach((r, i) => (r.id = i));
|
||||
|
||||
return { tables, relationships };
|
||||
}
|
44
src/utils/modalTitles.js
Normal file
44
src/utils/modalTitles.js
Normal file
@ -0,0 +1,44 @@
|
||||
import { MODAL } from "../data/constants";
|
||||
|
||||
export const getModalTitle = (modal) => {
|
||||
switch (modal) {
|
||||
case MODAL.IMPORT:
|
||||
case MODAL.IMPORT_SRC:
|
||||
return "Import diagram";
|
||||
case MODAL.CODE:
|
||||
return "Export source";
|
||||
case MODAL.IMG:
|
||||
return "Export image";
|
||||
case MODAL.RENAME:
|
||||
return "Rename diagram";
|
||||
case MODAL.OPEN:
|
||||
return "Open diagram";
|
||||
case MODAL.SAVEAS:
|
||||
return "Save as";
|
||||
case MODAL.NEW:
|
||||
return "Create new diagram";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
export const getOkText = (modal) => {
|
||||
switch (modal) {
|
||||
case MODAL.IMPORT:
|
||||
case MODAL.IMPORT_SRC:
|
||||
return "Import";
|
||||
case MODAL.CODE:
|
||||
case MODAL.IMG:
|
||||
return "Export";
|
||||
case MODAL.RENAME:
|
||||
return "Rename";
|
||||
case MODAL.OPEN:
|
||||
return "Open";
|
||||
case MODAL.SAVEAS:
|
||||
return "Save as";
|
||||
case MODAL.NEW:
|
||||
return "Create";
|
||||
default:
|
||||
return "Confirm";
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user