Skip to content

Commit

Permalink
Merge pull request #963 from Jappzy/workflow-hotkey-enhancements
Browse files Browse the repository at this point in the history
Hotkey Shortcuts and Open in New Tab/Window
  • Loading branch information
arm4b authored Oct 10, 2022
2 parents 7bd9b7b + 921b12a commit e172f1a
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 36 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ Added

Contributed by @ParthS007

* Added new Hotkey Shortcuts for Workflow Designer: Save (ctrl/cmd + s), Open (ctrl/cmd + o),
Undo/Redo (ctrl/cmd + z, shift + z), Copy/Cut/Paste (ctrl/cmd + c/x/v). #963, #991

Contributed by @Jappzy and @cded from @Bitovi

Changed
~~~~~~~
* Updated nodejs from `14.16.1` to `14.20.1`, fixing the local build under ARM processor architecture. #880
Expand Down
61 changes: 36 additions & 25 deletions apps/st2-workflows/workflows.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ import React, { Component } from 'react';
// import ReactDOM from 'react-dom';
import { Provider, connect } from 'react-redux';
import { PropTypes } from 'prop-types';
import { HotKeys } from 'react-hotkeys';
import { pick, mapValues, get } from 'lodash';
import { mapValues, get } from 'lodash';
import cx from 'classnames';
import url from 'url';
import Menu from '@stackstorm/module-menu';
Expand All @@ -34,18 +33,6 @@ import globalStore from '@stackstorm/module-store';
import store from './store';
import style from './style.css';

function guardKeyHandlers(obj, names) {
const filteredObj = pick(obj, names);
return mapValues(filteredObj, fn => {
return e => {
if(e.target === document.body) {
e.preventDefault();
fn.call(obj);
}
};
});
}

const POLL_INTERVAL = 5000;

@connect(
Expand Down Expand Up @@ -285,7 +272,7 @@ export default class Workflows extends Component {
// don't need to return anything to the store. the handler will change dirty.
return {};
})();

