Skip to content

Commit e172f1a

Browse files
authored
Merge pull request #963 from Jappzy/workflow-hotkey-enhancements
Hotkey Shortcuts and Open in New Tab/Window
2 parents 7bd9b7b + 921b12a commit e172f1a

File tree

6 files changed

+126
-36
lines changed

6 files changed

+126
-36
lines changed

CHANGELOG.rst

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ Added
1111

1212
Contributed by @ParthS007
1313

14+
* Added new Hotkey Shortcuts for Workflow Designer: Save (ctrl/cmd + s), Open (ctrl/cmd + o),
15+
Undo/Redo (ctrl/cmd + z, shift + z), Copy/Cut/Paste (ctrl/cmd + c/x/v). #963, #991
16+
17+
Contributed by @Jappzy and @cded from @Bitovi
18+
1419
Changed
1520
~~~~~~~
1621
* Updated nodejs from `14.16.1` to `14.20.1`, fixing the local build under ARM processor architecture. #880

apps/st2-workflows/workflows.component.js

+36-25
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ import React, { Component } from 'react';
1717
// import ReactDOM from 'react-dom';
1818
import { Provider, connect } from 'react-redux';
1919
import { PropTypes } from 'prop-types';
20-
import { HotKeys } from 'react-hotkeys';
21-
import { pick, mapValues, get } from 'lodash';
20+
import { mapValues, get } from 'lodash';
2221
import cx from 'classnames';
2322
import url from 'url';
2423
import Menu from '@stackstorm/module-menu';
@@ -34,18 +33,6 @@ import globalStore from '@stackstorm/module-store';
3433
import store from './store';
3534
import style from './style.css';
3635

37-
function guardKeyHandlers(obj, names) {
38-
const filteredObj = pick(obj, names);
39-
return mapValues(filteredObj, fn => {
40-
return e => {
41-
if(e.target === document.body) {
42-
e.preventDefault();
43-
fn.call(obj);
44-
}
45-
};
46-
});
47-
}
48-
4936
const POLL_INTERVAL = 5000;
5037

