query designer - undo

This commit is contained in:
Jan Prochazka
2020-12-30 18:33:17 +01:00
parent d71f27f8b3
commit 7a5187e283
6 changed files with 85 additions and 34 deletions

View File

@@ -128,10 +128,13 @@ export default function Designer({ value, onChange, conid, database }) {
const bringToFront = React.useCallback( const bringToFront = React.useCallback(
(table) => { (table) => {
onChange((current) => ({ onChange(
...current, (current) => ({
tables: [...(current.tables || []).filter((x) => x.designerId != table.designerId), table], ...current,
})); tables: [...(current.tables || []).filter((x) => x.designerId != table.designerId), table],
}),
true
);
}, },
[onChange] [onChange]
); );
@@ -260,14 +263,17 @@ export default function Designer({ value, onChange, conid, database }) {
const handleSelectColumn = React.useCallback( const handleSelectColumn = React.useCallback(
(column) => { (column) => {
onChange((current) => ({ onChange(
...current, (current) => ({
columns: (current.columns || []).find( ...current,
(x) => x.designerId == column.designerId && x.columnName == column.columnName columns: (current.columns || []).find(
) (x) => x.designerId == column.designerId && x.columnName == column.columnName
? current.columns )
: [...cleanupDesignColumns(current.columns), _.pick(column, ['designerId', 'columnName'])], ? current.columns
})); : [...cleanupDesignColumns(current.columns), _.pick(column, ['designerId', 'columnName'])],
}),
true
);
}, },
[onChange] [onChange]
); );
@@ -275,19 +281,20 @@ export default function Designer({ value, onChange, conid, database }) {
const handleChangeColumn = React.useCallback( const handleChangeColumn = React.useCallback(
(column, changeFunc) => { (column, changeFunc) => {
onChange((current) => { onChange((current) => {
const existing = (current.columns || []).find( const currentColumns = (current || {}).columns || [];
const existing = currentColumns.find(
(x) => x.designerId == column.designerId && x.columnName == column.columnName (x) => x.designerId == column.designerId && x.columnName == column.columnName
); );
if (existing) { if (existing) {
return { return {
...current, ...current,
columns: current.columns.map((x) => (x == existing ? changeFunc(existing) : x)), columns: currentColumns.map((x) => (x == existing ? changeFunc(existing) : x)),
}; };
} else { } else {
return { return {
...current, ...current,
columns: [ columns: [
...cleanupDesignColumns(current.columns), ...cleanupDesignColumns(currentColumns),
changeFunc(_.pick(column, ['designerId', 'columnName'])), changeFunc(_.pick(column, ['designerId', 'columnName'])),
], ],
}; };

View File

@@ -6,7 +6,7 @@ import InlineButton from '../widgets/InlineButton';
import { findDesignerFilterType } from './designerTools'; import { findDesignerFilterType } from './designerTools';
function getTableDisplayName(column, tables) { 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; if (table) return table.alias || table.pureName;
return ''; return '';
} }

View File

@@ -2,7 +2,7 @@ import React from 'react';
import useHasPermission from '../utility/useHasPermission'; import useHasPermission from '../utility/useHasPermission';
import ToolbarButton from '../widgets/ToolbarButton'; 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(); const hasPermission = useHasPermission();
return ( return (
<> <>
@@ -14,6 +14,12 @@ export default function QueryDesignToolbar({ execute, isDatabaseDefined, busy, s
Save Save
</ToolbarButton> </ToolbarButton>
)} )}
<ToolbarButton disabled={!modelState.canUndo} onClick={() => dispatchModel({ type: 'undo' })} icon="icon undo">
Undo
</ToolbarButton>
<ToolbarButton disabled={!modelState.canRedo} onClick={() => dispatchModel({ type: 'redo' })} icon="icon redo">
Redo
</ToolbarButton>
</> </>
); );
} }

View File

@@ -24,6 +24,7 @@ import QueryDesigner from '../designer/QueryDesigner';
import QueryDesignColumns from '../designer/QueryDesignColumns'; import QueryDesignColumns from '../designer/QueryDesignColumns';
import { findEngineDriver } from 'dbgate-tools'; import { findEngineDriver } from 'dbgate-tools';
import { generateDesignedQuery } from '../designer/designerTools'; import { generateDesignedQuery } from '../designer/designerTools';
import useUndoReducer from '../utility/useUndoReducer';
export default function QueryDesignTab({ tabid, conid, database, tabVisible, toolbarPortalRef, ...other }) { export default function QueryDesignTab({ tabid, conid, database, tabVisible, toolbarPortalRef, ...other }) {
const [sessionId, setSessionId] = React.useState(null); const [sessionId, setSessionId] = React.useState(null);
@@ -36,11 +37,26 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too
const connection = useConnectionInfo({ conid }); const connection = useConnectionInfo({ conid });
const engine = findEngineDriver(connection, extensions); const engine = findEngineDriver(connection, extensions);
const [sqlPreview, setSqlPreview] = React.useState(''); const [sqlPreview, setSqlPreview] = React.useState('');
const { editorData, setEditorData, isLoading } = useEditorData({ const { initialData, setEditorData, isLoading } = useEditorData({
tabid, 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(() => { const handleSessionDone = React.useCallback(() => {
setBusy(false); setBusy(false);
@@ -53,8 +69,19 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too
}; };
React.useEffect(() => { React.useEffect(() => {
generatePreview(editorData, engine); generatePreview(modelState.value, engine);
}, [editorData, 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(() => { React.useEffect(() => {
if (sessionId && socket) { if (sessionId && socket) {
@@ -136,15 +163,15 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too
<> <>
<VerticalSplitter initialValue="70%"> <VerticalSplitter initialValue="70%">
<QueryDesigner <QueryDesigner
value={editorData || {}} value={modelState.value || {}}
conid={conid} conid={conid}
database={database} database={database}
engine={connection && connection.engine} engine={connection && connection.engine}
onChange={setEditorData} onChange={handleChange}
></QueryDesigner> ></QueryDesigner>
<ResultTabs sessionId={sessionId} executeNumber={executeNumber}> <ResultTabs sessionId={sessionId} executeNumber={executeNumber}>
<TabPage label="Columns" key="columns"> <TabPage label="Columns" key="columns">
<QueryDesignColumns value={editorData || {}} onChange={setEditorData} /> <QueryDesignColumns value={modelState.value || {}} onChange={handleChange} />
</TabPage> </TabPage>
<TabPage label="SQL" key="sql"> <TabPage label="SQL" key="sql">
<SqlEditor value={sqlPreview} engine={engine} readOnly /> <SqlEditor value={sqlPreview} engine={engine} readOnly />
@@ -164,6 +191,8 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too
tabVisible && tabVisible &&
ReactDOM.createPortal( ReactDOM.createPortal(
<QueryDesignToolbar <QueryDesignToolbar
modelState={modelState}
dispatchModel={dispatchModel}
isDatabaseDefined={conid && database} isDatabaseDefined={conid && database}
execute={handleExecute} execute={handleExecute}
busy={busy} busy={busy}
@@ -178,7 +207,7 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too
<SaveTabModal <SaveTabModal
modalState={saveFileModalState} modalState={saveFileModalState}
tabVisible={tabVisible} tabVisible={tabVisible}
data={editorData} data={modelState.value}
format="json" format="json"
folder="query" folder="query"
tabid={tabid} tabid={tabid}

View File

@@ -1,24 +1,31 @@
import _ from 'lodash'; import _ from 'lodash';
import React from 'react'; import React from 'react';
function reducer(state, action) { const reducer = (options) => (state, action) => {
const { mergeNearActions } = options || {};
const useMerge =
action.useMerge || (mergeNearActions && state.lastActionTm && new Date().getTime() - state.lastActionTm < 100);
switch (action.type) { switch (action.type) {
case 'set': case 'set':
return { return {
history: [...state.history.slice(0, state.current + 1), action.value], history: [...state.history.slice(0, useMerge ? state.current : state.current + 1), action.value],
current: state.current + 1, current: useMerge ? state.current : state.current + 1,
value: action.value, value: action.value,
canUndo: true, canUndo: true,
canRedo: false, canRedo: false,
lastActionTm: new Date().getTime(),
}; };
case 'compute': { case 'compute': {
const newValue = action.compute(state.history[state.current]); const newValue = action.compute(state.history[state.current]);
return { return {
history: [...state.history.slice(0, state.current + 1), newValue], history: [...state.history.slice(0, useMerge ? state.current : state.current + 1), newValue],
current: state.current + 1, current: useMerge ? state.current : state.current + 1,
value: newValue, value: newValue,
canUndo: true, canUndo: true,
canRedo: false, canRedo: false,
lastActionTm: new Date().getTime(),
}; };
} }
case 'undo': case 'undo':
@@ -29,6 +36,7 @@ function reducer(state, action) {
value: state.history[state.current - 1], value: state.history[state.current - 1],
canUndo: state.current > 1, canUndo: state.current > 1,
canRedo: true, canRedo: true,
lastActionTm: null,
}; };
return state; return state;
case 'redo': case 'redo':
@@ -39,6 +47,7 @@ function reducer(state, action) {
value: state.history[state.current + 1], value: state.history[state.current + 1],
canUndo: true, canUndo: true,
canRedo: state.current < state.history.length - 2, canRedo: state.current < state.history.length - 2,
lastActionTm: null,
}; };
return state; return state;
case 'reset': case 'reset':
@@ -46,12 +55,13 @@ function reducer(state, action) {
history: [action.value], history: [action.value],
current: 0, current: 0,
value: action.value, value: action.value,
lastActionTm: null,
}; };
} }
} };
export default function useUndoReducer(initialValue) { export default function useUndoReducer(initialValue, options) {
return React.useReducer(reducer, { return React.useReducer(reducer(options), {
history: [initialValue], history: [initialValue],
current: 0, current: 0,
value: initialValue, value: initialValue,

View File

@@ -109,7 +109,6 @@ export default function ToolBar({ toolbarPortalRef }) {
} else if (openTabdata) { } else if (openTabdata) {
try { try {
const json = JSON.parse(decodeURIComponent(openTabdata)); const json = JSON.parse(decodeURIComponent(openTabdata));
console.log('TABDATA', json);
openFavorite(json); openFavorite(json);
window.history.replaceState(null, null, ' '); window.history.replaceState(null, null, ' ');
} catch (err) { } catch (err) {