Skip to content

Commit

Permalink
feat(xod-client): add a Catcher component that autorecovers the who…
Browse files Browse the repository at this point in the history
…le state of the IDE to the previous stable state and show an error message
  • Loading branch information
brusherru committed Dec 23, 2020
1 parent 99c3a0b commit 3eed4fe
Show file tree
Hide file tree
Showing 14 changed files with 409 additions and 77 deletions.
17 changes: 15 additions & 2 deletions packages/xod-client-browser/src/containers/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class App extends client.App {
this.onStopDebuggerSessionClicked = this.onStopDebuggerSessionClicked.bind(
this
);
this.onFirstRun = this.onFirstRun.bind(this);

this.hideInstallAppPopup = this.hideInstallAppPopup.bind(this);

Expand All @@ -63,10 +64,20 @@ class App extends client.App {
this.urlActions = {
[client.URL_ACTION_TYPES.OPEN_TUTORIAL]: this.onOpenTutorial,
};
}

componentDidMount() {
super.componentDidMount();
document.addEventListener('click', this.onDocumentClick);
}
componentWillUnmount() {
super.componentWillUnmount();
document.removeEventListener('click', this.onDocumentClick);
}

props.actions.openProject(props.tutorialProject);
onFirstRun() {
super.onFirstRun();
this.props.actions.openProject(this.props.tutorialProject);
}

onDocumentClick(e) {
Expand Down Expand Up @@ -428,4 +439,6 @@ const mapDispatchToProps = dispatch => ({
),
});

export default connect(mapStateToProps, mapDispatchToProps)(App);
export default connect(mapStateToProps, mapDispatchToProps, null, {
withRef: true,
})(App);
82 changes: 49 additions & 33 deletions packages/xod-client-electron/src/view/containers/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,46 @@ class App extends client.App {

this.initNativeMenu();

this.hotkeyHandlers = R.merge(
{
[client.COMMAND.UPLOAD_WITH_DEBUG]: this
.onUploadToArduinoAndDebugClicked,
},
this.defaultHotkeyHandlers
);

this.urlActions = {
// actionPathName: params => this.props.actions.someAction(params.foo, params.bar),
[client.URL_ACTION_TYPES.OPEN_TUTORIAL]: this.onOpenTutorialProject,
};
}

shouldComponentUpdate(nextProps, nextState) {
const props = this.props;
// Custom checks for some props
// All other props will be checked for ===
const propChecks = {
currentPatchPath: (prev, next) => prev.equals(next), // Maybe.equals
popups: R.equals,
popupsData: R.equals,
};
const propEquality = R.mapObjIndexed((val, key) =>
R.ifElse(
R.has(key),
R.pipe(R.prop(key), check => check(val, nextProps[key])),
() => val === nextProps[key]
)(propChecks)
)(props);

return (
R.any(R.equals(false), R.values(propEquality)) ||
!R.equals(this.state, nextState)
);
}

componentDidMount() {
super.componentDidMount();

// Reactions on messages from Main Process
ipcRenderer.on(EVENTS.PROJECT_PATH_CHANGED, (event, projectPath) =>
this.setState({ projectPath })
Expand Down Expand Up @@ -191,18 +231,6 @@ class App extends client.App {
this.showError(error);
});

this.hotkeyHandlers = R.merge(
{
[client.COMMAND.UPLOAD_WITH_DEBUG]: this
.onUploadToArduinoAndDebugClicked,
},
this.defaultHotkeyHandlers
);

this.urlActions = {
// actionPathName: params => this.props.actions.someAction(params.foo, params.bar),
[client.URL_ACTION_TYPES.OPEN_TUTORIAL]: this.onOpenTutorialProject,
};
ipcRenderer.on(EVENTS.XOD_URL_CLICKED, (event, { actionName, params }) => {
const action = this.urlActions[actionName];

Expand Down Expand Up @@ -239,26 +267,12 @@ class App extends client.App {
}
}

shouldComponentUpdate(nextProps, nextState) {
const props = this.props;
// Custom checks for some props
// All other props will be checked for ===
const propChecks = {
currentPatchPath: (prev, next) => prev.equals(next), // Maybe.equals
popups: R.equals,
popupsData: R.equals,
};
const propEquality = R.mapObjIndexed((val, key) =>
R.ifElse(
R.has(key),
R.pipe(R.prop(key), check => check(val, nextProps[key])),
() => val === nextProps[key]
)(propChecks)
)(props);

return (
R.any(R.equals(false), R.values(propEquality)) ||
!R.equals(this.state, nextState)
componentWillUnmount() {
super.componentWillUnmount();
// Unsubscribe from all ipc events
R.map(
eventName => ipcRenderer.removeAllListeners(eventName),
ipcRenderer.eventNames()
);
}

Expand Down Expand Up @@ -1141,4 +1155,6 @@ const mapDispatchToProps = dispatch => ({
),
});

export default connect(mapStateToProps, mapDispatchToProps)(App);
export default connect(mapStateToProps, mapDispatchToProps, null, {
withRef: true,
})(App);
2 changes: 2 additions & 0 deletions packages/xod-client/src/core/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export * from '../project/actionTypes';
export * from '../messages/actionTypes';

export const SHOW_CODE_REQUESTED = 'SHOW_CODE_REQUESTED';

export const RECOVER_STATE = 'RECOVER_STATE';
7 changes: 6 additions & 1 deletion packages/xod-client/src/core/actions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { undoPatch, redoPatch } from '../project/actions';
import { SHOW_CODE_REQUESTED } from './actionTypes';
import { SHOW_CODE_REQUESTED, RECOVER_STATE } from './actionTypes';
import { getCurrentPatchPath } from '../editor/selectors';
import { isInput } from '../utils/browser';

Expand All @@ -24,6 +24,11 @@ export const showCode = code => ({
payload: { code },
});

export const recoverState = state => ({
type: RECOVER_STATE,
payload: state,
});

export * from '../user/actions';
export * from '../editor/actions';
export * from '../project/actions';
Expand Down
99 changes: 64 additions & 35 deletions packages/xod-client/src/core/containers/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,21 @@ import { DO_NOT_USE_TETHERING_INTERNET_IN_BROWSER } from '../../debugger/message
import { COMMAND } from '../../utils/constants';

import formatErrorMessage from '../formatErrorMessage';
import initialProjectState from '../../project/state';

export default class App extends React.Component {
constructor(props) {
super(props);

this.appRef = null;
this.isBrowser = false; // by default

this.transformProjectForTranspiler = this.transformProjectForTranspiler.bind(
this
);
this.getGlobals = this.getGlobals.bind(this);
this.onSelectAll = this.onSelectAll.bind(this);
this.onFocusOut = this.onFocusOut.bind(this);

this.defaultHotkeyHandlers = {
[COMMAND.UNDO]: this.props.actions.undoCurrentPatch,
Expand All @@ -86,57 +90,70 @@ export default class App extends React.Component {
[COMMAND.PAN_TO_CENTER]: this.props.actions.panToCenter,
};

// When the parent component <Catcher> catches an error
// the <App> container will be constructed again,
// however the redux state still unrecovered,
// so <App> should open the initial (empty) project
// to be shown beneath the "Recovering" spinner
// until the state will be recovered.
this.props.actions.openProject(initialProjectState);
}

componentDidMount() {
document.addEventListener('cut', this.props.actions.cutEntities);
document.addEventListener('copy', this.props.actions.copyEntities);
document.addEventListener('paste', this.props.actions.pasteEntities);

/**
* We have to handle some hotkeys, because:
* - Browser IDE should prevent default event handling
* - Electron IDE cannot handle some hotkeys correctly
* "...some keybindings cannot be overridden on Windows/Linux because they are hard-coded in Chrome."
* See details: https://github.com/electron/electron/issues/7165 and related issues
*/
document.addEventListener('keydown', event => {
// Prevent selecting all contents with "Ctrl+a" or "Command+a"
const key = event.keyCode || event.which;
const mod = event.metaKey || event.ctrlKey;

// CmdOrCtrl+A
if (mod && key === 65 && !isInputTarget(event)) {
event.preventDefault();
this.props.actions.selectAll();
}
});
}
componentDidMount() {
document.addEventListener('cut', this.props.actions.cutEntities);
document.addEventListener('copy', this.props.actions.copyEntities);
document.addEventListener('paste', this.props.actions.pasteEntities);
this.props.actions.fetchGrant(/* startup */ true);
document.addEventListener('keydown', this.onSelectAll);

// TODO: Replace with ref and `React.createRef`
// after updating React >16.3
const appEl = document.getElementById('App');

if (elementHasFocusFunction(appEl)) appEl.focus();
this.appRef = document.getElementById('App');
if (elementHasFocusFunction(this.appRef)) this.appRef.focus();

/**
* Listen capturing focusout on any element inside body element
* and if element unfocused to the body element — focus on the `App`
* component to make sure main hotkeys will work anytime.
*/
document.body.addEventListener(
'focusout',
// We need a timeout here to await while focus changed on the some element
() =>
setTimeout(() => {
// If focused changed to the body element — focus to the App
if (
document.activeElement === document.body &&
elementHasFocusFunction(appEl)
) {
appEl.focus();
}
}, 0),
true
);
document.body.addEventListener('focusout', this.onFocusOut, true);
}

componentWillUnmount() {
// In case that IDE crashes with some error it will reconstruct
// the <App> container, so we have to clear the document events
// to avoid duplicating of handlers
document.removeEventListener('cut', this.props.actions.cutEntities);
document.removeEventListener('copy', this.props.actions.copyEntities);
document.removeEventListener('paste', this.props.actions.pasteEntities);
document.removeEventListener('keydown', this.selectAll);
document.body.removeEventListener('focusout', this.onFocusOut, true);
}

// This method will be called once on the first run of the IDE
onFirstRun() {
this.props.actions.fetchGrant(/* startup */ true);
}

onFocusOut() {
// We need a timeout here to await while focus changed on the some element
setTimeout(() => {
// If focused changed to the body element — focus to the App
if (
document.activeElement === document.body &&
this.appRef &&
elementHasFocusFunction(this.appRef)
) {
this.appRef.focus();
}
}, 0);
}

onShowCodeArduino(liveness = LIVENESS.NONE) {
Expand All @@ -153,6 +170,18 @@ export default class App extends React.Component {
)(liveness);
}

onSelectAll(event) {
// Prevent selecting all contents with "Ctrl+a" or "Command+a"
const key = event.keyCode || event.which;
const mod = event.metaKey || event.ctrlKey;

// CmdOrCtrl+A
if (mod && key === 65 && !isInputTarget(event)) {
event.preventDefault();
this.props.actions.selectAll();
}
}

onRunSimulation() {
if (this.props.isSimulationAbortable) {
this.props.actions.addError(SIMULATION_ALREADY_RUNNING);
Expand Down
Loading

0 comments on commit 3eed4fe

Please sign in to comment.