delete diagram + modal

This commit is contained in:
Guy Ben-Aharon
2024-08-23 03:55:02 +03:00
parent c43e8d7f30
commit 91577eccde
7 changed files with 396 additions and 288 deletions

View File

@@ -24,6 +24,7 @@ export interface ChartDBContext {
loadDiagram: (diagramId: string) => Promise<Diagram | undefined>;
updateDiagramUpdatedAt: () => Promise<void>;
clearDiagramData: () => Promise<void>;
deleteDiagram: () => Promise<void>;
// Database type operations
updateDatabaseType: (databaseType: DatabaseType) => Promise<void>;
@@ -140,12 +141,12 @@ export const chartDBContext = createContext<ChartDBContext>({
updateDiagramUpdatedAt: emptyFn,
loadDiagram: emptyFn,
clearDiagramData: emptyFn,
deleteDiagram: emptyFn,
// Database type operations
updateDatabaseType: emptyFn,
// Table operations
// updateTables: emptyFn,
createTable: emptyFn,
getTable: emptyFn,
addTable: emptyFn,

View File

@@ -10,11 +10,13 @@ import { DBRelationship } from '@/lib/domain/db-relationship';
import { useStorage } from '@/hooks/use-storage';
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
import { Diagram } from '@/lib/domain/diagram';
import { useNavigate } from 'react-router-dom';
export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const db = useStorage();
const navigate = useNavigate();
const { addUndoAction, resetRedoStack, resetUndoStack } =
useRedoUndoStack();
const [diagramId, setDiagramId] = useState('');
@@ -65,6 +67,29 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
]);
}, [db, diagramId, resetRedoStack, resetUndoStack]);
const deleteDiagram: ChartDBContext['deleteDiagram'] =
useCallback(async () => {
const [config] = await Promise.all([
db.getConfig(),
db.deleteDiagramTables(diagramId),
db.deleteDiagramRelationships(diagramId),
db.deleteDiagram(diagramId),
]);
if (config?.defaultDiagramId === diagramId) {
const diagrams = await db.listDiagrams();
if (diagrams.length > 0) {
const defaultDiagramId = diagrams[0].id;
await db.updateConfig({ defaultDiagramId });
navigate(`/diagrams/${defaultDiagramId}`);
} else {
await db.updateConfig({ defaultDiagramId: '' });
navigate('/diagrams');
}
}
}, [db, diagramId, navigate]);
const updateDiagramUpdatedAt: ChartDBContext['updateDiagramUpdatedAt'] =
useCallback(async () => {
const updatedAt = new Date();
@@ -947,6 +972,7 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
loadDiagram,
updateDatabaseType,
clearDiagramData,
deleteDiagram,
updateDiagramUpdatedAt,
createTable,
addTable,

View File

@@ -14,6 +14,16 @@ export interface DialogContext {
// Export SQL dialog
openExportSQLDialog: (params: { targetDatabaseType: DatabaseType }) => void;
closeExportSQLDialog: () => void;
// Alert dialog
showAlert: (params: {
onAction: () => void;
title: string;
description: string;
actionLabel: string;
closeLabel: string;
}) => void;
closeAlert: () => void;
}
export const dialogContext = createContext<DialogContext>({
@@ -23,4 +33,6 @@ export const dialogContext = createContext<DialogContext>({
closeOpenDiagramDialog: emptyFn,
openExportSQLDialog: emptyFn,
closeExportSQLDialog: emptyFn,
closeAlert: emptyFn,
showAlert: emptyFn,
});

View File

@@ -4,6 +4,7 @@ import { CreateDiagramDialog } from '@/dialogs/create-diagram-dialog/create-diag
import { OpenDiagramDialog } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
import { ExportSQLDialog } from '@/dialogs/export-sql-dialog/export-sql-dialog';
import { DatabaseType } from '@/lib/domain/database-type';
import { BaseAlertDialog } from '@/dialogs/base-alert-dialog/base-alert-dialog';
export const DialogProvider: React.FC<React.PropsWithChildren> = ({
children,
@@ -14,6 +15,20 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
const [openExportSQLDialogParams, setOpenExportSQLDialogParams] = useState<{
targetDatabaseType: DatabaseType;
}>({ targetDatabaseType: DatabaseType.GENERIC });
const [showAlert, setShowAlert] = useState(false);
const [alertParams, setAlertParams] = useState<{
onAction: () => void;
title: string;
description: string;
actionLabel: string;
closeLabel: string;
}>({
onAction: () => {},
title: '',
description: '',
actionLabel: '',
closeLabel: '',
});
const openExportSQLDialogHandler: DialogContext['openExportSQLDialog'] =
useCallback(
@@ -24,6 +39,24 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
[setOpenExportSQLDialog]
);
const showAlertHandler: DialogContext['showAlert'] = useCallback(
({ onAction, title, description, actionLabel, closeLabel }) => {
setAlertParams({
onAction,
title,
description,
actionLabel,
closeLabel,
});
setShowAlert(true);
},
[setShowAlert, setAlertParams]
);
const closeAlertHandler = useCallback(() => {
setShowAlert(false);
}, [setShowAlert]);
return (
<dialogContext.Provider
value={{
@@ -33,6 +66,8 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
openExportSQLDialog: openExportSQLDialogHandler,
closeExportSQLDialog: () => setOpenExportSQLDialog(false),
showAlert: showAlertHandler,
closeAlert: closeAlertHandler,
}}
>
{children}
@@ -42,6 +77,7 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
dialog={{ open: openExportSQLDialog }}
{...openExportSQLDialogParams}
/>
<BaseAlertDialog dialog={{ open: showAlert }} {...alertParams} />
</dialogContext.Provider>
);
};

View File

@@ -0,0 +1,64 @@
import React, { useCallback } from 'react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/alert-dialog/alert-dialog';
import { AlertDialogProps } from '@radix-ui/react-alert-dialog';
import { useDialog } from '@/hooks/use-dialog';
export interface BaseAlertDialogProps {
title: string;
description: string;
actionLabel: string;
closeLabel: string;
onAction: () => void;
dialog: AlertDialogProps;
}
export const BaseAlertDialog: React.FC<BaseAlertDialogProps> = ({
title,
description,
actionLabel,
closeLabel,
onAction,
dialog,
}) => {
const { closeAlert } = useDialog();
const alertHandler = useCallback(() => {
onAction();
closeAlert();
}, [onAction, closeAlert]);
return (
<AlertDialog
{...dialog}
onOpenChange={(open) => {
if (!open) {
closeAlert();
}
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>
{description}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={closeAlert}>
{closeLabel}
</AlertDialogCancel>
<AlertDialogAction onClick={alertHandler}>
{actionLabel}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -33,6 +33,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
dialog,
}) => {
const { closeOpenDiagramDialog } = useDialog();
const { updateConfig } = useStorage();
const navigate = useNavigate();
const { listDiagrams } = useStorage();
const [diagrams, setDiagrams] = useState<Diagram[]>([]);
@@ -57,7 +58,8 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
}, [listDiagrams, setDiagrams, dialog.open]);
const openDiagram = (diagramId: string) => {
if (selectedDiagramId) {
if (diagramId) {
updateConfig({ defaultDiagramId: diagramId });
navigate(`/diagrams/${diagramId}`);
}
};

View File

@@ -32,28 +32,23 @@ import {
databaseTypeToLabelMap,
} from '@/lib/databases';
import { DatabaseType } from '@/lib/domain/database-type';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/alert-dialog/alert-dialog';
export interface TopNavbarProps {}
export const TopNavbar: React.FC<TopNavbarProps> = () => {
const { diagramName, updateDiagramName, currentDiagram, clearDiagramData } =
useChartDB();
const {
diagramName,
updateDiagramName,
currentDiagram,
clearDiagramData,
deleteDiagram,
} = useChartDB();
const {
openCreateDiagramDialog,
openOpenDiagramDialog,
openExportSQLDialog,
showAlert,
} = useDialog();
const [showClearAlert, setShowClearAlert] = useState(false);
const [editMode, setEditMode] = useState(false);
const { exportImage } = useExportImage();
const [editedDiagramName, setEditedDiagramName] =
@@ -91,11 +86,6 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
setEditMode(true);
};
const clearDiagramDataHandler = useCallback(async () => {
setShowClearAlert(false);
await clearDiagramData();
}, [clearDiagramData]);
const exportPNG = useCallback(() => {
exportImage('png');
}, [exportImage]);
@@ -120,279 +110,256 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
}, []);
return (
<>
<nav className="flex flex-row items-center justify-between px-4 h-12 border-b">
<div className="flex flex-1 justify-start gap-x-3">
<div className="flex font-primary items-center">
<a
href="https://chartdb.io"
target="_blank"
className="cursor-pointer"
rel="noreferrer"
>
<img
src={ChartDBLogo}
alt="chartDB"
className="h-4 max-w-fit"
/>
</a>
</div>
<div>
<Menubar className="border-none shadow-none">
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={createNewDiagram}>
New
<MenubarShortcut>T</MenubarShortcut>
</MenubarItem>
<MenubarItem onClick={openDiagram}>
Open
</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
Export SQL
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem
onClick={() =>
openExportSQLDialog({
targetDatabaseType:
DatabaseType.GENERIC,
})
}
>
{
databaseTypeToLabelMap[
'generic'
]
}
</MenubarItem>
<MenubarItem
onClick={() =>
openExportSQLDialog({
targetDatabaseType:
DatabaseType.POSTGRESQL,
})
}
>
{
databaseTypeToLabelMap[
'postgresql'
]
}
</MenubarItem>
<MenubarItem
onClick={() =>
openExportSQLDialog({
targetDatabaseType:
DatabaseType.MYSQL,
})
}
>
{
databaseTypeToLabelMap[
'mysql'
]
}
</MenubarItem>
<MenubarItem
onClick={() =>
openExportSQLDialog({
targetDatabaseType:
DatabaseType.SQL_SERVER,
})
}
>
{
databaseTypeToLabelMap[
'sql_server'
]
}
</MenubarItem>
<MenubarItem
onClick={() =>
openExportSQLDialog({
targetDatabaseType:
DatabaseType.MARIADB,
})
}
>
{
databaseTypeToLabelMap[
'mariadb'
]
}
</MenubarItem>
<MenubarItem
onClick={() =>
openExportSQLDialog({
targetDatabaseType:
DatabaseType.SQLITE,
})
}
>
{
databaseTypeToLabelMap[
'sqlite'
]
}
</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSub>
<MenubarSubTrigger>
Export as
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem onClick={exportPNG}>
PNG
</MenubarItem>
<MenubarItem onClick={exportJPG}>
JPG
</MenubarItem>
<MenubarItem onClick={exportSVG}>
SVG
</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSeparator />
<MenubarItem>Exit</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Edit</MenubarTrigger>
<MenubarContent>
<MenubarItem>
Undo{' '}
<MenubarShortcut>Z</MenubarShortcut>
</MenubarItem>
<MenubarItem>
Redo{' '}
<MenubarShortcut>Z</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem
onClick={() => setShowClearAlert(true)}
>
Clear
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Help</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={openChartDBIO}>
Visit ChartDB
</MenubarItem>
<MenubarItem onClick={openJoinSlack}>
Join us on Slack
</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>
</div>
<nav className="flex flex-row items-center justify-between px-4 h-12 border-b">
<div className="flex flex-1 justify-start gap-x-3">
<div className="flex font-primary items-center">
<a
href="https://chartdb.io"
target="_blank"
className="cursor-pointer"
rel="noreferrer"
>
<img
src={ChartDBLogo}
alt="chartDB"
className="h-4 max-w-fit"
/>
</a>
</div>
<div className="flex flex-row flex-1 justify-center items-center group gap-2">
<Tooltip>
<TooltipTrigger>
<img
src={
databaseSecondaryLogoMap[
currentDiagram.databaseType
]
}
className="h-5 max-w-fit"
alt="database"
/>
</TooltipTrigger>
<TooltipContent>
{
databaseTypeToLabelMap[
<div>
<Menubar className="border-none shadow-none">
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={createNewDiagram}>
New
<MenubarShortcut>T</MenubarShortcut>
</MenubarItem>
<MenubarItem onClick={openDiagram}>
Open
</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
Export SQL
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem
onClick={() =>
openExportSQLDialog({
targetDatabaseType:
DatabaseType.GENERIC,
})
}
>
{databaseTypeToLabelMap['generic']}
</MenubarItem>
<MenubarItem
onClick={() =>
openExportSQLDialog({
targetDatabaseType:
DatabaseType.POSTGRESQL,
})
}
>
{
databaseTypeToLabelMap[
'postgresql'
]
}
</MenubarItem>
<MenubarItem
onClick={() =>
openExportSQLDialog({
targetDatabaseType:
DatabaseType.MYSQL,
})
}
>
{databaseTypeToLabelMap['mysql']}
</MenubarItem>
<MenubarItem
onClick={() =>
openExportSQLDialog({
targetDatabaseType:
DatabaseType.SQL_SERVER,
})
}
>
{
databaseTypeToLabelMap[
'sql_server'
]
}
</MenubarItem>
<MenubarItem
onClick={() =>
openExportSQLDialog({
targetDatabaseType:
DatabaseType.MARIADB,
})
}
>
{databaseTypeToLabelMap['mariadb']}
</MenubarItem>
<MenubarItem
onClick={() =>
openExportSQLDialog({
targetDatabaseType:
DatabaseType.SQLITE,
})
}
>
{databaseTypeToLabelMap['sqlite']}
</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSub>
<MenubarSubTrigger>
Export as
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem onClick={exportPNG}>
PNG
</MenubarItem>
<MenubarItem onClick={exportJPG}>
JPG
</MenubarItem>
<MenubarItem onClick={exportSVG}>
SVG
</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSeparator />
<MenubarItem
onClick={() =>
showAlert({
title: 'Delete Diagram',
description:
'This action cannot be undone. This will permanently delete the diagram.',
actionLabel: 'Delete',
closeLabel: 'Cancel',
onAction: deleteDiagram,
})
}
>
Delete Diagram
</MenubarItem>
<MenubarSeparator />
<MenubarItem>Exit</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Edit</MenubarTrigger>
<MenubarContent>
<MenubarItem>
Undo <MenubarShortcut>Z</MenubarShortcut>
</MenubarItem>
<MenubarItem>
Redo <MenubarShortcut>Z</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem
onClick={() =>
showAlert({
title: 'Clear Diagram',
description:
'This action cannot be undone. This will permanently delete all the data in the diagram.',
actionLabel: 'Clear',
closeLabel: 'Cancel',
onAction: clearDiagramData,
})
}
>
Clear
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Help</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={openChartDBIO}>
Visit ChartDB
</MenubarItem>
<MenubarItem onClick={openJoinSlack}>
Join us on Slack
</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>
</div>
</div>
<div className="flex flex-row flex-1 justify-center items-center group gap-2">
<Tooltip>
<TooltipTrigger>
<img
src={
databaseSecondaryLogoMap[
currentDiagram.databaseType
]
}
</TooltipContent>
</Tooltip>
<div className="flex">
<Label>Diagrams/</Label>
</div>
<div className="flex flex-row items-center gap-1">
{editMode ? (
<>
<Input
ref={inputRef}
autoFocus
type="text"
placeholder={diagramName}
value={editedDiagramName}
onClick={(e) => e.stopPropagation()}
onChange={(e) =>
setEditedDiagramName(e.target.value)
}
className="h-7 focus-visible:ring-0"
/>
<Button
variant="ghost"
className="hover:bg-primary-foreground p-2 w-7 h-7 text-slate-500 hover:text-slate-700 hidden group-hover:flex"
onClick={editDiagramName}
>
<Check />
</Button>
</>
) : (
<>
<Label>{diagramName}</Label>
<Button
variant="ghost"
className="hover:bg-primary-foreground p-2 w-7 h-7 text-slate-500 hover:text-slate-700 hidden group-hover:flex"
onClick={enterEditMode}
>
<Pencil />
</Button>
</>
)}
</div>
className="h-5 max-w-fit"
alt="database"
/>
</TooltipTrigger>
<TooltipContent>
{databaseTypeToLabelMap[currentDiagram.databaseType]}
</TooltipContent>
</Tooltip>
<div className="flex">
<Label>Diagrams/</Label>
</div>
<div className="hidden flex-1 justify-end sm:flex">
<Tooltip>
<TooltipTrigger>
<Badge variant="secondary" className="flex gap-1">
<Save className="h-4" />
Last saved
<TimeAgo datetime={currentDiagram.updatedAt} />
</Badge>
</TooltipTrigger>
<TooltipContent>
{currentDiagram.updatedAt.toLocaleString()}
</TooltipContent>
</Tooltip>
<div className="flex flex-row items-center gap-1">
{editMode ? (
<>
<Input
ref={inputRef}
autoFocus
type="text"
placeholder={diagramName}
value={editedDiagramName}
onClick={(e) => e.stopPropagation()}
onChange={(e) =>
setEditedDiagramName(e.target.value)
}
className="h-7 focus-visible:ring-0"
/>
<Button
variant="ghost"
className="hover:bg-primary-foreground p-2 w-7 h-7 text-slate-500 hover:text-slate-700 hidden group-hover:flex"
onClick={editDiagramName}
>
<Check />
</Button>
</>
) : (
<>
<Label>{diagramName}</Label>
<Button
variant="ghost"
className="hover:bg-primary-foreground p-2 w-7 h-7 text-slate-500 hover:text-slate-700 hidden group-hover:flex"
onClick={enterEditMode}
>
<Pencil />
</Button>
</>
)}
</div>
</nav>
<AlertDialog open={showClearAlert}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you absolutely sure?
</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently
delete all the data in the diagram.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
onClick={() => setShowClearAlert(false)}
>
Cancel
</AlertDialogCancel>
<AlertDialogAction onClick={clearDiagramDataHandler}>
Clear
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
</div>
<div className="hidden flex-1 justify-end sm:flex">
<Tooltip>
<TooltipTrigger>
<Badge variant="secondary" className="flex gap-1">
<Save className="h-4" />
Last saved
<TimeAgo datetime={currentDiagram.updatedAt} />
</Badge>
</TooltipTrigger>
<TooltipContent>
{currentDiagram.updatedAt.toLocaleString()}
</TooltipContent>
</Tooltip>
</div>
</nav>
);
};