PromucFlow_constructor/app/client/src/pages/Editor/GlobalHotKeys.tsx
rahulramesha 6cefc6dbea
feat: Undo/Redo (#6654)
* Scaffolding for undo-redo

* undo redo working Poc commit

* memory performance improvements by diffing

* dont run update on undo/redo"

* merging widget postion update and canvas bottom row update into one dsl update.

* fix tabs widget

* Visible updates per undo redo action (#6838)

Co-authored-by: Rahul R <rahulramesha@Rahuls-MacBook-Pro.local>

* resize atomic operation

* fix switch control state issue

* disallow undo/redo for snipping and comment mode

* disallow undo/redo for snipping and comment mode

* fix color picker issue in undo/redo

* add test for replayDSL

* option control fix, adding logs

* minor position change undo redo updates

* add test cases for replayHelpers

* property Upade visual change

* remove unused code

* global hot key jest test for undo redo

* Fixing batch updates on property change..

* add tests for toggle control in property pane

* unwanted utils.

* add tests for text control

* add tests for deletion

* add tests for dropping a new widget

* adding jest test for replayUtils

* add move widget tests

* add tests for color picker control

* add analytics for undo/redo

* add analytics for undo/redo

* tab addition atomic

* cypress tests for propertyPane, toasts and radiowidget optionControl

* replayDSL end of redo stack fix

* property update changes

* menu option control debounce input

* color picker empty undo fix

* fix cypress tests

* widget add/remove atomic

* revert alternative approach to handle atomic operations

* update replayDSL test

* add some comments

* addressing review comments

* flash color for property pane controls

* Fixing adding of tabs widget as well.

* code review comments.

* merging widget postion update and canvas bottom row update into one dsl update.

* fix ordering of tabs property control

* meta property update canvas min height.

* fixing failed specs.

* Fixing entity explorer update on deleting tab from entity explorer.

* address review comments and minor property update changes

* fixing failing tests

* merge conflicts

* changes to cater widget api.

* fix suggested widget table issue

* draggable list for undo redo

* fix widget name focus

* excluding canvas updates.

* fixing codeEditor update on propertySection collapse

* fixed failing test case

Co-authored-by: Abhinav Jha <abhinav@appsmith.com>
Co-authored-by: Rahul R <rahulramesha@Rahuls-MacBook-Pro.local>
Co-authored-by: root <root@DESKTOP-9GENCK0.localdomain>
Co-authored-by: Ashok Kumar M <35134347+marks0351@users.noreply.github.com>
Co-authored-by: Pawan Kumar <pawankumar@Pawans-MacBook-Pro.local>
2021-09-21 13:25:56 +05:30

372 lines
11 KiB
TypeScript

import React from "react";
import { connect } from "react-redux";
import { AppState } from "reducers";
import { Hotkey, Hotkeys } from "@blueprintjs/core";
import { HotkeysTarget } from "@blueprintjs/core/lib/esnext/components/hotkeys/hotkeysTarget.js";
import {
closePropertyPane,
closeTableFilterPane,
copyWidget,
cutWidget,
deleteSelectedWidget,
groupWidgets,
pasteWidget,
} from "actions/widgetActions";
import {
deselectAllInitAction,
selectAllWidgetsInCanvasInitAction,
} from "actions/widgetSelectionActions";
import { toggleShowGlobalSearchModal } from "actions/globalSearchActions";
import { isMac } from "utils/helpers";
import { getSelectedWidget, getSelectedWidgets } from "selectors/ui";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
import { getSelectedText } from "utils/helpers";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { WIDGETS_SEARCH_ID } from "constants/Explorer";
import { resetSnipingMode as resetSnipingModeAction } from "actions/propertyPaneActions";
import { showDebugger } from "actions/debuggerActions";
import { setCommentModeInUrl } from "pages/Editor/ToggleModeButton";
import { runActionViaShortcut } from "actions/pluginActionActions";
import {
filterCategories,
SearchCategory,
SEARCH_CATEGORY_ID,
} from "components/editorComponents/GlobalSearch/utils";
import { redoAction, undoAction } from "actions/pageActions";
import { Toaster } from "components/ads/Toast";
import { Variant } from "components/ads/common";
import { getAppMode } from "selectors/applicationSelectors";
import { APP_MODE } from "entities/App";
import { commentModeSelector } from "selectors/commentsSelectors";
import { createMessage, SAVE_HOTKEY_TOASTER_MESSAGE } from "constants/messages";
type Props = {
copySelectedWidget: () => void;
pasteCopiedWidget: () => void;
deleteSelectedWidget: () => void;
cutSelectedWidget: () => void;
groupSelectedWidget: () => void;
toggleShowGlobalSearchModal: (category: SearchCategory) => void;
resetSnipingMode: () => void;
openDebugger: () => void;
closeProppane: () => void;
closeTableFilterProppane: () => void;
executeAction: () => void;
selectAllWidgetsInit: () => void;
deselectAllWidgets: () => void;
selectedWidget?: string;
selectedWidgets: string[];
isDebuggerOpen: boolean;
children: React.ReactNode;
undo: () => void;
redo: () => void;
appMode?: APP_MODE;
isCommentMode: boolean;
};
@HotkeysTarget
class GlobalHotKeys extends React.Component<Props> {
public stopPropagationIfWidgetSelected(e: KeyboardEvent): boolean {
const multipleWidgetsSelected =
this.props.selectedWidgets && this.props.selectedWidgets.length;
const singleWidgetSelected =
this.props.selectedWidget &&
this.props.selectedWidget != MAIN_CONTAINER_WIDGET_ID;
if (
(singleWidgetSelected || multipleWidgetsSelected) &&
!getSelectedText()
) {
e.preventDefault();
e.stopPropagation();
return true;
}
return false;
}
public onOnmnibarHotKeyDown(
e: KeyboardEvent,
categoryId: SEARCH_CATEGORY_ID = SEARCH_CATEGORY_ID.NAVIGATION,
) {
e.preventDefault();
const category = filterCategories[categoryId];
this.props.toggleShowGlobalSearchModal(category);
AnalyticsUtil.logEvent("OPEN_OMNIBAR", {
source: "HOTKEY_COMBO",
category: category.title,
});
}
public renderHotkeys() {
return (
<Hotkeys>
<Hotkey
combo="mod + f"
global
label="Search entities"
onKeyDown={(e: any) => {
const widgetSearchInput = document.getElementById(
WIDGETS_SEARCH_ID,
);
if (widgetSearchInput) {
widgetSearchInput.focus();
e.preventDefault();
e.stopPropagation();
}
}}
/>
<Hotkey
allowInInput={false}
combo="mod + k"
global
label="Show omnibar"
onKeyDown={(e) => this.onOnmnibarHotKeyDown(e)}
/>
<Hotkey
allowInInput={false}
combo="mod + j"
global
label="Show omnibar"
onKeyDown={(e) =>
this.onOnmnibarHotKeyDown(e, SEARCH_CATEGORY_ID.SNIPPETS)
}
/>
<Hotkey
allowInInput={false}
combo="mod + l"
global
label="Show omnibar"
onKeyDown={(e) =>
this.onOnmnibarHotKeyDown(e, SEARCH_CATEGORY_ID.DOCUMENTATION)
}
/>
<Hotkey
allowInInput={false}
combo="mod + p"
global
label="Show omnibar"
onKeyDown={(e) =>
this.onOnmnibarHotKeyDown(e, SEARCH_CATEGORY_ID.INIT)
}
/>
<Hotkey
combo="mod + d"
global
group="Canvas"
label="Open Debugger"
onKeyDown={() => {
this.props.openDebugger();
if (this.props.isDebuggerOpen) {
AnalyticsUtil.logEvent("OPEN_DEBUGGER", {
source: "CANVAS",
});
}
}}
preventDefault
/>
<Hotkey
combo="mod + c"
global
group="Canvas"
label="Copy Widget"
onKeyDown={(e: any) => {
if (this.stopPropagationIfWidgetSelected(e)) {
this.props.copySelectedWidget();
}
}}
/>
<Hotkey
combo="mod + v"
global
group="Canvas"
label="Paste Widget"
onKeyDown={() => {
this.props.pasteCopiedWidget();
}}
/>
<Hotkey
combo="backspace"
global
group="Canvas"
label="Delete Widget"
onKeyDown={(e: any) => {
if (this.stopPropagationIfWidgetSelected(e) && isMac()) {
this.props.deleteSelectedWidget();
}
}}
/>
<Hotkey
combo="del"
global
group="Canvas"
label="Delete Widget"
onKeyDown={(e: any) => {
if (this.stopPropagationIfWidgetSelected(e)) {
this.props.deleteSelectedWidget();
}
}}
/>
<Hotkey
combo="mod + x"
global
group="Canvas"
label="Cut Widget"
onKeyDown={(e: any) => {
if (this.stopPropagationIfWidgetSelected(e)) {
this.props.cutSelectedWidget();
}
}}
/>
<Hotkey
combo="mod + a"
global
group="Canvas"
label="Select all Widget"
onKeyDown={(e: any) => {
this.props.selectAllWidgetsInit();
e.preventDefault();
}}
/>
<Hotkey
combo="esc"
global
group="Canvas"
label="Deselect all Widget"
onKeyDown={(e: any) => {
if (this.props.isCommentMode) {
AnalyticsUtil.logEvent("COMMENTS_TOGGLE_MODE", {
mode: this.props.appMode,
source: "HOTKEY",
combo: "esc",
});
}
setCommentModeInUrl(false);
this.props.resetSnipingMode();
this.props.deselectAllWidgets();
this.props.closeProppane();
this.props.closeTableFilterProppane();
e.preventDefault();
}}
/>
<Hotkey
combo="v"
global
label="Edit Mode"
onKeyDown={(e: any) => {
if (this.props.isCommentMode)
AnalyticsUtil.logEvent("COMMENTS_TOGGLE_MODE", {
mode: this.props.appMode,
source: "HOTKEY",
combo: "v",
});
setCommentModeInUrl(false);
this.props.resetSnipingMode();
e.preventDefault();
}}
/>
<Hotkey
combo="c"
global
label="Comment Mode"
onKeyDown={() => {
if (!this.props.isCommentMode)
AnalyticsUtil.logEvent("COMMENTS_TOGGLE_MODE", {
mode: "COMMENT",
source: "HOTKEY",
combo: "c",
});
setCommentModeInUrl(true);
}}
/>
<Hotkey
allowInInput
combo="mod + enter"
global
label="Execute Action"
onKeyDown={this.props.executeAction}
preventDefault
stopPropagation
/>
<Hotkey
combo="mod + z"
global
label="Undo change in canvas"
onKeyDown={this.props.undo}
preventDefault
stopPropagation
/>
<Hotkey
combo="mod + shift + z"
global
label="Redo change in canvas"
onKeyDown={this.props.redo}
preventDefault
stopPropagation
/>
<Hotkey
combo="mod + g"
global
group="Canvas"
label="Cut Widgets for grouping"
onKeyDown={(e: any) => {
if (this.stopPropagationIfWidgetSelected(e)) {
this.props.groupSelectedWidget();
}
}}
/>
<Hotkey
combo="mod + s"
global
label="Save progress"
onKeyDown={() => {
Toaster.show({
text: createMessage(SAVE_HOTKEY_TOASTER_MESSAGE),
variant: Variant.info,
});
}}
preventDefault
stopPropagation
/>
</Hotkeys>
);
}
render() {
return <div>{this.props.children}</div>;
}
}
const mapStateToProps = (state: AppState) => ({
selectedWidget: getSelectedWidget(state),
selectedWidgets: getSelectedWidgets(state),
isDebuggerOpen: state.ui.debugger.isOpen,
appMode: getAppMode(state),
isCommentMode: commentModeSelector(state),
});
const mapDispatchToProps = (dispatch: any) => {
return {
copySelectedWidget: () => dispatch(copyWidget(true)),
pasteCopiedWidget: () => dispatch(pasteWidget()),
deleteSelectedWidget: () => dispatch(deleteSelectedWidget(true)),
cutSelectedWidget: () => dispatch(cutWidget()),
groupSelectedWidget: () => dispatch(groupWidgets()),
toggleShowGlobalSearchModal: (category: SearchCategory) =>
dispatch(toggleShowGlobalSearchModal(category)),
resetSnipingMode: () => dispatch(resetSnipingModeAction()),
openDebugger: () => dispatch(showDebugger()),
closeProppane: () => dispatch(closePropertyPane()),
closeTableFilterProppane: () => dispatch(closeTableFilterPane()),
selectAllWidgetsInit: () => dispatch(selectAllWidgetsInCanvasInitAction()),
deselectAllWidgets: () => dispatch(deselectAllInitAction()),
executeAction: () => dispatch(runActionViaShortcut()),
undo: () => dispatch(undoAction()),
redo: () => dispatch(redoAction()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(GlobalHotKeys);