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
37 changes: 37 additions & 0 deletions src/components/CodeEditor/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Editor } from "@monaco-editor/react";
import { useEffect, useRef } from "react";
import { useDiagram, useSettings } from "../../hooks";
import { Button, Toast } from "@douyinfe/semi-ui";
import { useTranslation } from "react-i18next";
Expand All @@ -9,6 +10,7 @@ import "./styles.css";
export default function CodeEditor({
showCopyButton,
extraControls,
markers = [],
...props
}) {
const { settings } = useSettings();
Expand All @@ -24,14 +26,49 @@ export default function CodeEditor({
});
};

const editorRef = useRef(null);
const monacoRef = useRef(null);

const handleEditorMount = (editor, monaco) => {
setUpDBML(monaco, database);

setTimeout(() => {
editor.getAction("editor.action.formatDocument").run();
}, 300);

editorRef.current = editor;
monacoRef.current = monaco;

// Apply existing markers after mount
try {
const model = editor.getModel();
if (model) {
const normalized = markers.map((m) => ({
severity: m.severity ?? monaco.MarkerSeverity.Error,
...m,
}));
monaco.editor.setModelMarkers(model, "dbml", normalized);
}
} catch (err) {
console.warn("Failed to set initial markers", err);
}
};

useEffect(() => {
if (!editorRef.current || !monacoRef.current) return;
try {
const model = editorRef.current.getModel();
if (!model) return;
const normalized = markers.map((m) => ({
severity: m.severity ?? monacoRef.current.MarkerSeverity.Error,
...m,
}));
monacoRef.current.editor.setModelMarkers(model, "dbml", normalized);
} catch (err) {
console.warn("Failed to update markers", err);
}
}, [markers]);

return (
<div className="relative h-full">
<Editor
Expand Down
146 changes: 141 additions & 5 deletions src/components/EditorSidePanel/DBMLEditor.jsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,161 @@
import { useEffect, useState } from "react";
import { useDiagram, useEnums, useLayout } from "../../hooks";
import { toDBML } from "../../utils/exportAs/dbml";
import { fromDBML } from "../../utils/importFrom/dbml";
import { Button, Tooltip } from "@douyinfe/semi-ui";
import { IconTemplate } from "@douyinfe/semi-icons";
import { useTranslation } from "react-i18next";
import CodeEditor from "../CodeEditor";

export default function DBMLEditor() {
const { tables: currentTables, relationships } = useDiagram();
const diagram = useDiagram();
const { enums } = useEnums();
const {
tables: currentTables,
relationships,
setTables,
setRelationships,
database,
externalIssues,
} = diagram;
const { enums, setEnums } = useEnums();
const [value, setValue] = useState(() => toDBML({ ...diagram, enums }));
const { setLayout } = useLayout();
const { setExternalIssues } = diagram;
const { t } = useTranslation();

// Translate DBML parse errors to issues and Monaco markers
const [markers, setMarkers] = useState([]);

const toggleDBMLEditor = () => {
setLayout((prev) => ({ ...prev, dbmlEditor: !prev.dbmlEditor }));
};

useEffect(() => {
setValue(toDBML({ tables: currentTables, enums, relationships }));
}, [currentTables, enums, relationships]);
const normalized = toDBML({
tables: currentTables,
enums,
relationships,
database,
});
setValue(normalized);
}, [currentTables, enums, relationships, database]);

useEffect(() => {
const currentDbml = toDBML({
tables: currentTables,
enums,
relationships,
database,
});

if (value === currentDbml) {
// If editor content already matches diagram state,
// ensure any lingering external issues/markers are cleared
if (externalIssues?.length) setExternalIssues([]);
if (markers.length) setMarkers([]);
return;
}

const handle = setTimeout(() => {
try {
const parsed = fromDBML(value);
// Preserve coordinates when table names match existing ones
const nameToExisting = new Map(
currentTables.map((t) => [t.name, { x: t.x, y: t.y }]),
);
parsed.tables = parsed.tables.map((t) => {
const coords = nameToExisting.get(t.name);
return coords ? { ...t, ...coords } : t;
});
setTables(parsed.tables);
setRelationships(parsed.relationships);
setEnums(parsed.enums);
// Clear any previous external issues on success
setExternalIssues([]);
setMarkers([]);
} catch (err) {
const { issues: parsedIssues, markers: parsedMarkers } =
produceDiagnostics(err);
setExternalIssues(parsedIssues);
setMarkers(parsedMarkers);
}
}, 700);

return () => clearTimeout(handle);
}, [
value,
currentTables,
enums,
relationships,
database,
setTables,
setRelationships,
setEnums,
setExternalIssues,
externalIssues?.length,
markers.length,
]);

