diff --git a/packages/web/src/designer/Designer.js b/packages/web/src/designer/Designer.js index 2111addbf..7d951bb05 100644 --- a/packages/web/src/designer/Designer.js +++ b/packages/web/src/designer/Designer.js @@ -128,10 +128,13 @@ export default function Designer({ value, onChange, conid, database }) { const bringToFront = React.useCallback( (table) => { - onChange((current) => ({ - ...current, - tables: [...(current.tables || []).filter((x) => x.designerId != table.designerId), table], - })); + onChange( + (current) => ({ + ...current, + tables: [...(current.tables || []).filter((x) => x.designerId != table.designerId), table], + }), + true + ); }, [onChange] ); @@ -260,14 +263,17 @@ export default function Designer({ value, onChange, conid, database }) { const handleSelectColumn = React.useCallback( (column) => { - onChange((current) => ({ - ...current, - columns: (current.columns || []).find( - (x) => x.designerId == column.designerId && x.columnName == column.columnName - ) - ? current.columns - : [...cleanupDesignColumns(current.columns), _.pick(column, ['designerId', 'columnName'])], - })); + onChange( + (current) => ({ + ...current, + columns: (current.columns || []).find( + (x) => x.designerId == column.designerId && x.columnName == column.columnName + ) + ? current.columns + : [...cleanupDesignColumns(current.columns), _.pick(column, ['designerId', 'columnName'])], + }), + true + ); }, [onChange] ); @@ -275,19 +281,20 @@ export default function Designer({ value, onChange, conid, database }) { const handleChangeColumn = React.useCallback( (column, changeFunc) => { onChange((current) => { - const existing = (current.columns || []).find( + const currentColumns = (current || {}).columns || []; + const existing = currentColumns.find( (x) => x.designerId == column.designerId && x.columnName == column.columnName ); if (existing) { return { ...current, - columns: current.columns.map((x) => (x == existing ? changeFunc(existing) : x)), + columns: currentColumns.map((x) => (x == existing ? changeFunc(existing) : x)), }; } else { return { ...current, columns: [ - ...cleanupDesignColumns(current.columns), + ...cleanupDesignColumns(currentColumns), changeFunc(_.pick(column, ['designerId', 'columnName'])), ], }; diff --git a/packages/web/src/designer/QueryDesignColumns.js b/packages/web/src/designer/QueryDesignColumns.js index acea7edd8..6a90fd136 100644 --- a/packages/web/src/designer/QueryDesignColumns.js +++ b/packages/web/src/designer/QueryDesignColumns.js @@ -6,7 +6,7 @@ import InlineButton from '../widgets/InlineButton'; import { findDesignerFilterType } from './designerTools'; function getTableDisplayName(column, tables) { - const table = tables.find((x) => x.designerId == column.designerId); + const table = (tables || []).find((x) => x.designerId == column.designerId); if (table) return table.alias || table.pureName; return ''; } diff --git a/packages/web/src/designer/QueryDesignToolbar.js b/packages/web/src/designer/QueryDesignToolbar.js index 58efb9f30..69962401e 100644 --- a/packages/web/src/designer/QueryDesignToolbar.js +++ b/packages/web/src/designer/QueryDesignToolbar.js @@ -2,7 +2,7 @@ import React from 'react'; import useHasPermission from '../utility/useHasPermission'; import ToolbarButton from '../widgets/ToolbarButton'; -export default function QueryDesignToolbar({ execute, isDatabaseDefined, busy, save }) { +export default function QueryDesignToolbar({ execute, isDatabaseDefined, busy, save, modelState, dispatchModel }) { const hasPermission = useHasPermission(); return ( <> @@ -14,6 +14,12 @@ export default function QueryDesignToolbar({ execute, isDatabaseDefined, busy, s Save )} + dispatchModel({ type: 'undo' })} icon="icon undo"> + Undo + + dispatchModel({ type: 'redo' })} icon="icon redo"> + Redo + ); } diff --git a/packages/web/src/tabs/QueryDesignTab.js b/packages/web/src/tabs/QueryDesignTab.js index 4effaacac..db4f2c9ee 100644 --- a/packages/web/src/tabs/QueryDesignTab.js +++ b/packages/web/src/tabs/QueryDesignTab.js @@ -24,6 +24,7 @@ import QueryDesigner from '../designer/QueryDesigner'; import QueryDesignColumns from '../designer/QueryDesignColumns'; import { findEngineDriver } from 'dbgate-tools'; import { generateDesignedQuery } from '../designer/designerTools'; +import useUndoReducer from '../utility/useUndoReducer'; export default function QueryDesignTab({ tabid, conid, database, tabVisible, toolbarPortalRef, ...other }) { const [sessionId, setSessionId] = React.useState(null); @@ -36,11 +37,26 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too const connection = useConnectionInfo({ conid }); const engine = findEngineDriver(connection, extensions); const [sqlPreview, setSqlPreview] = React.useState(''); - const { editorData, setEditorData, isLoading } = useEditorData({ + const { initialData, setEditorData, isLoading } = useEditorData({ tabid, }); + const [modelState, dispatchModel] = useUndoReducer( + { + tables: [], + references: [], + columns: [], + }, + { mergeNearActions: true } + ); - const editorRef = React.useRef(null); + React.useEffect(() => { + // @ts-ignore + if (initialData) dispatchModel({ type: 'reset', value: initialData }); + }, [initialData]); + + React.useEffect(() => { + setEditorData(modelState.value); + }, [modelState]); const handleSessionDone = React.useCallback(() => { setBusy(false); @@ -53,8 +69,19 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too }; React.useEffect(() => { - generatePreview(editorData, engine); - }, [editorData, engine]); + generatePreview(modelState.value, engine); + }, [modelState.value, engine]); + + const handleChange = React.useCallback( + (value, skipUndoChain) => + // @ts-ignore + dispatchModel({ + type: 'compute', + useMerge: skipUndoChain, + compute: (v) => (_.isFunction(value) ? value(v) : value), + }), + [dispatchModel] + ); React.useEffect(() => { if (sessionId && socket) { @@ -136,15 +163,15 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too <> - + @@ -164,6 +191,8 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too tabVisible && ReactDOM.createPortal( (state, action) => { + const { mergeNearActions } = options || {}; + + const useMerge = + action.useMerge || (mergeNearActions && state.lastActionTm && new Date().getTime() - state.lastActionTm < 100); + switch (action.type) { case 'set': return { - history: [...state.history.slice(0, state.current + 1), action.value], - current: state.current + 1, + history: [...state.history.slice(0, useMerge ? state.current : state.current + 1), action.value], + current: useMerge ? state.current : state.current + 1, value: action.value, canUndo: true, canRedo: false, + lastActionTm: new Date().getTime(), }; case 'compute': { const newValue = action.compute(state.history[state.current]); return { - history: [...state.history.slice(0, state.current + 1), newValue], - current: state.current + 1, + history: [...state.history.slice(0, useMerge ? state.current : state.current + 1), newValue], + current: useMerge ? state.current : state.current + 1, value: newValue, canUndo: true, canRedo: false, + lastActionTm: new Date().getTime(), }; } case 'undo': @@ -29,6 +36,7 @@ function reducer(state, action) { value: state.history[state.current - 1], canUndo: state.current > 1, canRedo: true, + lastActionTm: null, }; return state; case 'redo': @@ -39,6 +47,7 @@ function reducer(state, action) { value: state.history[state.current + 1], canUndo: true, canRedo: state.current < state.history.length - 2, + lastActionTm: null, }; return state; case 'reset': @@ -46,12 +55,13 @@ function reducer(state, action) { history: [action.value], current: 0, value: action.value, + lastActionTm: null, }; } -} +}; -export default function useUndoReducer(initialValue) { - return React.useReducer(reducer, { +export default function useUndoReducer(initialValue, options) { + return React.useReducer(reducer(options), { history: [initialValue], current: 0, value: initialValue, diff --git a/packages/web/src/widgets/Toolbar.js b/packages/web/src/widgets/Toolbar.js index 34d5bef79..d6321a522 100644 --- a/packages/web/src/widgets/Toolbar.js +++ b/packages/web/src/widgets/Toolbar.js @@ -109,7 +109,6 @@ export default function ToolBar({ toolbarPortalRef }) { } else if (openTabdata) { try { const json = JSON.parse(decodeURIComponent(openTabdata)); - console.log('TABDATA', json); openFavorite(json); window.history.replaceState(null, null, ' '); } catch (err) {