store.dispatch({
type: 'SAVE_WORKFLOW',
promise,
Expand All @@ -295,10 +282,37 @@ export default class Workflows extends Component {

style = style

keyMap = {
undo: [ 'ctrl+z', 'meta+z' ],
redo: [ 'ctrl+shift+z', 'meta+shift+z' ],
handleTaskDelete: [ 'del', 'backspace' ],
keyHandlers = {
undo: () => {
store.dispatch({ type: 'FLOW_UNDO' });
},
redo: () => {
store.dispatch({ type: 'FLOW_REDO' });
},
save: async (x) => {
if (x) {
x.preventDefault();
x.stopPropagation();
}

try {
await this.save();
store.dispatch({ type: 'PUSH_SUCCESS', source: 'icon-save', message: 'Workflow saved.' });
}
catch(e) {
const faultString = get(e, 'response.data.faultstring');
store.dispatch({ type: 'PUSH_ERROR', source: 'icon-save', error: `Error saving workflow: ${faultString}` });
}
},
copy: () => {
store.dispatch({ type: 'PUSH_WARNING', source: 'icon-save', message: 'Select a task to copy' });
},
cut: () => {
store.dispatch({ type: 'PUSH_WARNING', source: 'icon-save', message: 'Nothing to cut' });
},
paste: () => {
store.dispatch({ type: 'PUSH_WARNING', source: 'icon-save', message: 'Nothing to paste' });
},
}

render() {
Expand All @@ -323,13 +337,10 @@ export default class Workflows extends Component {
<Menu location={location} routes={this.props.routes} />
<div className="component-row-content">
{ !isCollapsed.palette && <Palette className="palette" actions={actions} /> }
<HotKeys
<div
style={{ flex: 1}}
keyMap={this.keyMap}
attach={document.body}
handlers={guardKeyHandlers(this.props, [ 'undo', 'redo' ])}
>
<Canvas className="canvas" location={location} match={match} fetchActionscalled={e => this.props.fetchActions()} saveData={e => this.save()} dirtyflag={this.props.dirty}>
<Canvas className="canvas" location={location} match={match} fetchActionscalled={e => this.props.fetchActions()} save={this.keyHandlers.save} dirtyflag={this.props.dirty} undo={this.keyHandlers.undo} redo={this.keyHandlers.redo}>
<Toolbar>
<ToolbarButton key="undo" icon="icon-redirect" title="Undo" errorMessage="Could not undo." onClick={() => undo()} />
<ToolbarButton key="redo" icon="icon-redirect2" title="Redo" errorMessage="Could not redo." onClick={() => redo()} />
Expand Down Expand Up @@ -379,7 +390,7 @@ export default class Workflows extends Component {
</ToolbarDropdown>
</Toolbar>
</Canvas>
</HotKeys>
</div>
{ !isCollapsed.details && <Details className="details" actions={actions} /> }
</div>
</div>
Expand Down
76 changes: 67 additions & 9 deletions modules/st2flow-canvas/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,14 @@ export default class Canvas extends Component {
dirtyflag: PropTypes.bool,
fetchActionscalled: PropTypes.func,
saveData: PropTypes.func,
undo: PropTypes.func,
redo: PropTypes.func,
save: PropTypes.func,
}

state = {
scale: 0,
copiedTask: null,
}

componentDidMount() {
Expand Down Expand Up @@ -714,17 +718,71 @@ export default class Canvas extends Component {
style={{height: '100%'}}
focused={true}
attach={document.body}
handlers={{handleTaskDelete: e => {
// This will break if canvas elements (tasks/transitions) become focus targets with
// tabindex or automatically focusing elements. But in that case, the Task already
// has a handler for delete waiting.
if(e.target === document.body) {
e.preventDefault();
if(selectedTask) {
keyMap={{
copy: [ 'ctrl+c', 'command+c' ],
cut: [ 'ctrl+x', 'command+x' ],
paste: [ 'ctrl+v', 'command+v' ],
open: [ 'ctrl+o', 'command+o' ],
undo: [ 'ctrl+z', 'command+z' ],
redo: [ 'ctrl+shift+z', 'command+shift+z' ],
save: [ 'ctrl+s', 'command+s' ],
}}
handlers={{
copy: () => {
if (selectedTask) {
this.setState({ copiedTask: selectedTask });
}
},
cut: () => {
if (selectedTask) {
this.setState({ copiedTask: selectedTask });
this.handleTaskDelete(selectedTask);
}
}
}}}
},
paste: () => {
if (document.activeElement.tagName === 'TEXTAREA' || document.activeElement.tagName === 'INPUT') {
// allow regular copy/paste from clipboard when inputs or textareas are focused
return;
}

const { copiedTask } = this.state;
if (copiedTask) {
const taskHeight = copiedTask.size.y;
const taskCoords = copiedTask.coords;

const newCoords = {
x: taskCoords.x,
y: taskCoords.y + taskHeight + 10,
};

const lastIndex = tasks
.map(task => (task.name.match(/task(\d+)/) || [])[1])
.reduce((acc, item) => Math.max(acc, item || 0), 0);

this.props.issueModelCommand('addTask', {
name: `task${lastIndex + 1}`,
action: copiedTask.action,
coords: Vector.max(newCoords, new Vector(0, 0)),
});
}
},
open: () => {
if (selectedTask) {
window.open(`${location.origin}/#/action/${selectedTask.action}`, '_blank');
}
},
undo: () => {
this.props.undo();
},
redo: () => {
this.props.redo();
},
save: (e) => {
e.preventDefault();
e.stopPropagation();
this.props.save();
},
}}
>
<div
className={cx(this.props.className, this.style.component)}
Expand Down
16 changes: 15 additions & 1 deletion modules/st2flow-palette/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,22 @@ export default class Action extends Component<{

render() {
const { action } = this.props;

const href = `${location.origin}/#/action/${action.ref}`;

const supportedRunnerTypes = {
'orquesta': href,
'mistral-v2': href
};

return (
<div className={this.style.action} ref={this.actionRef} draggable>
<div
draggable
className={this.style.action}
ref={this.actionRef}
href={supportedRunnerTypes[action.runner_type]}
target="_blank"
rel="noopener noreferrer">
<div className={this.style.actionName}>{ action.ref }</div>
<div className={this.style.actionDescription}>{ action.description }</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions modules/st2flow-palette/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ limitations under the License.

position: relative;

display: block;

&-name {
color: #2d2d2d;
font-size: 14px;
Expand Down
2 changes: 1 addition & 1 deletion tasks/lint.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const plugins = require('gulp-load-plugins')(settings.plugins);

gulp.task('lint', (done) => gulp.src(settings.lint, { cwd: settings.dev })
.pipe(plugins.plumber())
.pipe(plugins.eslint())
.pipe(plugins.eslint({ fix: true }))
.pipe(plugins.eslint.format())
.pipe(plugins.eslint.failAfterError())
.on('end', () => done())
Expand Down

0 comments on commit e172f1a

Please sign in to comment.