fix: add import from canvas (#995)

* fix: add import from canvas

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
This commit is contained in:
Jonathan Fishner
2025-12-09 14:09:48 +02:00
committed by GitHub
parent b16336bae2
commit 8b31944f67
8 changed files with 259 additions and 345 deletions

View File

@@ -101,8 +101,12 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
});
const openImportDatabaseDialogHandler: DialogContext['openImportDatabaseDialog'] =
useCallback(
({ databaseType }) => {
setImportDatabaseDialogParams({ databaseType });
({ databaseType, importMethods, initialImportMethod }) => {
setImportDatabaseDialogParams({
databaseType,
importMethods,
initialImportMethod,
});
setOpenImportDatabaseDialog(true);
},
[setOpenImportDatabaseDialog]
@@ -144,8 +148,9 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
closeCreateRelationshipDialog: () =>
setOpenCreateRelationshipDialog(false),
openImportDatabaseDialog: openImportDatabaseDialogHandler,
closeImportDatabaseDialog: () =>
setOpenImportDatabaseDialog(false),
closeImportDatabaseDialog: () => {
setOpenImportDatabaseDialog(false);
},
openTableSchemaDialog: openTableSchemaDialogHandler,
closeTableSchemaDialog: () => setOpenTableSchemaDialog(false),
openStarUsDialog: () => setOpenStarUsDialog(true),

View File

@@ -46,6 +46,8 @@ import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-lang
import type { ImportMethod } from '@/lib/import-method/import-method';
import { detectImportMethod } from '@/lib/import-method/detect-import-method';
import { verifyDBML } from '@/lib/dbml/dbml-import/verify-dbml';
import { importDBMLToDiagram } from '@/lib/dbml/dbml-import/dbml-import';
import { sqlImportToDiagram } from '@/lib/data/sql-import';
import {
clearErrorHighlight,
highlightErrorLine,
@@ -78,6 +80,7 @@ export interface ImportDatabaseProps {
title: string;
importMethod: ImportMethod;
setImportMethod: (method: ImportMethod) => void;
importMethods?: ImportMethod[];
}
export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
@@ -93,6 +96,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
title,
importMethod,
setImportMethod,
importMethods,
}) => {
const { effectiveTheme } = useTheme();
const [errorMessage, setErrorMessage] = useState('');
@@ -143,11 +147,31 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
const validateResponse = verifyDBML(scriptResult, { databaseType });
if (!validateResponse.hasError) {
setErrorMessage('');
setSqlValidation({
isValid: true,
errors: [],
warnings: [],
});
// Try to count tables/relationships for DBML
(async () => {
try {
const diagram = await importDBMLToDiagram(
scriptResult,
{ databaseType }
);
setSqlValidation({
isValid: true,
errors: [],
warnings: [],
tableCount: diagram.tables?.length ?? 0,
relationshipCount:
diagram.relationships?.length ?? 0,
});
} catch {
// If parsing fails, just show validation without counts
setSqlValidation({
isValid: true,
errors: [],
warnings: [],
});
}
})();
} else {
let errorMsg = 'Invalid DBML syntax';
let line: number = 1;
@@ -184,10 +208,10 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
// SQL validation
// First run our validation based on database type
const validation = validateSQL(scriptResult, databaseType);
setSqlValidation(validation);
// If we have auto-fixable errors, show the auto-fix button
if (validation.fixedSQL && validation.errors.length > 0) {
setSqlValidation(validation);
setShowAutoFixButton(true);
// Don't try to parse invalid SQL
setErrorMessage('SQL contains syntax errors');
@@ -197,14 +221,33 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
// Hide auto-fix button if no fixes available
setShowAutoFixButton(false);
// Validate the SQL (either original or already fixed)
// Validate the SQL (either original or already fixed) and count tables/relationships
parseSQLError({
sqlContent: scriptResult,
sourceDatabaseType: databaseType,
}).then((result) => {
}).then(async (result) => {
if (result.success) {
setErrorMessage('');
// Try to parse and count tables/relationships for successful validation
try {
const diagram = await sqlImportToDiagram({
sqlContent: scriptResult,
sourceDatabaseType: databaseType,
targetDatabaseType: databaseType,
});
setSqlValidation({
...validation,
tableCount: diagram.tables?.length ?? 0,
relationshipCount: diagram.relationships?.length ?? 0,
});
} catch {
// If parsing fails, just show validation without counts
setSqlValidation(validation);
}
} else if (!result.success && result.error) {
setSqlValidation(validation);
setErrorMessage(result.error);
}
});
@@ -333,6 +376,12 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
};
}, []);
// Use ref to track current import method to avoid closure issues
const importMethodRef = useRef(importMethod);
useEffect(() => {
importMethodRef.current = importMethod;
}, [importMethod]);
const handleEditorDidMount = useCallback(
(editor: editor.IStandaloneCodeEditor) => {
editorRef.current = editor;
@@ -357,7 +406,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
// First, detect content type to determine if we should switch modes
const detectedType = detectImportMethod(content);
if (detectedType && detectedType !== importMethod) {
const currentMethod = importMethodRef.current;
if (detectedType && detectedType !== currentMethod) {
// Switch to the detected mode immediately
setImportMethod(detectedType);
@@ -373,7 +423,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
// For DDL and DBML modes, do NOT format as it can break the syntax
} else {
// Content type didn't change, apply formatting based on current mode
if (importMethod === 'query' && !isLargeFile) {
if (currentMethod === 'query' && !isLargeFile) {
// Only format JSON content if not too large
setTimeout(() => {
editor
@@ -387,7 +437,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
pasteDisposableRef.current = disposable;
},
[importMethod, setImportMethod]
[setImportMethod]
);
const renderHeader = useCallback(() => {
@@ -409,6 +459,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
databaseEdition={databaseEdition}
setShowSSMSInfoDialog={setShowSSMSInfoDialog}
showSSMSInfoDialog={showSSMSInfoDialog}
importMethods={importMethods}
/>
),
[
@@ -419,6 +470,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
databaseEdition,
setShowSSMSInfoDialog,
showSSMSInfoDialog,
importMethods,
]
);
@@ -489,6 +541,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
errorMessage={errorMessage}
isAutoFixing={isAutoFixing}
onErrorClick={handleErrorClick}
importMethod={importMethod}
/>
) : null}
</div>

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import logo from '@/assets/logo-2.png';
import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group';
import { DatabaseType } from '@/lib/domain/database-type';
@@ -36,8 +36,11 @@ export interface InstructionsSectionProps {
setImportMethod: (method: ImportMethod) => void;
showSSMSInfoDialog: boolean;
setShowSSMSInfoDialog: (show: boolean) => void;
importMethods?: ImportMethod[];
}
const defaultImportMethods: ImportMethod[] = ['query', 'ddl', 'dbml'];
export const InstructionsSection: React.FC<InstructionsSectionProps> = ({
databaseType,
databaseEdition,
@@ -46,12 +49,27 @@ export const InstructionsSection: React.FC<InstructionsSectionProps> = ({
setImportMethod,
setShowSSMSInfoDialog,
showSSMSInfoDialog,
importMethods = defaultImportMethods,
}) => {
const { t } = useTranslation();
const showSmartQuery = useMemo(
() => importMethods.includes('query'),
[importMethods]
);
const showDDL = useMemo(
() => importMethods.includes('ddl'),
[importMethods]
);
const showDBML = useMemo(
() => importMethods.includes('dbml'),
[importMethods]
);
return (
<div className="flex w-full flex-1 flex-col gap-4">
{databaseTypeToEditionMap[databaseType].length > 0 ? (
{showSmartQuery &&
databaseTypeToEditionMap[databaseType].length > 0 ? (
<div className="flex flex-col gap-1">
<p className="text-sm leading-6 text-primary">
{t(
@@ -134,47 +152,52 @@ export const InstructionsSection: React.FC<InstructionsSectionProps> = ({
setImportMethod(selectedImportMethod);
}}
>
<ToggleGroupItem
value="query"
variant="outline"
className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700"
>
<Avatar className="h-3 w-4 rounded-none">
<AvatarImage src={logo} alt="query" />
<AvatarFallback>Query</AvatarFallback>
</Avatar>
Smart Query
</ToggleGroupItem>
{!DatabasesWithoutDDLInstructions.includes(
databaseType
) && (
{showSmartQuery && (
<ToggleGroupItem
value="ddl"
value="query"
variant="outline"
className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700"
>
<Avatar className="h-3 w-4 rounded-none">
<AvatarImage src={logo} alt="query" />
<AvatarFallback>Query</AvatarFallback>
</Avatar>
Smart Query
</ToggleGroupItem>
)}
{showDDL &&
!DatabasesWithoutDDLInstructions.includes(
databaseType
) && (
<ToggleGroupItem
value="ddl"
variant="outline"
className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700"
>
<Avatar className="size-4 rounded-none">
<FileCode size={16} />
</Avatar>
SQL Script
</ToggleGroupItem>
)}
{showDBML && (
<ToggleGroupItem
value="dbml"
variant="outline"
className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700"
>
<Avatar className="size-4 rounded-none">
<FileCode size={16} />
<Code size={16} />
</Avatar>
SQL Script
DBML
</ToggleGroupItem>
)}
<ToggleGroupItem
value="dbml"
variant="outline"
className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700"
>
<Avatar className="size-4 rounded-none">
<Code size={16} />
</Avatar>
DBML
</ToggleGroupItem>
</ToggleGroup>
</div>
<div className="flex flex-col gap-2">
<div className="text-sm font-semibold">Instructions:</div>
{importMethod === 'query' ? (
{importMethod === 'query' && showSmartQuery ? (
<SmartQueryInstructions
databaseType={databaseType}
databaseEdition={databaseEdition}

View File

@@ -11,6 +11,7 @@ interface SQLValidationStatusProps {
errorMessage: string;
isAutoFixing?: boolean;
onErrorClick?: (line: number) => void;
importMethod?: 'ddl' | 'dbml' | 'query';
}
export const SQLValidationStatus: React.FC<SQLValidationStatusProps> = ({
@@ -18,6 +19,7 @@ export const SQLValidationStatus: React.FC<SQLValidationStatusProps> = ({
errorMessage,
isAutoFixing = false,
onErrorClick,
importMethod = 'ddl',
}) => {
const hasErrors = useMemo(
() => validation?.errors.length && validation.errors.length > 0,
@@ -168,7 +170,46 @@ export const SQLValidationStatus: React.FC<SQLValidationStatusProps> = ({
<div className="flex items-start gap-2">
<CheckCircle className="mt-0.5 size-4 shrink-0 text-green-700 dark:text-green-300" />
<div className="flex-1 text-sm text-green-700 dark:text-green-300">
SQL syntax validated successfully
<div>
{importMethod === 'dbml'
? 'DBML syntax validated successfully'
: 'SQL syntax validated successfully'}
</div>
{(validation.tableCount !== undefined ||
validation.relationshipCount !==
undefined) && (
<div className="mt-1 flex gap-2 text-xs">
{validation.tableCount !== undefined &&
validation.tableCount > 0 && (
<span>
<span className="font-semibold">
{validation.tableCount}
</span>{' '}
table
{validation.tableCount !== 1
? 's'
: ''}
</span>
)}
{validation.relationshipCount !==
undefined &&
validation.relationshipCount >
0 && (
<span>
<span className="font-semibold">
{
validation.relationshipCount
}
</span>{' '}
relationship
{validation.relationshipCount !==
1
? 's'
: ''}
</span>
)}
</div>
)}
</div>
</div>
</div>

View File

@@ -10,39 +10,40 @@ import type { Diagram } from '@/lib/domain/diagram';
import { loadFromDatabaseMetadata } from '@/lib/data/import-metadata/import';
import { useChartDB } from '@/hooks/use-chartdb';
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
import { Trans, useTranslation } from 'react-i18next';
import { useReactFlow } from '@xyflow/react';
import { useTranslation } from 'react-i18next';
import type { BaseDialogProps } from '../common/base-dialog-props';
import { useAlert } from '@/context/alert-context/alert-context';
import { sqlImportToDiagram } from '@/lib/data/sql-import';
import { importDBMLToDiagram } from '@/lib/dbml/dbml-import/dbml-import';
import type { ImportMethod } from '@/lib/import-method/import-method';
export interface ImportDatabaseDialogProps extends BaseDialogProps {
databaseType: DatabaseType;
importMethods?: ImportMethod[];
initialImportMethod?: ImportMethod;
}
const defaultImportMethods: ImportMethod[] = ['query', 'ddl', 'dbml'];
export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
dialog,
databaseType,
importMethods = defaultImportMethods,
initialImportMethod,
}) => {
const [importMethod, setImportMethod] = useState<ImportMethod>('query');
const [importMethod, setImportMethod] = useState<ImportMethod>(
initialImportMethod ?? importMethods[0]
);
const { closeImportDatabaseDialog } = useDialog();
const { showAlert } = useAlert();
const {
tables,
relationships,
removeTables,
removeRelationships,
addTables,
addRelationships,
diagramName,
databaseType: currentDatabaseType,
updateDatabaseType,
tables: existingTables,
} = useChartDB();
const [scriptResult, setScriptResult] = useState('');
const { resetRedoStack, resetUndoStack } = useRedoUndoStack();
const { setNodes } = useReactFlow();
const { t } = useTranslation();
const [databaseEdition, setDatabaseEdition] = useState<
DatabaseEdition | undefined
@@ -56,7 +57,8 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
if (!dialog.open) return;
setDatabaseEdition(undefined);
setScriptResult('');
}, [dialog.open]);
setImportMethod(initialImportMethod ?? importMethods[0]);
}, [dialog.open, importMethods, initialImportMethod]);
const importDatabase = useCallback(async () => {
let diagram: Diagram | undefined;
@@ -85,247 +87,54 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
});
}
const tableIdsToRemove = tables
.filter((table) =>
diagram.tables?.some(
(t) => t.name === table.name && t.schema === table.schema
)
)
.map((table) => table.id);
const relationshipIdsToRemove = relationships
.filter((relationship) => {
const sourceTable = tables.find(
(table) => table.id === relationship.sourceTableId
);
const targetTable = tables.find(
(table) => table.id === relationship.targetTableId
);
if (!sourceTable || !targetTable) return true; // should not happen
const sourceField = sourceTable.fields.find(
(field) => field.id === relationship.sourceFieldId
);
const targetField = targetTable.fields.find(
(field) => field.id === relationship.targetFieldId
);
if (!sourceField || !targetField) return true; // should not happen
const replacementSourceTable = diagram.tables?.find(
(table) =>
table.name === sourceTable.name &&
table.schema === sourceTable.schema
);
const replacementTargetTable = diagram.tables?.find(
(table) =>
table.name === targetTable.name &&
table.schema === targetTable.schema
);
// if the source or target field of the relationship is not in the new table, remove the relationship
if (
(replacementSourceTable &&
!replacementSourceTable.fields.some(
(field) => field.name === sourceField.name
)) ||
(replacementTargetTable &&
!replacementTargetTable.fields.some(
(field) => field.name === targetField.name
))
) {
return true;
}
return diagram.relationships?.some((r) => {
const sourceNewTable = diagram.tables?.find(
(table) => table.id === r.sourceTableId
);
const targetNewTable = diagram.tables?.find(
(table) => table.id === r.targetTableId
);
const sourceNewField = sourceNewTable?.fields.find(
(field) => field.id === r.sourceFieldId
);
const targetNewField = targetNewTable?.fields.find(
(field) => field.id === r.targetFieldId
);
return (
sourceField.name === sourceNewField?.name &&
sourceTable.name === sourceNewTable?.name &&
sourceTable.schema === sourceNewTable?.schema &&
targetField.name === targetNewField?.name &&
targetTable.name === targetNewTable?.name &&
targetTable.schema === targetNewTable?.schema
);
});
})
.map((relationship) => relationship.id);
const newRelationshipsNumber = diagram.relationships?.filter(
(relationship) => {
const newSourceTable = diagram.tables?.find(
(table) => table.id === relationship.sourceTableId
);
const newTargetTable = diagram.tables?.find(
(table) => table.id === relationship.targetTableId
);
const newSourceField = newSourceTable?.fields.find(
(field) => field.id === relationship.sourceFieldId
);
const newTargetField = newTargetTable?.fields.find(
(field) => field.id === relationship.targetFieldId
);
return !relationships.some((r) => {
const sourceTable = tables.find(
(table) => table.id === r.sourceTableId
);
const targetTable = tables.find(
(table) => table.id === r.targetTableId
);
const sourceField = sourceTable?.fields.find(
(field) => field.id === r.sourceFieldId
);
const targetField = targetTable?.fields.find(
(field) => field.id === r.targetFieldId
);
return (
sourceField?.name === newSourceField?.name &&
sourceTable?.name === newSourceTable?.name &&
sourceTable?.schema === newSourceTable?.schema &&
targetField?.name === newTargetField?.name &&
targetTable?.name === newTargetTable?.name &&
targetTable?.schema === newTargetTable?.schema
);
});
}
).length;
const newTablesNumber = diagram.tables?.filter(
(table) =>
!tables.some(
(t) => t.name === table.name && t.schema === table.schema
)
).length;
const shouldRemove = new Promise<boolean>((resolve) => {
if (
tableIdsToRemove.length === 0 &&
relationshipIdsToRemove.length === 0 &&
newTablesNumber === 0 &&
newRelationshipsNumber === 0
) {
resolve(true);
return;
}
const content = (
<>
<div className="!mb-2">
{t(
'import_database_dialog.override_alert.content.alert'
)}
</div>
{(newTablesNumber ?? 0 > 0) ? (
<div className="!m-0 text-blue-500">
<Trans
i18nKey="import_database_dialog.override_alert.content.new_tables"
values={{
newTablesNumber,
}}
components={{
bold: <span className="font-bold" />,
}}
/>
</div>
) : null}
{(newRelationshipsNumber ?? 0 > 0) ? (
<div className="!m-0 text-blue-500">
<Trans
i18nKey="import_database_dialog.override_alert.content.new_relationships"
values={{
newRelationshipsNumber,
}}
components={{
bold: <span className="font-bold" />,
}}
/>
</div>
) : null}
{tableIdsToRemove.length > 0 && (
<div className="!m-0 text-red-500">
<Trans
i18nKey="import_database_dialog.override_alert.content.tables_override"
values={{
tablesOverrideNumber:
tableIdsToRemove.length,
}}
components={{
bold: <span className="font-bold" />,
}}
/>
</div>
)}
<div className="!mt-2">
{t(
'import_database_dialog.override_alert.content.proceed'
)}
</div>
</>
);
showAlert({
title: t('import_database_dialog.override_alert.title'),
content,
actionLabel: t('import_database_dialog.override_alert.import'),
closeLabel: t('import_database_dialog.override_alert.cancel'),
onAction: () => resolve(true),
onClose: () => resolve(false),
});
});
if (!(await shouldRemove)) return;
await Promise.all([
removeTables(tableIdsToRemove, { updateHistory: false }),
removeRelationships(relationshipIdsToRemove, {
updateHistory: false,
}),
]);
await Promise.all([
addTables(diagram.tables ?? [], { updateHistory: false }),
addRelationships(diagram.relationships ?? [], {
updateHistory: false,
}),
]);
if (currentDatabaseType === DatabaseType.GENERIC) {
await updateDatabaseType(databaseType);
// Skip if nothing to import
const newTablesNumber = diagram.tables?.length ?? 0;
const newRelationshipsNumber = diagram.relationships?.length ?? 0;
if (newTablesNumber === 0 && newRelationshipsNumber === 0) {
return;
}
setNodes((nodes) =>
nodes.map((node) => ({
...node,
selected:
diagram.tables?.some((table) => table.id === node.id) ??
false,
}))
);
resetRedoStack();
resetUndoStack();
// Close dialog immediately to prevent re-render blocking
closeImportDatabaseDialog();
// Calculate position offset for new tables to avoid overlap
let offsetX = 0;
if (existingTables.length > 0) {
// Find the rightmost table
const rightmostTable = existingTables.reduce((max, table) => {
const tableRight = table.x + (table.width ?? 250);
const maxRight = max.x + (max.width ?? 250);
return tableRight > maxRight ? table : max;
});
// Position new tables 150px to the right of the rightmost table
offsetX = rightmostTable.x + (rightmostTable.width ?? 250) + 150;
}
// Apply offset to imported tables
const positionedTables =
diagram.tables?.map((table) => ({
...table,
x: table.x + offsetX,
})) ?? [];
// Use queueMicrotask to defer work after dialog closes but before next paint
queueMicrotask(async () => {
// Add tables and relationships
await Promise.all([
addTables(positionedTables, { updateHistory: false }),
addRelationships(diagram.relationships ?? [], {
updateHistory: false,
}),
]);
if (currentDatabaseType === DatabaseType.GENERIC) {
await updateDatabaseType(databaseType);
}
// Reset undo/redo stacks
resetRedoStack();
resetUndoStack();
});
}, [
importMethod,
databaseEdition,
@@ -333,18 +142,12 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
updateDatabaseType,
databaseType,
scriptResult,
tables,
addRelationships,
addTables,
closeImportDatabaseDialog,
relationships,
removeRelationships,
removeTables,
resetRedoStack,
resetUndoStack,
showAlert,
setNodes,
t,
closeImportDatabaseDialog,
existingTables,
]);
return (
@@ -371,6 +174,7 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
title={t('import_database_dialog.title', { diagramName })}
importMethod={importMethod}
setImportMethod={setImportMethod}
importMethods={importMethods}
/>
</DialogContent>
</Dialog>

View File

@@ -9,6 +9,7 @@ export interface ValidationResult {
warnings: ValidationWarning[];
fixedSQL?: string;
tableCount?: number;
relationshipCount?: number;
}
export interface ValidationError {

View File

@@ -11,7 +11,7 @@ import { useDialog } from '@/hooks/use-dialog';
import { useReactFlow } from '@xyflow/react';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Table, Workflow, Group, View, StickyNote } from 'lucide-react';
import { Table, Workflow, Group, View, StickyNote, Import } from 'lucide-react';
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
import { useLocalConfig } from '@/hooks/use-local-config';
import { useCanvas } from '@/hooks/use-canvas';
@@ -23,7 +23,8 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
const { createTable, readonly, createArea, databaseType, createNote } =
useChartDB();
const { schemasDisplayed } = useDiagramFilter();
const { openCreateRelationshipDialog } = useDialog();
const { openCreateRelationshipDialog, openImportDatabaseDialog } =
useDialog();
const { screenToFlowPosition } = useReactFlow();
const { t } = useTranslation();
const { showDBViews } = useLocalConfig();
@@ -142,6 +143,16 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
openCreateRelationshipDialog();
}, [openCreateRelationshipDialog]);
const importSqlDbmlHandler = useCallback(() => {
// Defer dialog opening to prevent Radix UI context menu/dialog portal conflicts
queueMicrotask(() => {
openImportDatabaseDialog({
databaseType,
importMethods: ['ddl', 'dbml'],
});
});
}, [openImportDatabaseDialog, databaseType]);
if (!isDesktop) {
return <>{children}</>;
}
@@ -189,6 +200,14 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
>
{t('canvas_context_menu.new_note')}
<StickyNote className="size-3.5" />
</ContextMenuItem>{' '}
<ContextMenuSeparator />
<ContextMenuItem
onClick={importSqlDbmlHandler}
className="flex justify-between gap-4"
>
Import SQL/DBML
<Import className="size-3.5" />
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>

View File

@@ -188,56 +188,24 @@ export const Menu: React.FC<MenuProps> = () => {
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType: DatabaseType.POSTGRESQL,
databaseType,
importMethods: ['ddl', 'dbml'],
initialImportMethod: 'ddl',
})
}
>
{databaseTypeToLabelMap['postgresql']}
SQL
</MenubarItem>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType: DatabaseType.MYSQL,
databaseType,
importMethods: ['ddl', 'dbml'],
initialImportMethod: 'dbml',
})
}
>
{databaseTypeToLabelMap['mysql']}
</MenubarItem>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType: DatabaseType.SQL_SERVER,
})
}
>
{databaseTypeToLabelMap['sql_server']}
</MenubarItem>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType: DatabaseType.MARIADB,
})
}
>
{databaseTypeToLabelMap['mariadb']}
</MenubarItem>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType: DatabaseType.SQLITE,
})
}
>
{databaseTypeToLabelMap['sqlite']}
</MenubarItem>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType: DatabaseType.ORACLE,
})
}
>
{databaseTypeToLabelMap['oracle']}
DBML
</MenubarItem>
</MenubarSubContent>
</MenubarSub>