mirror of
https://github.com/chartdb/chartdb.git
synced 2026-02-10 21:49:56 -06:00
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:
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface ValidationResult {
|
||||
warnings: ValidationWarning[];
|
||||
fixedSQL?: string;
|
||||
tableCount?: number;
|
||||
relationshipCount?: number;
|
||||
}
|
||||
|
||||
export interface ValidationError {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user