const produceDiagnostics = (err) => {
// Prefer diagnostics from @dbml/core if present
if (Array.isArray(err?.diags) && err.diags.length > 0) {
const issues = err.diags.map((d) => {
const ln = d?.location?.start?.line;
const col = d?.location?.start?.column;
const code = d?.code ? ` [${d.code}]` : "";
if (ln && col) return `line ${ln}, col ${col}: ${d.message}${code}`;
return d.message + code;
});

const markers = err.diags.map((d) => {
const start = d?.location?.start || {};
const end = d?.location?.end || {};
const startLineNumber = start.line || 1;
const startColumn = start.column || 1;
const endLineNumber = end.line || startLineNumber;
const endColumn = end.column || startColumn + 1;
return {
startLineNumber,
startColumn,
endLineNumber,
endColumn,
message: d.message,
};
});
return { issues, markers };
}

// Fallbacks
const message =
(typeof err?.message === "string" && err.message) ||
(typeof err?.description === "string" && err.description) ||
(() => {
try {
return JSON.stringify(err);
} catch {
return String(err);
}
})();

// Try to extract line/column from string messages
const m =
/line\s+(\d+)\s*,\s*column\s*(\d+)/i.exec(message) ||
/\((\d+)\s*[:|,]\s*(\d+)\)/.exec(message);
const ln = m ? parseInt(m[1], 10) : 1;
const col = m ? parseInt(m[2], 10) : 1;
return {
issues: [message],
markers: [
{
startLineNumber: ln,
startColumn: col,
endLineNumber: ln,
endColumn: col + 1,
message,
},
],
};
};

return (
<CodeEditor
Expand All @@ -29,8 +164,9 @@ export default function DBMLEditor() {
language="dbml"
onChange={setValue}
height="100%"
markers={markers}
options={{
readOnly: true,
readOnly: false,
minimap: { enabled: false },
}}
extraControls={
Expand Down
52 changes: 32 additions & 20 deletions src/components/EditorSidePanel/Issues.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,24 @@ export default function Issues() {
const { t } = useTranslation();
const { settings } = useSettings();
const { enums } = useEnums();
const { tables, relationships, database } = useDiagram();
const { tables, relationships, database, externalIssues } = useDiagram();
const [issues, setIssues] = useState([]);

useEffect(() => {
const findIssues = async () => {
const newIssues = getIssues({
tables: tables,
relationships: relationships,
types: types,
database: database,
enums: enums,
});
const newIssues = getIssues({
tables: tables,
relationships: relationships,
types: types,
database: database,
enums: enums,
});

if (!arrayIsEqual(newIssues, issues)) {
setIssues(newIssues);
}
};
const combined = [...externalIssues, ...newIssues];

findIssues();
}, [tables, relationships, issues, types, database, enums]);
if (!arrayIsEqual(combined, issues)) {
setIssues(combined);
}
}, [tables, relationships, issues, types, database, enums, externalIssues]);

return (
<Collapse lazyRender keepDOM={false} style={{ width: "100%" }}>
Expand All @@ -54,11 +52,25 @@ export default function Issues() {
<div className="mb-1">{t("strict_mode_is_on_no_issues")}</div>
) : issues.length > 0 ? (
<>
{issues.map((e, i) => (
<div key={i} className="py-2">
{e}
</div>
))}
{issues.map((e, i) => {
const text =
typeof e === "string"
? e
: typeof e?.message === "string"
? e.message
: (() => {
try {
return JSON.stringify(e);
} catch {
return String(e);
}
})();
return (
<div key={i} className="py-2">
{text}
</div>
);
})}
</>
) : (
<div>{t("no_issues")}</div>
Expand Down
3 changes: 3 additions & 0 deletions src/context/DiagramContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default function DiagramContextProvider({ children }) {
const [database, setDatabase] = useState(DB.GENERIC);
const [tables, setTables] = useState([]);
const [relationships, setRelationships] = useState([]);
const [externalIssues, setExternalIssues] = useState([]);
const { transform } = useTransform();
const { setUndoStack, setRedoStack } = useUndoRedo();
const { selectedElement, setSelectedElement } = useSelect();
Expand Down Expand Up @@ -244,6 +245,8 @@ export default function DiagramContextProvider({ children }) {
setDatabase,
tablesCount: tables.length,
relationshipsCount: relationships.length,
externalIssues,
setExternalIssues,
}}
>
{children}
Expand Down