Merge pull request #4 from chartdb/redo-undo

Redo undo
This commit is contained in:
Guy Ben-Aharon
2024-08-20 18:22:12 +03:00
committed by GitHub
15 changed files with 1277 additions and 426 deletions

14
package-lock.json generated
View File

@@ -24,6 +24,7 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@uidotdev/usehooks": "^2.4.1",
"@xyflow/react": "^12.0.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
@@ -2736,6 +2737,19 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@uidotdev/usehooks": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz",
"integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==",
"license": "MIT",
"engines": {
"node": ">=16"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",

View File

@@ -27,6 +27,7 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@uidotdev/usehooks": "^2.4.1",
"@xyflow/react": "^12.0.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",

View File

@@ -16,7 +16,10 @@ export interface ChartDBContext {
// General operations
updateDiagramId: (id: string) => Promise<void>;
updateDiagramName: (name: string) => Promise<void>;
updateDiagramName: (
name: string,
options?: { updateHistory: boolean }
) => Promise<void>;
loadDiagram: (diagramId: string) => Promise<Diagram | undefined>;
// Database type operations
@@ -24,13 +27,23 @@ export interface ChartDBContext {
// Table operations
createTable: () => Promise<DBTable>;
addTable: (table: DBTable) => Promise<void>;
addTable: (
table: DBTable,
options?: { updateHistory: boolean }
) => Promise<void>;
getTable: (id: string) => DBTable | null;
removeTable: (id: string) => Promise<void>;
updateTable: (id: string, table: Partial<DBTable>) => Promise<void>;
updateTables: (tables: PartialExcept<DBTable, 'id'>[]) => Promise<void>;
removeTable: (
id: string,
options?: { updateHistory: boolean }
) => Promise<void>;
updateTable: (
id: string,
table: Partial<DBTable>,
options?: { updateHistory: boolean }
) => Promise<void>;
updateTablesState: (
updateFn: (tables: DBTable[]) => PartialExcept<DBTable, 'id'>[]
updateFn: (tables: DBTable[]) => PartialExcept<DBTable, 'id'>[],
options?: { updateHistory: boolean }
) => Promise<void>;
// Field operations
@@ -38,21 +51,39 @@ export interface ChartDBContext {
updateField: (
tableId: string,
fieldId: string,
field: Partial<DBField>
field: Partial<DBField>,
options?: { updateHistory: boolean }
) => Promise<void>;
removeField: (
tableId: string,
fieldId: string,
options?: { updateHistory: boolean }
) => Promise<void>;
removeField: (tableId: string, fieldId: string) => Promise<void>;
createField: (tableId: string) => Promise<DBField>;
addField: (tableId: string, field: DBField) => Promise<void>;
addField: (
tableId: string,
field: DBField,
options?: { updateHistory: boolean }
) => Promise<void>;
// Index operations
createIndex: (tableId: string) => Promise<DBIndex>;
addIndex: (tableId: string, index: DBIndex) => Promise<void>;
addIndex: (
tableId: string,
index: DBIndex,
options?: { updateHistory: boolean }
) => Promise<void>;
getIndex: (tableId: string, indexId: string) => DBIndex | null;
removeIndex: (tableId: string, indexId: string) => Promise<void>;
removeIndex: (
tableId: string,
indexId: string,
options?: { updateHistory: boolean }
) => Promise<void>;
updateIndex: (
tableId: string,
indexId: string,
index: Partial<DBIndex>
index: Partial<DBIndex>,
options?: { updateHistory: boolean }
) => Promise<void>;
// Relationship operations
@@ -62,19 +93,33 @@ export interface ChartDBContext {
sourceFieldId: string;
targetFieldId: string;
}) => Promise<DBRelationship>;
addRelationship: (relationship: DBRelationship) => Promise<void>;
addRelationship: (
relationship: DBRelationship,
options?: { updateHistory: boolean }
) => Promise<void>;
addRelationships: (
relationships: DBRelationship[],
options?: { updateHistory: boolean }
) => Promise<void>;
getRelationship: (id: string) => DBRelationship | null;
removeRelationship: (id: string) => Promise<void>;
removeRelationships: (...ids: string[]) => Promise<void>;
removeRelationship: (
id: string,
options?: { updateHistory: boolean }
) => Promise<void>;
removeRelationships: (
ids: string[],
options?: { updateHistory: boolean }
) => Promise<void>;
updateRelationship: (
id: string,
relationship: Partial<DBRelationship>
relationship: Partial<DBRelationship>,
options?: { updateHistory: boolean }
) => Promise<void>;
}
export const chartDBContext = createContext<ChartDBContext>({
databaseType: DatabaseType.GENERIC,
diagramName: 'New Diagram',
diagramName: '',
diagramId: '',
tables: [],
relationships: [],
@@ -88,7 +133,7 @@ export const chartDBContext = createContext<ChartDBContext>({
updateDatabaseType: emptyFn,
// Table operations
updateTables: emptyFn,
// updateTables: emptyFn,
createTable: emptyFn,
getTable: emptyFn,
addTable: emptyFn,
@@ -117,4 +162,5 @@ export const chartDBContext = createContext<ChartDBContext>({
removeRelationship: emptyFn,
updateRelationship: emptyFn,
removeRelationships: emptyFn,
addRelationships: emptyFn,
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
import { emptyFn } from '@/lib/utils';
import { createContext } from 'react';
export interface HistoryContext {
undo: () => void;
redo: () => void;
hasUndo: boolean;
hasRedo: boolean;
}
export const historyContext = createContext<HistoryContext>({
undo: emptyFn,
redo: emptyFn,
hasUndo: false,
hasRedo: false,
});

View File

@@ -0,0 +1,246 @@
import React, { useMemo } from 'react';
import { historyContext } from './history-context';
import { useChartDB } from '@/hooks/use-chartdb';
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
import { RedoUndoActionHandlers } from './redo-undo-action';
export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const {
addRedoAction,
addUndoAction,
undoStack,
redoStack,
hasRedo,
hasUndo,
} = useRedoUndoStack();
const {
addTable,
removeTable,
updateTable,
updateDiagramName,
removeField,
addField,
updateField,
addRelationship,
addRelationships,
removeRelationship,
updateRelationship,
updateTablesState,
addIndex,
removeIndex,
updateIndex,
removeRelationships,
} = useChartDB();
const redoActionHandlers = useMemo(
(): RedoUndoActionHandlers => ({
updateDiagramName: ({ redoData: { name } }) => {
return updateDiagramName(name, { updateHistory: false });
},
addTable: ({ redoData: { table } }) => {
return addTable(table, { updateHistory: false });
},
removeTable: ({ redoData: { tableId } }) => {
return removeTable(tableId, { updateHistory: false });
},
updateTable: ({ redoData: { tableId, table } }) => {
return updateTable(tableId, table, { updateHistory: false });
},
updateTablesState: ({ redoData: { tables } }) => {
return updateTablesState(() => tables, {
updateHistory: false,
});
},
addField: ({ redoData: { tableId, field } }) => {
return addField(tableId, field, { updateHistory: false });
},
removeField: ({ redoData: { tableId, fieldId } }) => {
return removeField(tableId, fieldId, { updateHistory: false });
},
updateField: ({ redoData: { tableId, fieldId, field } }) => {
return updateField(tableId, fieldId, field, {
updateHistory: false,
});
},
addRelationship: ({ redoData: { relationship } }) => {
return addRelationship(relationship, { updateHistory: false });
},
addRelationships: ({ redoData: { relationships } }) => {
return addRelationships(relationships, {
updateHistory: false,
});
},
removeRelationship: ({ redoData: { relationshipId } }) => {
return removeRelationship(relationshipId, {
updateHistory: false,
});
},
updateRelationship: ({
redoData: { relationshipId, relationship },
}) => {
return updateRelationship(relationshipId, relationship, {
updateHistory: false,
});
},
removeRelationships: ({ redoData: { relationshipsIds } }) => {
return removeRelationships(relationshipsIds, {
updateHistory: false,
});
},
addIndex: ({ redoData: { tableId, index } }) => {
return addIndex(tableId, index, { updateHistory: false });
},
removeIndex: ({ redoData: { tableId, indexId } }) => {
return removeIndex(tableId, indexId, { updateHistory: false });
},
updateIndex: ({ redoData: { tableId, indexId, index } }) => {
return updateIndex(tableId, indexId, index, {
updateHistory: false,
});
},
}),
[
addTable,
removeTable,
updateTable,
updateDiagramName,
removeField,
addField,
updateField,
addRelationship,
addRelationships,
removeRelationship,
updateRelationship,
updateTablesState,
addIndex,
removeIndex,
updateIndex,
removeRelationships,
]
);
const undoActionHandlers = useMemo(
(): RedoUndoActionHandlers => ({
updateDiagramName: ({ undoData: { name } }) => {
return updateDiagramName(name, { updateHistory: false });
},
addTable: ({ undoData: { tableId } }) => {
return removeTable(tableId, { updateHistory: false });
},
removeTable: ({ undoData: { table } }) => {
return addTable(table, { updateHistory: false });
},
updateTable: ({ undoData: { tableId, table } }) => {
return updateTable(tableId, table, { updateHistory: false });
},
addField: ({ undoData: { fieldId, tableId } }) => {
return removeField(tableId, fieldId, { updateHistory: false });
},
removeField: ({ undoData: { tableId, field } }) => {
return addField(tableId, field, { updateHistory: false });
},
updateField: ({ undoData: { tableId, fieldId, field } }) => {
return updateField(tableId, fieldId, field, {
updateHistory: false,
});
},
addRelationship: ({ undoData: { relationshipId } }) => {
return removeRelationship(relationshipId, {
updateHistory: false,
});
},
removeRelationship: ({ undoData: { relationship } }) => {
return addRelationship(relationship, { updateHistory: false });
},
removeRelationships: ({ undoData: { relationships } }) => {
return addRelationships(relationships, {
updateHistory: false,
});
},
updateRelationship: ({
undoData: { relationshipId, relationship },
}) => {
return updateRelationship(relationshipId, relationship, {
updateHistory: false,
});
},
updateTablesState: ({ undoData: { tables } }) => {
return updateTablesState(() => tables, {
updateHistory: false,
});
},
addIndex: ({ undoData: { tableId, indexId } }) => {
return removeIndex(tableId, indexId, { updateHistory: false });
},
removeIndex: ({ undoData: { tableId, index } }) => {
return addIndex(tableId, index, { updateHistory: false });
},
updateIndex: ({ undoData: { tableId, indexId, index } }) => {
return updateIndex(tableId, indexId, index, {
updateHistory: false,
});
},
addRelationships: ({ undoData: { relationshipIds } }) => {
return removeRelationships(relationshipIds, {
updateHistory: false,
});
},
}),
[
addTable,
removeTable,
updateTable,
updateDiagramName,
removeField,
addField,
updateField,
addRelationship,
addRelationships,
removeRelationship,
updateRelationship,
updateTablesState,
addIndex,
removeIndex,
updateIndex,
removeRelationships,
]
);
const undo = async () => {
const action = undoStack.pop();
if (!action) {
return;
}
const handler = undoActionHandlers[action.action];
addRedoAction(action);
await handler?.({
undoData: action.undoData,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
};
const redo = async () => {
const action = redoStack.pop();
if (!action) {
return;
}
const handler = redoActionHandlers[action.action];
addUndoAction(action);
await handler?.({
redoData: action.redoData,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
};
return (
<historyContext.Provider value={{ undo, redo, hasRedo, hasUndo }}>
{children}
</historyContext.Provider>
);
};

View File

@@ -0,0 +1,144 @@
import { DBTable } from '@/lib/domain/db-table';
import { ChartDBContext } from '../chartdb-context/chartdb-context';
import { DBField } from '@/lib/domain/db-field';
import { DBIndex } from '@/lib/domain/db-index';
import { DBRelationship } from '@/lib/domain/db-relationship';
type Action = keyof ChartDBContext;
type RedoUndoActionBase<T extends Action, RD, UD> = {
action: T;
redoData: RD;
undoData: UD;
};
type RedoUndoActionUpdateDiagramName = RedoUndoActionBase<
'updateDiagramName',
{ name: string },
{ name: string }
>;
type RedoUndoActionUpdateTable = RedoUndoActionBase<
'updateTable',
{ tableId: string; table: Partial<DBTable> },
{ tableId: string; table: Partial<DBTable> }
>;
type RedoUndoActionAddTable = RedoUndoActionBase<
'addTable',
{ table: DBTable },
{ tableId: string }
>;
type RedoUndoActionRemoveTable = RedoUndoActionBase<
'removeTable',
{ tableId: string },
{ table: DBTable }
>;
type RedoUndoActionUpdateTablesState = RedoUndoActionBase<
'updateTablesState',
{ tables: DBTable[] },
{ tables: DBTable[] }
>;
type RedoUndoActionAddField = RedoUndoActionBase<
'addField',
{ tableId: string; field: DBField },
{ tableId: string; fieldId: string }
>;
type RedoUndoActionRemoveField = RedoUndoActionBase<
'removeField',
{ tableId: string; fieldId: string },
{ tableId: string; field: DBField }
>;
type RedoUndoActionUpdateField = RedoUndoActionBase<
'updateField',
{ tableId: string; fieldId: string; field: Partial<DBField> },
{ tableId: string; fieldId: string; field: Partial<DBField> }
>;
type RedoUndoActionAddIndex = RedoUndoActionBase<
'addIndex',
{ tableId: string; index: DBIndex },
{ tableId: string; indexId: string }
>;
type RedoUndoActionRemoveIndex = RedoUndoActionBase<
'removeIndex',
{ tableId: string; indexId: string },
{ tableId: string; index: DBIndex }
>;
type RedoUndoActionUpdateIndex = RedoUndoActionBase<
'updateIndex',
{ tableId: string; indexId: string; index: Partial<DBIndex> },
{ tableId: string; indexId: string; index: Partial<DBIndex> }
>;
type RedoUndoActionAddRelationship = RedoUndoActionBase<
'addRelationship',
{ relationship: DBRelationship },
{ relationshipId: string }
>;
type RedoUndoActionAddRelationships = RedoUndoActionBase<
'addRelationships',
{ relationships: DBRelationship[] },
{ relationshipIds: string[] }
>;
type RedoUndoActionRemoveRelationship = RedoUndoActionBase<
'removeRelationship',
{ relationshipId: string },
{ relationship: DBRelationship }
>;
type RedoUndoActionUpdateRelationship = RedoUndoActionBase<
'updateRelationship',
{ relationshipId: string; relationship: Partial<DBRelationship> },
{ relationshipId: string; relationship: Partial<DBRelationship> }
>;
type RedoUndoActionRemoveRelationships = RedoUndoActionBase<
'removeRelationships',
{ relationshipsIds: string[] },
{ relationships: DBRelationship[] }
>;
export type RedoUndoAction =
| RedoUndoActionAddTable
| RedoUndoActionRemoveTable
| RedoUndoActionUpdateTable
| RedoUndoActionUpdateDiagramName
| RedoUndoActionUpdateTablesState
| RedoUndoActionAddField
| RedoUndoActionRemoveField
| RedoUndoActionUpdateField
| RedoUndoActionAddIndex
| RedoUndoActionRemoveIndex
| RedoUndoActionUpdateIndex
| RedoUndoActionAddRelationship
| RedoUndoActionAddRelationships
| RedoUndoActionRemoveRelationship
| RedoUndoActionUpdateRelationship
| RedoUndoActionRemoveRelationships;
export type RedoActionData<T extends Action> = Extract<
RedoUndoAction,
{ action: T }
>['redoData'];
export type UndoActionData<T extends Action> = Extract<
RedoUndoAction,
{ action: T }
>['undoData'];
export type RedoUndoActionHandlers = {
[K in RedoUndoAction['action']]: (args: {
redoData: RedoActionData<K>;
undoData: UndoActionData<K>;
}) => Promise<void>;
};

View File

@@ -0,0 +1,23 @@
import { createContext } from 'react';
import { RedoUndoAction } from './redo-undo-action';
import { emptyFn } from '@/lib/utils';
export interface RedoUndoStackContext {
redoStack: RedoUndoAction[];
undoStack: RedoUndoAction[];
addRedoAction: (action: RedoUndoAction) => void;
addUndoAction: (action: RedoUndoAction) => void;
resetRedoStack: () => void;
hasRedo: boolean;
hasUndo: boolean;
}
export const redoUndoStackContext = createContext<RedoUndoStackContext>({
redoStack: [],
undoStack: [],
addRedoAction: emptyFn,
addUndoAction: emptyFn,
resetRedoStack: emptyFn,
hasRedo: false,
hasUndo: false,
});

View File

@@ -0,0 +1,51 @@
import React, { useCallback } from 'react';
import { RedoUndoAction } from './redo-undo-action';
import {
RedoUndoStackContext,
redoUndoStackContext,
} from './redo-undo-stack-context';
export const RedoUndoStackProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const [undoStack, setUndoStack] = React.useState<RedoUndoAction[]>([]);
const [redoStack, setRedoStack] = React.useState<RedoUndoAction[]>([]);
const addRedoAction: RedoUndoStackContext['addRedoAction'] = useCallback(
(action) => {
setRedoStack((prev) => [...prev, action]);
},
[setRedoStack]
);
const addUndoAction: RedoUndoStackContext['addUndoAction'] = useCallback(
(action) => {
setUndoStack((prev) => [...prev, action]);
},
[setUndoStack]
);
const resetRedoStack: RedoUndoStackContext['resetRedoStack'] =
useCallback(() => {
setRedoStack([]);
}, [setRedoStack]);
const hasRedo = redoStack.length > 0;
const hasUndo = undoStack.length > 0;
return (
<redoUndoStackContext.Provider
value={{
redoStack,
undoStack,
addRedoAction,
addUndoAction,
resetRedoStack,
hasRedo,
hasUndo,
}}
>
{children}
</redoUndoStackContext.Provider>
);
};

View File

@@ -16,7 +16,7 @@ import { getDatabaseLogo } from '@/lib/databases';
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
import { Textarea } from '@/components/textarea/textarea';
import { useStorage } from '@/hooks/use-storage';
import { loadFromDatabaseMetadata } from '@/lib/domain/diagram';
import { Diagram, loadFromDatabaseMetadata } from '@/lib/domain/diagram';
import { useCreateDiagramDialog } from '@/hooks/use-create-diagram-dialog';
import { useNavigate } from 'react-router-dom';
import { useConfig } from '@/hooks/use-config';
@@ -24,6 +24,7 @@ import {
DatabaseMetadata,
loadDatabaseMetadata,
} from '@/lib/data/import-metadata/metadata-types/database-metadata';
import { generateId } from '@/lib/utils';
enum CreateDiagramDialogStep {
SELECT_DATABASE = 'SELECT_DATABASE',
@@ -59,14 +60,22 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
}, [listDiagrams, setDiagramNumber]);
const createNewDiagram = useCallback(async () => {
const databaseMetadata: DatabaseMetadata =
loadDatabaseMetadata(scriptResult);
let diagram: Diagram = {
id: generateId(),
name: `Diagram ${diagramNumber}`,
databaseType: databaseType ?? DatabaseType.GENERIC,
};
const diagram = loadFromDatabaseMetadata({
databaseType,
databaseMetadata,
diagramNumber,
});
if (scriptResult.trim().length !== 0) {
const databaseMetadata: DatabaseMetadata =
loadDatabaseMetadata(scriptResult);
diagram = loadFromDatabaseMetadata({
databaseType,
databaseMetadata,
diagramNumber,
});
}
await addDiagram({ diagram });
await updateConfig({ defaultDiagramId: diagram.id });

4
src/hooks/use-history.ts Normal file
View File

@@ -0,0 +1,4 @@
import { useContext } from 'react';
import { historyContext } from '@/context/history-context/history-context';
export const useHistory = () => useContext(historyContext);

View File

@@ -0,0 +1,4 @@
import { redoUndoStackContext } from '@/context/history-context/redo-undo-stack-context';
import { useContext } from 'react';
export const useRedoUndoStack = () => useContext(redoUndoStackContext);

View File

@@ -156,7 +156,7 @@ export const Canvas: React.FC<CanvasProps> = () => {
.filter((id) => !!id) as string[];
if (relationshipsToRemove.length > 0) {
removeRelationships(...relationshipsToRemove);
removeRelationships(relationshipsToRemove);
}
const selectionChanges = changes.filter(

View File

@@ -1,12 +1,14 @@
import React from 'react';
import { Card, CardContent } from '@/components/card/card';
import { ZoomIn, ZoomOut, Save } from 'lucide-react';
import { ZoomIn, ZoomOut, Save, Redo, Undo } from 'lucide-react';
import { Separator } from '@/components/separator/separator';
import { ToolbarButton } from './toolbar-button';
import { useHistory } from '@/hooks/use-history';
export interface ToolbarProps {}
export const Toolbar: React.FC<ToolbarProps> = () => {
const { redo, undo, hasRedo, hasUndo } = useHistory();
return (
<div className="px-1">
<Card className="shadow-none p-0 bg-secondary h-[44px]">
@@ -21,6 +23,13 @@ export const Toolbar: React.FC<ToolbarProps> = () => {
<ToolbarButton>
<Save />
</ToolbarButton>
<Separator orientation="vertical" />
<ToolbarButton onClick={undo} disabled={!hasUndo}>
<Undo />
</ToolbarButton>
<ToolbarButton onClick={redo} disabled={!hasRedo}>
<Redo />
</ToolbarButton>
</CardContent>
</Card>
</div>

View File

@@ -7,6 +7,8 @@ import { ReactFlowProvider } from '@xyflow/react';
import { StorageProvider } from './context/storage-context/storage-provider';
import { CreateDiagramDialogProvider } from './dialogs/create-diagram-dialog/create-diagram-dialog-provider';
import { ConfigProvider } from './context/config-context/config-provider';
import { HistoryProvider } from './context/history-context/history-provider';
import { RedoUndoStackProvider } from './context/history-context/redo-undo-stack-provider';
const routes: RouteObject[] = [
...['', 'diagrams/:diagramId'].map((path) => ({
@@ -14,13 +16,17 @@ const routes: RouteObject[] = [
element: (
<StorageProvider>
<ConfigProvider>
<ChartDBProvider>
<CreateDiagramDialogProvider>
<ReactFlowProvider>
<EditorPage />
</ReactFlowProvider>
</CreateDiagramDialogProvider>
</ChartDBProvider>
<RedoUndoStackProvider>
<ChartDBProvider>
<HistoryProvider>
<CreateDiagramDialogProvider>
<ReactFlowProvider>
<EditorPage />
</ReactFlowProvider>
</CreateDiagramDialogProvider>
</HistoryProvider>
</ChartDBProvider>
</RedoUndoStackProvider>
</ConfigProvider>
</StorageProvider>
),