Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions src/components/EditorCanvas/Canvas.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
gridCircleRadius,
minAreaSize,
} from "../../data/constants";
import { Toast } from "@douyinfe/semi-ui";
import { Toast, Modal, Button } from "@douyinfe/semi-ui";
import Table from "./Table";
import Area from "./Area";
import Relationship from "./Relationship";
Expand All @@ -25,6 +25,7 @@ import {
useNotes,
useLayout,
useSaveState,
useFileDrop,
} from "../../hooks";
import { useTranslation } from "react-i18next";
import { useEventListener } from "usehooks-ts";
Expand Down Expand Up @@ -58,6 +59,16 @@ export default function Canvas() {
bulkSelectedElements,
setBulkSelectedElements,
} = useSelect();
const {
isDragOver,
showImportModal,
handleDragOver,
handleDragLeave,
handleDrop,
handleImportOverwrite,
handleImportAppend,
handleImportCancel,
} = useFileDrop();
const notDragging = {
id: -1,
type: ObjectType.NONE,
Expand Down Expand Up @@ -685,12 +696,22 @@ export default function Canvas() {
return (
<div className="grow h-full touch-none" id="canvas">
<div
className="w-full h-full"
className="w-full h-full relative"
style={{
cursor: pointer.style,
backgroundColor: settings.mode === "dark" ? darkBgTheme : "white",
}}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{isDragOver && (
<div className="absolute inset-0 z-50 flex items-center justify-center bg-blue-500/10 border-2 border-dashed border-blue-500 rounded-md pointer-events-none">
<div className="text-blue-600 dark:text-blue-400 font-semibold text-lg">
{t("drop_to_import") || "Drop file to import"}
</div>
</div>
)}
<svg
id="diagram"
ref={canvasRef}
Expand Down Expand Up @@ -863,6 +884,31 @@ export default function Canvas() {
</table>
</div>
)}
<Modal
centered
size="small"
title={t("import_diagram") || "Import diagram"}
visible={showImportModal}
onCancel={handleImportCancel}
footer={
<div className="flex justify-end gap-2">
<Button onClick={handleImportCancel}>
{t("cancel") || "Cancel"}
</Button>
<Button onClick={handleImportAppend}>
{t("add") || "Add"}
</Button>
<Button theme="solid" onClick={handleImportOverwrite}>
{t("overwrite") || "Overwrite"}
</Button>
</div>
}
>
<p>
{t("overwrite_or_append_description") ||
"The current diagram is not empty. Would you like to overwrite it with the imported data or add to the existing diagram?"}
</p>
</Modal>
</div>
);
}
6 changes: 3 additions & 3 deletions src/data/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,16 @@ export const noteSchema = {
type: "object",
properties: {
id: { type: "integer" },
x: { type: "number" },
y: { type: "number" },
x: { type: ["number", "null"] },
y: { type: ["number", "null"] },
title: { type: "string" },
content: { type: "string" },
color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$" },
height: { type: "number" },
width: { type: "number" },
locked: { type: "boolean" },
},
required: ["id", "x", "y", "title", "content", "color", "height"],
required: ["id"],
};

