mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-19 14:08:37 +00:00
add import database dialog
This commit is contained in:
committed by
Guy Ben-Aharon
parent
970bd4d801
commit
45f4bf88a8
@@ -44,11 +44,19 @@ export interface ChartDBContext {
|
|||||||
table: DBTable,
|
table: DBTable,
|
||||||
options?: { updateHistory: boolean }
|
options?: { updateHistory: boolean }
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
addTables: (
|
||||||
|
tables: DBTable[],
|
||||||
|
options?: { updateHistory: boolean }
|
||||||
|
) => Promise<void>;
|
||||||
getTable: (id: string) => DBTable | null;
|
getTable: (id: string) => DBTable | null;
|
||||||
removeTable: (
|
removeTable: (
|
||||||
id: string,
|
id: string,
|
||||||
options?: { updateHistory: boolean }
|
options?: { updateHistory: boolean }
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
removeTables: (
|
||||||
|
ids: string[],
|
||||||
|
options?: { updateHistory: boolean }
|
||||||
|
) => Promise<void>;
|
||||||
updateTable: (
|
updateTable: (
|
||||||
id: string,
|
id: string,
|
||||||
table: Partial<DBTable>,
|
table: Partial<DBTable>,
|
||||||
@@ -163,7 +171,9 @@ export const chartDBContext = createContext<ChartDBContext>({
|
|||||||
createTable: emptyFn,
|
createTable: emptyFn,
|
||||||
getTable: emptyFn,
|
getTable: emptyFn,
|
||||||
addTable: emptyFn,
|
addTable: emptyFn,
|
||||||
|
addTables: emptyFn,
|
||||||
removeTable: emptyFn,
|
removeTable: emptyFn,
|
||||||
|
removeTables: emptyFn,
|
||||||
updateTable: emptyFn,
|
updateTable: emptyFn,
|
||||||
updateTablesState: emptyFn,
|
updateTablesState: emptyFn,
|
||||||
|
|
||||||
|
|||||||
@@ -253,21 +253,21 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const addTable: ChartDBContext['addTable'] = useCallback(
|
const addTables: ChartDBContext['addTables'] = useCallback(
|
||||||
async (table: DBTable, options = { updateHistory: true }) => {
|
async (tables: DBTable[], options = { updateHistory: true }) => {
|
||||||
setTables((tables) => [...tables, table]);
|
setTables((currentTables) => [...currentTables, ...tables]);
|
||||||
const updatedAt = new Date();
|
const updatedAt = new Date();
|
||||||
setDiagramUpdatedAt(updatedAt);
|
setDiagramUpdatedAt(updatedAt);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||||
db.addTable({ diagramId, table }),
|
...tables.map((table) => db.addTable({ diagramId, table })),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (options.updateHistory) {
|
if (options.updateHistory) {
|
||||||
addUndoAction({
|
addUndoAction({
|
||||||
action: 'addTable',
|
action: 'addTables',
|
||||||
redoData: { table },
|
redoData: { tables },
|
||||||
undoData: { tableId: table.id },
|
undoData: { tableIds: tables.map((t) => t.id) },
|
||||||
});
|
});
|
||||||
resetRedoStack();
|
resetRedoStack();
|
||||||
}
|
}
|
||||||
@@ -275,6 +275,13 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
[db, diagramId, setTables, addUndoAction, resetRedoStack]
|
[db, diagramId, setTables, addUndoAction, resetRedoStack]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const addTable: ChartDBContext['addTable'] = useCallback(
|
||||||
|
async (table: DBTable, options = { updateHistory: true }) => {
|
||||||
|
return addTables([table], options);
|
||||||
|
},
|
||||||
|
[addTables]
|
||||||
|
);
|
||||||
|
|
||||||
const createTable: ChartDBContext['createTable'] = useCallback(
|
const createTable: ChartDBContext['createTable'] = useCallback(
|
||||||
async (attributes) => {
|
async (attributes) => {
|
||||||
const table: DBTable = {
|
const table: DBTable = {
|
||||||
@@ -314,22 +321,27 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
[tables]
|
[tables]
|
||||||
);
|
);
|
||||||
|
|
||||||
const removeTable: ChartDBContext['removeTable'] = useCallback(
|
const removeTables: ChartDBContext['removeTables'] = useCallback(
|
||||||
async (id: string, options = { updateHistory: true }) => {
|
async (ids, options) => {
|
||||||
const table = getTable(id);
|
const tables = ids.map((id) => getTable(id)).filter((t) => !!t);
|
||||||
const relationshipsToRemove = relationships.filter(
|
const relationshipsToRemove = relationships.filter(
|
||||||
(relationship) =>
|
(relationship) =>
|
||||||
relationship.sourceTableId === id ||
|
ids.includes(relationship.sourceTableId) ||
|
||||||
relationship.targetTableId === id
|
ids.includes(relationship.targetTableId)
|
||||||
);
|
);
|
||||||
|
|
||||||
setRelationships((relationships) =>
|
setRelationships((relationships) =>
|
||||||
relationships.filter(
|
relationships.filter(
|
||||||
(relationship) =>
|
(relationship) =>
|
||||||
relationship.sourceTableId !== id &&
|
!relationshipsToRemove.some(
|
||||||
relationship.targetTableId !== id
|
(r) => r.id === relationship.id
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
setTables((tables) => tables.filter((table) => table.id !== id));
|
|
||||||
|
setTables((tables) =>
|
||||||
|
tables.filter((table) => !ids.includes(table.id))
|
||||||
|
);
|
||||||
|
|
||||||
const updatedAt = new Date();
|
const updatedAt = new Date();
|
||||||
setDiagramUpdatedAt(updatedAt);
|
setDiagramUpdatedAt(updatedAt);
|
||||||
@@ -338,14 +350,16 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
...relationshipsToRemove.map((relationship) =>
|
...relationshipsToRemove.map((relationship) =>
|
||||||
db.deleteRelationship({ diagramId, id: relationship.id })
|
db.deleteRelationship({ diagramId, id: relationship.id })
|
||||||
),
|
),
|
||||||
db.deleteTable({ diagramId, id }),
|
...ids.map((id) => db.deleteTable({ diagramId, id })),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!!table && options.updateHistory) {
|
if (tables.length > 0 && options?.updateHistory) {
|
||||||
addUndoAction({
|
addUndoAction({
|
||||||
action: 'removeTable',
|
action: 'removeTables',
|
||||||
redoData: { tableId: id },
|
redoData: {
|
||||||
undoData: { table, relationships: relationshipsToRemove },
|
tableIds: ids,
|
||||||
|
},
|
||||||
|
undoData: { tables, relationships: relationshipsToRemove },
|
||||||
});
|
});
|
||||||
resetRedoStack();
|
resetRedoStack();
|
||||||
}
|
}
|
||||||
@@ -361,6 +375,13 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const removeTable: ChartDBContext['removeTable'] = useCallback(
|
||||||
|
async (id: string, options = { updateHistory: true }) => {
|
||||||
|
return removeTables([id], options);
|
||||||
|
},
|
||||||
|
[removeTables]
|
||||||
|
);
|
||||||
|
|
||||||
const updateTable: ChartDBContext['updateTable'] = useCallback(
|
const updateTable: ChartDBContext['updateTable'] = useCallback(
|
||||||
async (
|
async (
|
||||||
id: string,
|
id: string,
|
||||||
@@ -1147,8 +1168,10 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
updateDiagramUpdatedAt,
|
updateDiagramUpdatedAt,
|
||||||
createTable,
|
createTable,
|
||||||
addTable,
|
addTable,
|
||||||
|
addTables,
|
||||||
getTable,
|
getTable,
|
||||||
removeTable,
|
removeTable,
|
||||||
|
removeTables,
|
||||||
updateTable,
|
updateTable,
|
||||||
updateTablesState,
|
updateTablesState,
|
||||||
updateField,
|
updateField,
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ export interface DialogContext {
|
|||||||
// Create relationship dialog
|
// Create relationship dialog
|
||||||
openCreateRelationshipDialog: () => void;
|
openCreateRelationshipDialog: () => void;
|
||||||
closeCreateRelationshipDialog: () => void;
|
closeCreateRelationshipDialog: () => void;
|
||||||
|
|
||||||
|
// Import database dialog
|
||||||
|
openImportDatabaseDialog: (params: { databaseType: DatabaseType }) => void;
|
||||||
|
closeImportDatabaseDialog: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dialogContext = createContext<DialogContext>({
|
export const dialogContext = createContext<DialogContext>({
|
||||||
@@ -36,4 +40,6 @@ export const dialogContext = createContext<DialogContext>({
|
|||||||
showAlert: emptyFn,
|
showAlert: emptyFn,
|
||||||
closeCreateRelationshipDialog: emptyFn,
|
closeCreateRelationshipDialog: emptyFn,
|
||||||
openCreateRelationshipDialog: emptyFn,
|
openCreateRelationshipDialog: emptyFn,
|
||||||
|
openImportDatabaseDialog: emptyFn,
|
||||||
|
closeImportDatabaseDialog: emptyFn,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
BaseAlertDialogProps,
|
BaseAlertDialogProps,
|
||||||
} from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
} from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
||||||
import { CreateRelationshipDialog } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
|
import { CreateRelationshipDialog } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
|
||||||
|
import { ImportDatabaseDialog } from '@/dialogs/import-database-dialog/import-database-dialog';
|
||||||
|
|
||||||
export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@@ -21,6 +22,12 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
}>({ targetDatabaseType: DatabaseType.GENERIC });
|
}>({ targetDatabaseType: DatabaseType.GENERIC });
|
||||||
const [openCreateRelationshipDialog, setOpenCreateRelationshipDialog] =
|
const [openCreateRelationshipDialog, setOpenCreateRelationshipDialog] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [openImportDatabaseDialog, setOpenImportDatabaseDialog] =
|
||||||
|
useState(false);
|
||||||
|
const [openImportDatabaseDialogParams, setOpenImportDatabaseDialogParams] =
|
||||||
|
useState<{ databaseType: DatabaseType }>({
|
||||||
|
databaseType: DatabaseType.GENERIC,
|
||||||
|
});
|
||||||
const [showAlert, setShowAlert] = useState(false);
|
const [showAlert, setShowAlert] = useState(false);
|
||||||
const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({
|
const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({
|
||||||
title: '',
|
title: '',
|
||||||
@@ -35,6 +42,15 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
[setOpenExportSQLDialog]
|
[setOpenExportSQLDialog]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const openImportDatabaseDialogHandler: DialogContext['openImportDatabaseDialog'] =
|
||||||
|
useCallback(
|
||||||
|
({ databaseType }) => {
|
||||||
|
setOpenImportDatabaseDialog(true);
|
||||||
|
setOpenImportDatabaseDialogParams({ databaseType });
|
||||||
|
},
|
||||||
|
[setOpenImportDatabaseDialog]
|
||||||
|
);
|
||||||
|
|
||||||
const showAlertHandler: DialogContext['showAlert'] = useCallback(
|
const showAlertHandler: DialogContext['showAlert'] = useCallback(
|
||||||
(params) => {
|
(params) => {
|
||||||
setAlertParams(params);
|
setAlertParams(params);
|
||||||
@@ -62,6 +78,9 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setOpenCreateRelationshipDialog(true),
|
setOpenCreateRelationshipDialog(true),
|
||||||
closeCreateRelationshipDialog: () =>
|
closeCreateRelationshipDialog: () =>
|
||||||
setOpenCreateRelationshipDialog(false),
|
setOpenCreateRelationshipDialog(false),
|
||||||
|
openImportDatabaseDialog: openImportDatabaseDialogHandler,
|
||||||
|
closeImportDatabaseDialog: () =>
|
||||||
|
setOpenImportDatabaseDialog(false),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -75,6 +94,10 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
<CreateRelationshipDialog
|
<CreateRelationshipDialog
|
||||||
dialog={{ open: openCreateRelationshipDialog }}
|
dialog={{ open: openCreateRelationshipDialog }}
|
||||||
/>
|
/>
|
||||||
|
<ImportDatabaseDialog
|
||||||
|
dialog={{ open: openImportDatabaseDialog }}
|
||||||
|
{...openImportDatabaseDialogParams}
|
||||||
|
/>
|
||||||
</dialogContext.Provider>
|
</dialogContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
} = useRedoUndoStack();
|
} = useRedoUndoStack();
|
||||||
const {
|
const {
|
||||||
addTable,
|
addTable,
|
||||||
|
addTables,
|
||||||
removeTable,
|
removeTable,
|
||||||
|
removeTables,
|
||||||
updateTable,
|
updateTable,
|
||||||
updateDiagramName,
|
updateDiagramName,
|
||||||
removeField,
|
removeField,
|
||||||
@@ -42,9 +44,15 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
addTable: ({ redoData: { table } }) => {
|
addTable: ({ redoData: { table } }) => {
|
||||||
return addTable(table, { updateHistory: false });
|
return addTable(table, { updateHistory: false });
|
||||||
},
|
},
|
||||||
|
addTables: ({ redoData: { tables } }) => {
|
||||||
|
return addTables(tables, { updateHistory: false });
|
||||||
|
},
|
||||||
removeTable: ({ redoData: { tableId } }) => {
|
removeTable: ({ redoData: { tableId } }) => {
|
||||||
return removeTable(tableId, { updateHistory: false });
|
return removeTable(tableId, { updateHistory: false });
|
||||||
},
|
},
|
||||||
|
removeTables: ({ redoData: { tableIds } }) => {
|
||||||
|
return removeTables(tableIds, { updateHistory: false });
|
||||||
|
},
|
||||||
updateTable: ({ redoData: { tableId, table } }) => {
|
updateTable: ({ redoData: { tableId, table } }) => {
|
||||||
return updateTable(tableId, table, { updateHistory: false });
|
return updateTable(tableId, table, { updateHistory: false });
|
||||||
},
|
},
|
||||||
@@ -104,7 +112,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
addTable,
|
addTable,
|
||||||
|
addTables,
|
||||||
removeTable,
|
removeTable,
|
||||||
|
removeTables,
|
||||||
updateTable,
|
updateTable,
|
||||||
updateDiagramName,
|
updateDiagramName,
|
||||||
removeField,
|
removeField,
|
||||||
@@ -130,12 +140,21 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
addTable: ({ undoData: { tableId } }) => {
|
addTable: ({ undoData: { tableId } }) => {
|
||||||
return removeTable(tableId, { updateHistory: false });
|
return removeTable(tableId, { updateHistory: false });
|
||||||
},
|
},
|
||||||
|
addTables: ({ undoData: { tableIds } }) => {
|
||||||
|
return removeTables(tableIds, { updateHistory: false });
|
||||||
|
},
|
||||||
removeTable: async ({ undoData: { table, relationships } }) => {
|
removeTable: async ({ undoData: { table, relationships } }) => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
addTable(table, { updateHistory: false }),
|
addTable(table, { updateHistory: false }),
|
||||||
addRelationships(relationships, { updateHistory: false }),
|
addRelationships(relationships, { updateHistory: false }),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
removeTables: async ({ undoData: { tables, relationships } }) => {
|
||||||
|
await Promise.all([
|
||||||
|
addTables(tables, { updateHistory: false }),
|
||||||
|
addRelationships(relationships, { updateHistory: false }),
|
||||||
|
]);
|
||||||
|
},
|
||||||
updateTable: ({ undoData: { tableId, table } }) => {
|
updateTable: ({ undoData: { tableId, table } }) => {
|
||||||
return updateTable(tableId, table, { updateHistory: false });
|
return updateTable(tableId, table, { updateHistory: false });
|
||||||
},
|
},
|
||||||
@@ -200,7 +219,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
addTable,
|
addTable,
|
||||||
|
addTables,
|
||||||
removeTable,
|
removeTable,
|
||||||
|
removeTables,
|
||||||
updateTable,
|
updateTable,
|
||||||
updateDiagramName,
|
updateDiagramName,
|
||||||
removeField,
|
removeField,
|
||||||
|
|||||||
@@ -30,12 +30,24 @@ type RedoUndoActionAddTable = RedoUndoActionBase<
|
|||||||
{ tableId: string }
|
{ tableId: string }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
type RedoUndoActionAddTables = RedoUndoActionBase<
|
||||||
|
'addTables',
|
||||||
|
{ tables: DBTable[] },
|
||||||
|
{ tableIds: string[] }
|
||||||
|
>;
|
||||||
|
|
||||||
type RedoUndoActionRemoveTable = RedoUndoActionBase<
|
type RedoUndoActionRemoveTable = RedoUndoActionBase<
|
||||||
'removeTable',
|
'removeTable',
|
||||||
{ tableId: string },
|
{ tableId: string },
|
||||||
{ table: DBTable; relationships: DBRelationship[] }
|
{ table: DBTable; relationships: DBRelationship[] }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
type RedoUndoActionRemoveTables = RedoUndoActionBase<
|
||||||
|
'removeTables',
|
||||||
|
{ tableIds: string[] },
|
||||||
|
{ tables: DBTable[]; relationships: DBRelationship[] }
|
||||||
|
>;
|
||||||
|
|
||||||
type RedoUndoActionUpdateTablesState = RedoUndoActionBase<
|
type RedoUndoActionUpdateTablesState = RedoUndoActionBase<
|
||||||
'updateTablesState',
|
'updateTablesState',
|
||||||
{ tables: DBTable[] },
|
{ tables: DBTable[] },
|
||||||
@@ -110,7 +122,9 @@ type RedoUndoActionRemoveRelationships = RedoUndoActionBase<
|
|||||||
|
|
||||||
export type RedoUndoAction =
|
export type RedoUndoAction =
|
||||||
| RedoUndoActionAddTable
|
| RedoUndoActionAddTable
|
||||||
|
| RedoUndoActionAddTables
|
||||||
| RedoUndoActionRemoveTable
|
| RedoUndoActionRemoveTable
|
||||||
|
| RedoUndoActionRemoveTables
|
||||||
| RedoUndoActionUpdateTable
|
| RedoUndoActionUpdateTable
|
||||||
| RedoUndoActionUpdateDiagramName
|
| RedoUndoActionUpdateDiagramName
|
||||||
| RedoUndoActionUpdateTablesState
|
| RedoUndoActionUpdateTablesState
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export interface BaseAlertDialogProps {
|
|||||||
closeLabel?: string;
|
closeLabel?: string;
|
||||||
onAction?: () => void;
|
onAction?: () => void;
|
||||||
dialog?: AlertDialogProps;
|
dialog?: AlertDialogProps;
|
||||||
|
onClose?: () => void;
|
||||||
content?: React.ReactNode;
|
content?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,8 +31,15 @@ export const BaseAlertDialog: React.FC<BaseAlertDialogProps> = ({
|
|||||||
onAction,
|
onAction,
|
||||||
dialog,
|
dialog,
|
||||||
content,
|
content,
|
||||||
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const { closeAlert } = useDialog();
|
const { closeAlert } = useDialog();
|
||||||
|
|
||||||
|
const closeAlertHandler = useCallback(() => {
|
||||||
|
onClose?.();
|
||||||
|
closeAlert();
|
||||||
|
}, [onClose, closeAlert]);
|
||||||
|
|
||||||
const alertHandler = useCallback(() => {
|
const alertHandler = useCallback(() => {
|
||||||
onAction?.();
|
onAction?.();
|
||||||
closeAlert();
|
closeAlert();
|
||||||
@@ -57,7 +65,7 @@ export const BaseAlertDialog: React.FC<BaseAlertDialogProps> = ({
|
|||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
{closeLabel && (
|
{closeLabel && (
|
||||||
<AlertDialogCancel onClick={closeAlert}>
|
<AlertDialogCancel onClick={closeAlertHandler}>
|
||||||
{closeLabel}
|
{closeLabel}
|
||||||
</AlertDialogCancel>
|
</AlertDialogCancel>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { Button } from '@/components/button/button';
|
import { Button } from '@/components/button/button';
|
||||||
import {
|
import {
|
||||||
DialogClose,
|
DialogClose,
|
||||||
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
@@ -23,7 +24,6 @@ import {
|
|||||||
AvatarFallback,
|
AvatarFallback,
|
||||||
AvatarImage,
|
AvatarImage,
|
||||||
} from '@/components/avatar/avatar';
|
} from '@/components/avatar/avatar';
|
||||||
import { CreateDiagramDialogStep } from '../create-diagram-dialog-step';
|
|
||||||
import { SSMSInfo } from './ssms-info/ssms-info';
|
import { SSMSInfo } from './ssms-info/ssms-info';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/tabs/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@/components/tabs/tabs';
|
||||||
@@ -32,10 +32,15 @@ import {
|
|||||||
databaseClientToLabelMap,
|
databaseClientToLabelMap,
|
||||||
databaseTypeToClientsMap,
|
databaseTypeToClientsMap,
|
||||||
} from '@/lib/domain/database-clients';
|
} from '@/lib/domain/database-clients';
|
||||||
|
import { isDatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
|
||||||
|
|
||||||
export interface ImportDatabaseStepProps {
|
const errorScriptOutputMessage =
|
||||||
setStep: React.Dispatch<React.SetStateAction<CreateDiagramDialogStep>>;
|
'Invalid JSON. Please correct it or contact us at chartdb.io@gmail.com for help.';
|
||||||
createNewDiagram: () => void;
|
|
||||||
|
export interface ImportDatabaseProps {
|
||||||
|
goBack?: () => void;
|
||||||
|
onImport: () => void;
|
||||||
|
onCreateEmptyDiagram?: () => void;
|
||||||
scriptResult: string;
|
scriptResult: string;
|
||||||
setScriptResult: React.Dispatch<React.SetStateAction<string>>;
|
setScriptResult: React.Dispatch<React.SetStateAction<string>>;
|
||||||
databaseType: DatabaseType;
|
databaseType: DatabaseType;
|
||||||
@@ -43,24 +48,54 @@ export interface ImportDatabaseStepProps {
|
|||||||
setDatabaseEdition: React.Dispatch<
|
setDatabaseEdition: React.Dispatch<
|
||||||
React.SetStateAction<DatabaseEdition | undefined>
|
React.SetStateAction<DatabaseEdition | undefined>
|
||||||
>;
|
>;
|
||||||
errorMessage: string;
|
keepDialogAfterImport?: boolean;
|
||||||
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImportDatabaseStep: React.FC<ImportDatabaseStepProps> = ({
|
export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||||
setScriptResult,
|
setScriptResult,
|
||||||
setStep,
|
goBack,
|
||||||
scriptResult,
|
scriptResult,
|
||||||
createNewDiagram,
|
onImport,
|
||||||
|
onCreateEmptyDiagram,
|
||||||
databaseType,
|
databaseType,
|
||||||
databaseEdition,
|
databaseEdition,
|
||||||
setDatabaseEdition,
|
setDatabaseEdition,
|
||||||
errorMessage,
|
keepDialogAfterImport,
|
||||||
|
title,
|
||||||
}) => {
|
}) => {
|
||||||
const databaseClients = databaseTypeToClientsMap[databaseType];
|
const databaseClients = databaseTypeToClientsMap[databaseType];
|
||||||
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
const [databaseClient, setDatabaseClient] = useState<
|
const [databaseClient, setDatabaseClient] = useState<
|
||||||
DatabaseClient | undefined
|
DatabaseClient | undefined
|
||||||
>();
|
>();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (scriptResult.trim().length === 0) {
|
||||||
|
setErrorMessage('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedResult = JSON.parse(scriptResult);
|
||||||
|
|
||||||
|
if (isDatabaseMetadata(parsedResult)) {
|
||||||
|
setErrorMessage('');
|
||||||
|
} else {
|
||||||
|
setErrorMessage(errorScriptOutputMessage);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setErrorMessage(errorScriptOutputMessage);
|
||||||
|
}
|
||||||
|
}, [scriptResult]);
|
||||||
|
|
||||||
|
const handleImport = useCallback(() => {
|
||||||
|
if (errorMessage.length === 0 && scriptResult.trim().length !== 0) {
|
||||||
|
onImport();
|
||||||
|
}
|
||||||
|
}, [errorMessage.length, onImport, scriptResult]);
|
||||||
|
|
||||||
const handleInputChange = useCallback(
|
const handleInputChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
const inputValue = e.target.value;
|
const inputValue = e.target.value;
|
||||||
@@ -72,12 +107,11 @@ export const ImportDatabaseStep: React.FC<ImportDatabaseStepProps> = ({
|
|||||||
const renderHeader = useCallback(() => {
|
const renderHeader = useCallback(() => {
|
||||||
return (
|
return (
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
{t('new_diagram_dialog.import_database.title')}
|
<DialogDescription className="hidden" />
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
);
|
);
|
||||||
}, [t]);
|
}, [title]);
|
||||||
|
|
||||||
const renderContent = useCallback(() => {
|
const renderContent = useCallback(() => {
|
||||||
return (
|
return (
|
||||||
@@ -253,27 +287,30 @@ export const ImportDatabaseStep: React.FC<ImportDatabaseStepProps> = ({
|
|||||||
return (
|
return (
|
||||||
<DialogFooter className="mt-4 flex !justify-between gap-2">
|
<DialogFooter className="mt-4 flex !justify-between gap-2">
|
||||||
<div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2">
|
<div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2">
|
||||||
<Button
|
{goBack && (
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() =>
|
|
||||||
setStep(CreateDiagramDialogStep.SELECT_DATABASE)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('new_diagram_dialog.back')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:space-x-2">
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="secondary"
|
||||||
onClick={createNewDiagram}
|
onClick={goBack}
|
||||||
>
|
>
|
||||||
{t('new_diagram_dialog.empty_diagram')}
|
{t('new_diagram_dialog.back')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
)}
|
||||||
<DialogClose asChild>
|
</div>
|
||||||
|
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:space-x-2">
|
||||||
|
{onCreateEmptyDiagram && (
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={onCreateEmptyDiagram}
|
||||||
|
>
|
||||||
|
{t('new_diagram_dialog.empty_diagram')}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{keepDialogAfterImport ? (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="default"
|
variant="default"
|
||||||
@@ -281,15 +318,37 @@ export const ImportDatabaseStep: React.FC<ImportDatabaseStepProps> = ({
|
|||||||
scriptResult.trim().length === 0 ||
|
scriptResult.trim().length === 0 ||
|
||||||
errorMessage.length > 0
|
errorMessage.length > 0
|
||||||
}
|
}
|
||||||
onClick={createNewDiagram}
|
onClick={handleImport}
|
||||||
>
|
>
|
||||||
{t('new_diagram_dialog.import')}
|
{t('new_diagram_dialog.import')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
) : (
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="default"
|
||||||
|
disabled={
|
||||||
|
scriptResult.trim().length === 0 ||
|
||||||
|
errorMessage.length > 0
|
||||||
|
}
|
||||||
|
onClick={handleImport}
|
||||||
|
>
|
||||||
|
{t('new_diagram_dialog.import')}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
);
|
);
|
||||||
}, [createNewDiagram, errorMessage.length, scriptResult, setStep, t]);
|
}, [
|
||||||
|
handleImport,
|
||||||
|
keepDialogAfterImport,
|
||||||
|
onCreateEmptyDiagram,
|
||||||
|
errorMessage.length,
|
||||||
|
scriptResult,
|
||||||
|
goBack,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -8,19 +8,16 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useConfig } from '@/hooks/use-config';
|
import { useConfig } from '@/hooks/use-config';
|
||||||
import {
|
import {
|
||||||
DatabaseMetadata,
|
DatabaseMetadata,
|
||||||
isDatabaseMetadata,
|
|
||||||
loadDatabaseMetadata,
|
loadDatabaseMetadata,
|
||||||
} from '@/lib/data/import-metadata/metadata-types/database-metadata';
|
} from '@/lib/data/import-metadata/metadata-types/database-metadata';
|
||||||
import { generateDiagramId } from '@/lib/utils';
|
import { generateDiagramId } from '@/lib/utils';
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { useDialog } from '@/hooks/use-dialog';
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
import { DatabaseEdition } from '@/lib/domain/database-edition';
|
import { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||||
import { SelectDatabaseStep } from './select-database-step/select-database-step';
|
import { SelectDatabase } from './select-database/select-database';
|
||||||
import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
|
import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
|
||||||
import { ImportDatabaseStep } from './import-database-step/import-database-step';
|
import { ImportDatabase } from '../common/import-database/import-database';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
const errorScriptOutputMessage =
|
|
||||||
'Invalid JSON. Please correct it or contact us at chartdb.io@gmail.com for help.';
|
|
||||||
|
|
||||||
export interface CreateDiagramDialogProps {
|
export interface CreateDiagramDialogProps {
|
||||||
dialog: DialogProps;
|
dialog: DialogProps;
|
||||||
@@ -30,13 +27,13 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
dialog,
|
dialog,
|
||||||
}) => {
|
}) => {
|
||||||
const { diagramId } = useChartDB();
|
const { diagramId } = useChartDB();
|
||||||
|
const { t } = useTranslation();
|
||||||
const [databaseType, setDatabaseType] = useState<DatabaseType>(
|
const [databaseType, setDatabaseType] = useState<DatabaseType>(
|
||||||
DatabaseType.GENERIC
|
DatabaseType.GENERIC
|
||||||
);
|
);
|
||||||
const { closeCreateDiagramDialog } = useDialog();
|
const { closeCreateDiagramDialog } = useDialog();
|
||||||
const { updateConfig } = useConfig();
|
const { updateConfig } = useConfig();
|
||||||
const [scriptResult, setScriptResult] = useState('');
|
const [scriptResult, setScriptResult] = useState('');
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
|
||||||
const [databaseEdition, setDatabaseEdition] = useState<
|
const [databaseEdition, setDatabaseEdition] = useState<
|
||||||
DatabaseEdition | undefined
|
DatabaseEdition | undefined
|
||||||
>();
|
>();
|
||||||
@@ -60,57 +57,23 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
setDatabaseType(DatabaseType.GENERIC);
|
setDatabaseType(DatabaseType.GENERIC);
|
||||||
setDatabaseEdition(undefined);
|
setDatabaseEdition(undefined);
|
||||||
setScriptResult('');
|
setScriptResult('');
|
||||||
setErrorMessage('');
|
|
||||||
}, [dialog.open]);
|
}, [dialog.open]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (scriptResult.trim().length === 0) {
|
|
||||||
setErrorMessage('');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsedResult = JSON.parse(scriptResult);
|
|
||||||
|
|
||||||
if (isDatabaseMetadata(parsedResult)) {
|
|
||||||
setErrorMessage('');
|
|
||||||
} else {
|
|
||||||
setErrorMessage(errorScriptOutputMessage);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setErrorMessage(errorScriptOutputMessage);
|
|
||||||
}
|
|
||||||
}, [scriptResult]);
|
|
||||||
|
|
||||||
const hasExistingDiagram = (diagramId ?? '').trim().length !== 0;
|
const hasExistingDiagram = (diagramId ?? '').trim().length !== 0;
|
||||||
|
|
||||||
const createNewDiagram = useCallback(async () => {
|
const importNewDiagram = useCallback(async () => {
|
||||||
let diagram: Diagram = {
|
const databaseMetadata: DatabaseMetadata =
|
||||||
id: generateDiagramId(),
|
loadDatabaseMetadata(scriptResult);
|
||||||
name: `Diagram ${diagramNumber}`,
|
|
||||||
databaseType: databaseType ?? DatabaseType.GENERIC,
|
const diagram = loadFromDatabaseMetadata({
|
||||||
|
databaseType,
|
||||||
|
databaseMetadata,
|
||||||
|
diagramNumber,
|
||||||
databaseEdition:
|
databaseEdition:
|
||||||
databaseEdition?.trim().length === 0
|
databaseEdition?.trim().length === 0
|
||||||
? undefined
|
? undefined
|
||||||
: databaseEdition,
|
: databaseEdition,
|
||||||
createdAt: new Date(),
|
});
|
||||||
updatedAt: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (errorMessage.length === 0 && scriptResult.trim().length !== 0) {
|
|
||||||
const databaseMetadata: DatabaseMetadata =
|
|
||||||
loadDatabaseMetadata(scriptResult);
|
|
||||||
|
|
||||||
diagram = loadFromDatabaseMetadata({
|
|
||||||
databaseType,
|
|
||||||
databaseMetadata,
|
|
||||||
diagramNumber,
|
|
||||||
databaseEdition:
|
|
||||||
databaseEdition?.trim().length === 0
|
|
||||||
? undefined
|
|
||||||
: databaseEdition,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await addDiagram({ diagram });
|
await addDiagram({ diagram });
|
||||||
await updateConfig({ defaultDiagramId: diagram.id });
|
await updateConfig({ defaultDiagramId: diagram.id });
|
||||||
@@ -125,7 +88,33 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
updateConfig,
|
updateConfig,
|
||||||
scriptResult,
|
scriptResult,
|
||||||
diagramNumber,
|
diagramNumber,
|
||||||
errorMessage,
|
]);
|
||||||
|
|
||||||
|
const createEmptyDiagram = useCallback(async () => {
|
||||||
|
const diagram: Diagram = {
|
||||||
|
id: generateDiagramId(),
|
||||||
|
name: `Diagram ${diagramNumber}`,
|
||||||
|
databaseType: databaseType ?? DatabaseType.GENERIC,
|
||||||
|
databaseEdition:
|
||||||
|
databaseEdition?.trim().length === 0
|
||||||
|
? undefined
|
||||||
|
: databaseEdition,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await addDiagram({ diagram });
|
||||||
|
await updateConfig({ defaultDiagramId: diagram.id });
|
||||||
|
closeCreateDiagramDialog();
|
||||||
|
navigate(`/diagrams/${diagram.id}`);
|
||||||
|
}, [
|
||||||
|
databaseType,
|
||||||
|
addDiagram,
|
||||||
|
databaseEdition,
|
||||||
|
closeCreateDiagramDialog,
|
||||||
|
navigate,
|
||||||
|
updateConfig,
|
||||||
|
diagramNumber,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -146,23 +135,28 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
showClose={hasExistingDiagram}
|
showClose={hasExistingDiagram}
|
||||||
>
|
>
|
||||||
{step === CreateDiagramDialogStep.SELECT_DATABASE ? (
|
{step === CreateDiagramDialogStep.SELECT_DATABASE ? (
|
||||||
<SelectDatabaseStep
|
<SelectDatabase
|
||||||
createNewDiagram={createNewDiagram}
|
createNewDiagram={createEmptyDiagram}
|
||||||
databaseType={databaseType}
|
databaseType={databaseType}
|
||||||
hasExistingDiagram={hasExistingDiagram}
|
hasExistingDiagram={hasExistingDiagram}
|
||||||
setDatabaseType={setDatabaseType}
|
setDatabaseType={setDatabaseType}
|
||||||
setStep={setStep}
|
onContinue={() =>
|
||||||
|
setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ImportDatabaseStep
|
<ImportDatabase
|
||||||
createNewDiagram={createNewDiagram}
|
onImport={importNewDiagram}
|
||||||
|
onCreateEmptyDiagram={createEmptyDiagram}
|
||||||
databaseEdition={databaseEdition}
|
databaseEdition={databaseEdition}
|
||||||
databaseType={databaseType}
|
databaseType={databaseType}
|
||||||
errorMessage={errorMessage}
|
|
||||||
scriptResult={scriptResult}
|
scriptResult={scriptResult}
|
||||||
setDatabaseEdition={setDatabaseEdition}
|
setDatabaseEdition={setDatabaseEdition}
|
||||||
setStep={setStep}
|
goBack={() =>
|
||||||
|
setStep(CreateDiagramDialogStep.SELECT_DATABASE)
|
||||||
|
}
|
||||||
setScriptResult={setScriptResult}
|
setScriptResult={setScriptResult}
|
||||||
|
title={t('new_diagram_dialog.import_database.title')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -13,20 +13,19 @@ import { DatabaseType } from '@/lib/domain/database-type';
|
|||||||
import { databaseTypeToLabelMap, getDatabaseLogo } from '@/lib/databases';
|
import { databaseTypeToLabelMap, getDatabaseLogo } from '@/lib/databases';
|
||||||
import { Link } from '@/components/link/link';
|
import { Link } from '@/components/link/link';
|
||||||
import { LayoutGrid } from 'lucide-react';
|
import { LayoutGrid } from 'lucide-react';
|
||||||
import { CreateDiagramDialogStep } from '../create-diagram-dialog-step';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
|
||||||
export interface SelectDatabaseStepProps {
|
export interface SelectDatabaseProps {
|
||||||
setStep: React.Dispatch<React.SetStateAction<CreateDiagramDialogStep>>;
|
onContinue: () => void;
|
||||||
databaseType: DatabaseType;
|
databaseType: DatabaseType;
|
||||||
setDatabaseType: React.Dispatch<React.SetStateAction<DatabaseType>>;
|
setDatabaseType: React.Dispatch<React.SetStateAction<DatabaseType>>;
|
||||||
hasExistingDiagram: boolean;
|
hasExistingDiagram: boolean;
|
||||||
createNewDiagram: () => void;
|
createNewDiagram: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SelectDatabaseStep: React.FC<SelectDatabaseStepProps> = ({
|
export const SelectDatabase: React.FC<SelectDatabaseProps> = ({
|
||||||
setStep,
|
onContinue,
|
||||||
databaseType,
|
databaseType,
|
||||||
setDatabaseType,
|
setDatabaseType,
|
||||||
hasExistingDiagram,
|
hasExistingDiagram,
|
||||||
@@ -98,7 +97,7 @@ export const SelectDatabaseStep: React.FC<SelectDatabaseStepProps> = ({
|
|||||||
setDatabaseType(DatabaseType.GENERIC);
|
setDatabaseType(DatabaseType.GENERIC);
|
||||||
} else {
|
} else {
|
||||||
setDatabaseType(value);
|
setDatabaseType(value);
|
||||||
setStep(CreateDiagramDialogStep.IMPORT_DATABASE);
|
onContinue();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
type="single"
|
type="single"
|
||||||
@@ -118,7 +117,7 @@ export const SelectDatabaseStep: React.FC<SelectDatabaseStepProps> = ({
|
|||||||
renderDatabaseOption,
|
renderDatabaseOption,
|
||||||
renderExamplesOption,
|
renderExamplesOption,
|
||||||
setDatabaseType,
|
setDatabaseType,
|
||||||
setStep,
|
onContinue,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const renderFooter = useCallback(() => {
|
const renderFooter = useCallback(() => {
|
||||||
@@ -145,16 +144,14 @@ export const SelectDatabaseStep: React.FC<SelectDatabaseStepProps> = ({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="default"
|
variant="default"
|
||||||
disabled={databaseType === DatabaseType.GENERIC}
|
disabled={databaseType === DatabaseType.GENERIC}
|
||||||
onClick={() =>
|
onClick={onContinue}
|
||||||
setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{t('new_diagram_dialog.continue')}
|
{t('new_diagram_dialog.continue')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
);
|
);
|
||||||
}, [createNewDiagram, databaseType, hasExistingDiagram, setStep, t]);
|
}, [createNewDiagram, databaseType, hasExistingDiagram, onContinue, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
346
src/dialogs/import-database-dialog/import-database-dialog.tsx
Normal file
346
src/dialogs/import-database-dialog/import-database-dialog.tsx
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
import { Dialog, DialogContent } from '@/components/dialog/dialog';
|
||||||
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
|
import { DialogProps } from '@radix-ui/react-dialog';
|
||||||
|
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { ImportDatabase } from '../common/import-database/import-database';
|
||||||
|
import { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||||
|
import {
|
||||||
|
DatabaseMetadata,
|
||||||
|
loadDatabaseMetadata,
|
||||||
|
} from '@/lib/data/import-metadata/metadata-types/database-metadata';
|
||||||
|
import { loadFromDatabaseMetadata } from '@/lib/domain/diagram';
|
||||||
|
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';
|
||||||
|
|
||||||
|
export interface ImportDatabaseDialogProps {
|
||||||
|
dialog: DialogProps;
|
||||||
|
databaseType: DatabaseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
|
||||||
|
dialog,
|
||||||
|
databaseType,
|
||||||
|
}) => {
|
||||||
|
const { closeImportDatabaseDialog, showAlert } = useDialog();
|
||||||
|
const {
|
||||||
|
tables,
|
||||||
|
relationships,
|
||||||
|
removeTables,
|
||||||
|
removeRelationships,
|
||||||
|
addTables,
|
||||||
|
addRelationships,
|
||||||
|
diagramName,
|
||||||
|
} = useChartDB();
|
||||||
|
const [scriptResult, setScriptResult] = useState('');
|
||||||
|
const { resetRedoStack, resetUndoStack } = useRedoUndoStack();
|
||||||
|
const { setNodes } = useReactFlow();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [databaseEdition, setDatabaseEdition] = useState<
|
||||||
|
DatabaseEdition | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dialog.open) return;
|
||||||
|
setDatabaseEdition(undefined);
|
||||||
|
setScriptResult('');
|
||||||
|
}, [dialog.open]);
|
||||||
|
|
||||||
|
const importDatabase = useCallback(async () => {
|
||||||
|
const databaseMetadata: DatabaseMetadata =
|
||||||
|
loadDatabaseMetadata(scriptResult);
|
||||||
|
|
||||||
|
const diagram = loadFromDatabaseMetadata({
|
||||||
|
databaseType,
|
||||||
|
databaseMetadata,
|
||||||
|
databaseEdition:
|
||||||
|
databaseEdition?.trim().length === 0
|
||||||
|
? undefined
|
||||||
|
: databaseEdition,
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setNodes((nodes) =>
|
||||||
|
nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
selected:
|
||||||
|
diagram.tables?.some((table) => table.id === node.id) ??
|
||||||
|
false,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
resetRedoStack();
|
||||||
|
resetUndoStack();
|
||||||
|
|
||||||
|
closeImportDatabaseDialog();
|
||||||
|
}, [
|
||||||
|
databaseEdition,
|
||||||
|
databaseType,
|
||||||
|
scriptResult,
|
||||||
|
tables,
|
||||||
|
addRelationships,
|
||||||
|
addTables,
|
||||||
|
closeImportDatabaseDialog,
|
||||||
|
relationships,
|
||||||
|
removeRelationships,
|
||||||
|
removeTables,
|
||||||
|
resetRedoStack,
|
||||||
|
resetUndoStack,
|
||||||
|
showAlert,
|
||||||
|
setNodes,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
{...dialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
closeImportDatabaseDialog();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent
|
||||||
|
className="flex max-h-[90vh] w-[90vw] flex-col overflow-y-auto md:overflow-visible xl:min-w-[45vw]"
|
||||||
|
showClose
|
||||||
|
>
|
||||||
|
<ImportDatabase
|
||||||
|
databaseType={databaseType}
|
||||||
|
databaseEdition={databaseEdition}
|
||||||
|
setDatabaseEdition={setDatabaseEdition}
|
||||||
|
onImport={importDatabase}
|
||||||
|
scriptResult={scriptResult}
|
||||||
|
setScriptResult={setScriptResult}
|
||||||
|
keepDialogAfterImport
|
||||||
|
title={t('import_database_dialog.title', { diagramName })}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ export const en = {
|
|||||||
new: 'New',
|
new: 'New',
|
||||||
open: 'Open',
|
open: 'Open',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
|
import_database: 'Import Database',
|
||||||
export_sql: 'Export SQL',
|
export_sql: 'Export SQL',
|
||||||
export_as: 'Export as',
|
export_as: 'Export as',
|
||||||
delete_diagram: 'Delete Diagram',
|
delete_diagram: 'Delete Diagram',
|
||||||
@@ -240,6 +241,25 @@ export const en = {
|
|||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
import_database_dialog: {
|
||||||
|
title: 'Import to Current Diagram',
|
||||||
|
override_alert: {
|
||||||
|
title: 'Import Database',
|
||||||
|
content: {
|
||||||
|
alert: 'Importing this diagram will affect existing tables and relationships.',
|
||||||
|
new_tables:
|
||||||
|
'<bold>{{newTablesNumber}}</bold> new tables will be added.',
|
||||||
|
new_relationships:
|
||||||
|
'<bold>{{newRelationshipsNumber}}</bold> new relationships will be created.',
|
||||||
|
tables_override:
|
||||||
|
'<bold>{{tablesOverrideNumber}}</bold> tables will be overwritten.',
|
||||||
|
proceed: 'Do you want to proceed?',
|
||||||
|
},
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'One to One',
|
one_to_one: 'One to One',
|
||||||
one_to_many: 'One to Many',
|
one_to_many: 'One to Many',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const es: LanguageTranslation = {
|
|||||||
new: 'Nuevo',
|
new: 'Nuevo',
|
||||||
open: 'Abrir',
|
open: 'Abrir',
|
||||||
save: 'Guardar',
|
save: 'Guardar',
|
||||||
|
import_database: 'Importar Base de Datos',
|
||||||
export_sql: 'Exportar SQL',
|
export_sql: 'Exportar SQL',
|
||||||
export_as: 'Exportar como',
|
export_as: 'Exportar como',
|
||||||
delete_diagram: 'Eliminar Diagrama',
|
delete_diagram: 'Eliminar Diagrama',
|
||||||
@@ -241,6 +242,25 @@ export const es: LanguageTranslation = {
|
|||||||
title: 'Crear Relación',
|
title: 'Crear Relación',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
import_database_dialog: {
|
||||||
|
title: 'Importar a Diagrama Actual',
|
||||||
|
override_alert: {
|
||||||
|
title: 'Importar Base de Datos',
|
||||||
|
content: {
|
||||||
|
alert: 'Importar este diagrama afectará las tablas y relaciones existentes.',
|
||||||
|
new_tables:
|
||||||
|
'<bold>{{newTablesNumber}}</bold> nuevas tablas se agregarán.',
|
||||||
|
new_relationships:
|
||||||
|
'<bold>{{newRelationshipsNumber}}</bold> nuevas relaciones se crearán.',
|
||||||
|
tables_override:
|
||||||
|
'<bold>{{tablesOverrideNumber}}</bold> tablas se sobrescribirán.',
|
||||||
|
proceed: '¿Deseas continuar?',
|
||||||
|
},
|
||||||
|
import: 'Importar',
|
||||||
|
cancel: 'Cancelar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Uno a Uno',
|
one_to_one: 'Uno a Uno',
|
||||||
one_to_many: 'Uno a Muchos',
|
one_to_many: 'Uno a Muchos',
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const loadFromDatabaseMetadata = ({
|
|||||||
}: {
|
}: {
|
||||||
databaseType: DatabaseType;
|
databaseType: DatabaseType;
|
||||||
databaseMetadata: DatabaseMetadata;
|
databaseMetadata: DatabaseMetadata;
|
||||||
diagramNumber: number;
|
diagramNumber?: number;
|
||||||
databaseEdition?: DatabaseEdition;
|
databaseEdition?: DatabaseEdition;
|
||||||
}): Diagram => {
|
}): Diagram => {
|
||||||
const {
|
const {
|
||||||
@@ -71,9 +71,11 @@ export const loadFromDatabaseMetadata = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: generateDiagramId(),
|
id: generateDiagramId(),
|
||||||
name:
|
name: databaseMetadata.database_name
|
||||||
`${databaseMetadata.database_name}-db` ||
|
? `${databaseMetadata.database_name}-db`
|
||||||
`Diagram ${diagramNumber}`,
|
: diagramNumber
|
||||||
|
? `Diagram ${diagramNumber}`
|
||||||
|
: 'New Diagram',
|
||||||
databaseType: databaseType ?? DatabaseType.GENERIC,
|
databaseType: databaseType ?? DatabaseType.GENERIC,
|
||||||
databaseEdition,
|
databaseEdition,
|
||||||
tables: sortedTables,
|
tables: sortedTables,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
|
|||||||
openCreateDiagramDialog,
|
openCreateDiagramDialog,
|
||||||
openOpenDiagramDialog,
|
openOpenDiagramDialog,
|
||||||
openExportSQLDialog,
|
openExportSQLDialog,
|
||||||
|
openImportDatabaseDialog,
|
||||||
showAlert,
|
showAlert,
|
||||||
} = useDialog();
|
} = useDialog();
|
||||||
const { setTheme, theme } = useTheme();
|
const { setTheme, theme } = useTheme();
|
||||||
@@ -352,6 +353,72 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
|
|||||||
</MenubarShortcut>
|
</MenubarShortcut>
|
||||||
</MenubarItem>
|
</MenubarItem>
|
||||||
<MenubarSeparator />
|
<MenubarSeparator />
|
||||||
|
<MenubarSub>
|
||||||
|
<MenubarSubTrigger>
|
||||||
|
{t('menu.file.import_database')}
|
||||||
|
</MenubarSubTrigger>
|
||||||
|
<MenubarSubContent>
|
||||||
|
<MenubarItem
|
||||||
|
onClick={() =>
|
||||||
|
openImportDatabaseDialog({
|
||||||
|
databaseType:
|
||||||
|
DatabaseType.POSTGRESQL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
databaseTypeToLabelMap[
|
||||||
|
'postgresql'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem
|
||||||
|
onClick={() =>
|
||||||
|
openImportDatabaseDialog({
|
||||||
|
databaseType:
|
||||||
|
DatabaseType.MYSQL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
</MenubarSubContent>
|
||||||
|
</MenubarSub>
|
||||||
|
<MenubarSeparator />
|
||||||
<MenubarSub>
|
<MenubarSub>
|
||||||
<MenubarSubTrigger>
|
<MenubarSubTrigger>
|
||||||
{t('menu.file.export_sql')}
|
{t('menu.file.export_sql')}
|
||||||
|
|||||||
Reference in New Issue
Block a user