5138
@connect(
@@ -285,7 +272,7 @@ export default class Workflows extends Component {
285272
// don't need to return anything to the store. the handler will change dirty.
286273
return {};
287274
})();
288-
275+
289276
store.dispatch({
290277
type: 'SAVE_WORKFLOW',
291278
promise,
@@ -295,10 +282,37 @@ export default class Workflows extends Component {
295282

296283
style = style
297284

298-
keyMap = {
299-
undo: [ 'ctrl+z', 'meta+z' ],
300-
redo: [ 'ctrl+shift+z', 'meta+shift+z' ],
301-
handleTaskDelete: [ 'del', 'backspace' ],
285+
keyHandlers = {
286+
undo: () => {
287+
store.dispatch({ type: 'FLOW_UNDO' });
288+
},
289+
redo: () => {
290+
store.dispatch({ type: 'FLOW_REDO' });
291+
},
292+
save: async (x) => {
293+
if (x) {
294+
x.preventDefault();
295+
x.stopPropagation();
296+
}
297+
298+
try {
299+
await this.save();
300+
store.dispatch({ type: 'PUSH_SUCCESS', source: 'icon-save', message: 'Workflow saved.' });
301+
}
302+
catch(e) {
303+
const faultString = get(e, 'response.data.faultstring');
304+
store.dispatch({ type: 'PUSH_ERROR', source: 'icon-save', error: `Error saving workflow: ${faultString}` });
305+
}
306+
},
307+
copy: () => {
308+
store.dispatch({ type: 'PUSH_WARNING', source: 'icon-save', message: 'Select a task to copy' });
309+
},
310+
cut: () => {
311+
store.dispatch({ type: 'PUSH_WARNING', source: 'icon-save', message: 'Nothing to cut' });
312+
},
313+
paste: () => {
314+
store.dispatch({ type: 'PUSH_WARNING', source: 'icon-save', message: 'Nothing to paste' });
315+
},
302316
}
303317

304318
render() {
@@ -323,13 +337,10 @@ export default class Workflows extends Component {
323337
<Menu location={location} routes={this.props.routes} />
324338
<div className="component-row-content">
325339
{ !isCollapsed.palette && <Palette className="palette" actions={actions} /> }
326-
<HotKeys
340+
<div
327341
style={{ flex: 1}}
328-
keyMap={this.keyMap}
329-
attach={document.body}
330-
handlers={guardKeyHandlers(this.props, [ 'undo', 'redo' ])}
331342
>
332-
<Canvas className="canvas" location={location} match={match} fetchActionscalled={e => this.props.fetchActions()} saveData={e => this.save()} dirtyflag={this.props.dirty}>
343+
<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}>
333344
<Toolbar>
334345
<ToolbarButton key="undo" icon="icon-redirect" title="Undo" errorMessage="Could not undo." onClick={() => undo()} />
335346
<ToolbarButton key="redo" icon="icon-redirect2" title="Redo" errorMessage="Could not redo." onClick={() => redo()} />
@@ -379,7 +390,7 @@ export default class Workflows extends Component {
379390
</ToolbarDropdown>
380391
</Toolbar>
381392
</Canvas>
382-
</HotKeys>
393+
</div>
383394
{ !isCollapsed.details && <Details className="details" actions={actions} /> }
384395
</div>
385396
</div>

modules/st2flow-canvas/index.js

+67-9
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,14 @@ export default class Canvas extends Component {
229229
dirtyflag: PropTypes.bool,
230230
fetchActionscalled: PropTypes.func,
231231
saveData: PropTypes.func,
232+
undo: PropTypes.func,
233+
redo: PropTypes.func,
234+
save: PropTypes.func,
232235
}
233236

234237
state = {
235238
scale: 0,
239+
copiedTask: null,
236240
}
237241

238242
componentDidMount() {
@@ -714,17 +718,71 @@ export default class Canvas extends Component {
714718
style={{height: '100%'}}
715719
focused={true}
716720
attach={document.body}
717-
handlers={{handleTaskDelete: e => {
718-
// This will break if canvas elements (tasks/transitions) become focus targets with
719-
// tabindex or automatically focusing elements. But in that case, the Task already
720-
// has a handler for delete waiting.
721-
if(e.target === document.body) {
722-
e.preventDefault();
723-
if(selectedTask) {
721+
keyMap={{
722+
copy: [ 'ctrl+c', 'command+c' ],
723+
cut: [ 'ctrl+x', 'command+x' ],
724+
paste: [ 'ctrl+v', 'command+v' ],
725+
open: [ 'ctrl+o', 'command+o' ],
726+
undo: [ 'ctrl+z', 'command+z' ],
727+
redo: [ 'ctrl+shift+z', 'command+shift+z' ],
728+
save: [ 'ctrl+s', 'command+s' ],
729+
}}
730+
handlers={{
731+
copy: () => {
732+
if (selectedTask) {
733+
this.setState({ copiedTask: selectedTask });
734+
}
735+
},
736+
cut: () => {
737+
if (selectedTask) {
738+
this.setState({ copiedTask: selectedTask });
724739
this.handleTaskDelete(selectedTask);
725740
}
726-
}
727-
}}}
741+
},
742+
paste: () => {
743+
if (document.activeElement.tagName === 'TEXTAREA' || document.activeElement.tagName === 'INPUT') {
744+
// allow regular copy/paste from clipboard when inputs or textareas are focused
745+
return;
746+
}
747+
748+
const { copiedTask } = this.state;
749+
if (copiedTask) {
750+
const taskHeight = copiedTask.size.y;
751+
const taskCoords = copiedTask.coords;
752+
753+
const newCoords = {
754+
x: taskCoords.x,
755+
y: taskCoords.y + taskHeight + 10,
756+
};
757+
758+
const lastIndex = tasks
759+
.map(task => (task.name.match(/task(\d+)/) || [])[1])
760+
.reduce((acc, item) => Math.max(acc, item || 0), 0);
761+
762+
this.props.issueModelCommand('addTask', {
763+
name: `task${lastIndex + 1}`,
764+
action: copiedTask.action,
765+
coords: Vector.max(newCoords, new Vector(0, 0)),
766+
});
767+
}
768+
},
769+
open: () => {
770+
if (selectedTask) {
771+
window.open(`${location.origin}/#/action/${selectedTask.action}`, '_blank');
772+
}
773+
},
774+
undo: () => {
775+
this.props.undo();
776+
},
777+
redo: () => {
778+
this.props.redo();
779+
},
780+
save: (e) => {
781+
e.preventDefault();
782+
e.stopPropagation();
783+
this.props.save();
784+
},
785+
}}
728786
>
729787
<div
730788
className={cx(this.props.className, this.style.component)}

modules/st2flow-palette/action.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,22 @@ export default class Action extends Component<{
6767

6868
render() {
6969
const { action } = this.props;
70+
71+
const href = `${location.origin}/#/action/${action.ref}`;
72+
73+
const supportedRunnerTypes = {
74+
'orquesta': href,
75+
'mistral-v2': href
76+
};
77+
7078
return (
71-
<div className={this.style.action} ref={this.actionRef} draggable>
79+
<div
80+
draggable
81+
className={this.style.action}
82+
ref={this.actionRef}
83+
href={supportedRunnerTypes[action.runner_type]}
84+
target="_blank"
85+
rel="noopener noreferrer">
7286
<div className={this.style.actionName}>{ action.ref }</div>
7387
<div className={this.style.actionDescription}>{ action.description }</div>
7488
</div>

modules/st2flow-palette/style.css

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ limitations under the License.
3535

3636
position: relative;
3737

38+
display: block;
39+
3840
&-name {
3941
color: #2d2d2d;
4042
font-size: 14px;

tasks/lint.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const plugins = require('gulp-load-plugins')(settings.plugins);
2121

2222
gulp.task('lint', (done) => gulp.src(settings.lint, { cwd: settings.dev })
2323
.pipe(plugins.plumber())
24-
.pipe(plugins.eslint())
24+
.pipe(plugins.eslint({ fix: true }))
2525
.pipe(plugins.eslint.format())
2626
.pipe(plugins.eslint.failAfterError())
2727
.on('end', () => done())

0 commit comments

Comments
 (0)