export const typeSchema = {
Expand Down
1 change: 1 addition & 0 deletions src/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export { default as useTransform } from "./useTransform";
export { default as useTypes } from "./useTypes";
export { default as useUndoRedo } from "./useUndoRedo";
export { default as useEnums } from "./useEnums";
export { default as useFileDrop } from "./useFileDrop";
export { default as useThemedPage } from "./useThemedPage";
203 changes: 203 additions & 0 deletions src/hooks/useFileDrop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { useState, useCallback, useRef } from "react";
import { Toast } from "@douyinfe/semi-ui";
import { useTranslation } from "react-i18next";
import { importFromFile, isFileSupported } from "../utils/importFrom/file";
import { databases } from "../data/databases";
import { mergeCustomTypes } from "../utils/customTypes";
import useDiagram from "./useDiagram";
import useNotes from "./useNotes";
import useAreas from "./useAreas";
import useTypes from "./useTypes";
import useEnums from "./useEnums";
import useTransform from "./useTransform";
import useUndoRedo from "./useUndoRedo";

/**
* Hook that provides drag-and-drop file import functionality for the canvas.
* Handles dragover/dragleave/drop events and processes supported file types.
* When the canvas is not empty, prompts the user to overwrite or append.
*/
export default function useFileDrop() {
const { t } = useTranslation();
const [isDragOver, setIsDragOver] = useState(false);
const [showImportModal, setShowImportModal] = useState(false);
const pendingData = useRef(null);

const { tables, relationships, setTables, setRelationships, database } =
useDiagram();
const { notes, setNotes } = useNotes();
const { areas, setAreas } = useAreas();
const { types, setTypes } = useTypes();
const { enums, setEnums } = useEnums();
const { setTransform } = useTransform();
const { setUndoStack, setRedoStack } = useUndoRedo();

const isDiagramEmpty = useCallback(() => {
return (
tables.length === 0 &&
relationships.length === 0 &&
notes.length === 0 &&
areas.length === 0 &&
types.length === 0 &&
enums.length === 0
);
}, [tables, relationships, notes, areas, types, enums]);

const overwriteDiagram = useCallback(
(data) => {
if (data.tables) setTables(data.tables);
if (data.relationships) setRelationships(data.relationships);
setAreas(data.subjectAreas ?? data.areas ?? []);
setNotes(data.notes ?? []);
if (databases[database].hasEnums && data.enums) {
setEnums(data.enums);
}
if (databases[database].hasTypes && data.types) {
setTypes(data.types);
}
if (data.customTypes) {
mergeCustomTypes(data.customTypes);
}
setTransform((prev) => ({ ...prev, pan: { x: 0, y: 0 } }));
setUndoStack([]);
setRedoStack([]);
},
[
database,
setTables,
setRelationships,
setAreas,
setNotes,
setEnums,
setTypes,
setTransform,
setUndoStack,
setRedoStack,
],
);

const appendToDiagram = useCallback(
(data) => {
if (data.tables) {
setTables((prev) => [...prev, ...data.tables]);
}
if (data.relationships) {
setRelationships((prev) =>
[...prev, ...data.relationships].map((r, i) => ({ ...r, id: i })),
);
}
if (databases[database].hasEnums && data.enums?.length) {
setEnums((prev) => [...prev, ...data.enums]);
}
if (databases[database].hasTypes && data.types?.length) {
setTypes((prev) => [...prev, ...data.types]);
}
if (data.customTypes) {
mergeCustomTypes(data.customTypes);
}
setUndoStack([]);
setRedoStack([]);
},
[
database,
setTables,
setRelationships,
setEnums,
setTypes,
setUndoStack,
setRedoStack,
],
);

const handleImportOverwrite = useCallback(() => {
if (pendingData.current) {
overwriteDiagram(pendingData.current);
Toast.success(
t("file_imported_successfully") || "File imported successfully",
);
}
pendingData.current = null;
setShowImportModal(false);
}, [overwriteDiagram, t]);

const handleImportAppend = useCallback(() => {
if (pendingData.current) {
appendToDiagram(pendingData.current);
Toast.success(
t("file_imported_successfully") || "File imported successfully",
);
}
pendingData.current = null;
setShowImportModal(false);
}, [appendToDiagram, t]);

const handleImportCancel = useCallback(() => {
pendingData.current = null;
setShowImportModal(false);
}, []);

const handleDragOver = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = "copy";
setIsDragOver(true);
}, []);

const handleDragLeave = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
}, []);

const handleDrop = useCallback(
async (e) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);

const files = Array.from(e.dataTransfer.files);
if (files.length === 0) return;

const file = files[0];

if (!isFileSupported(file)) {
Toast.error(
t("file_type_not_supported") ||
"This file type is not supported. Supported types: JSON, DDB, DBML, SQL",
);
return;
}

const { data, error } = await importFromFile(file, database);

if (error) {
Toast.error(error);
return;
}

if (!data) return;

if (isDiagramEmpty()) {
overwriteDiagram(data);
Toast.success(
t("file_imported_successfully") || "File imported successfully",
);
} else {
pendingData.current = data;
setShowImportModal(true);
}
},
[database, isDiagramEmpty, overwriteDiagram, t],
);

return {
isDragOver,
showImportModal,
handleDragOver,
handleDragLeave,
handleDrop,
handleImportOverwrite,
handleImportAppend,
handleImportCancel,
};
}
Loading
Loading