Skip to content

Commit 45a9c44

Browse files
authored
Merge pull request #965 from Jappzy/autosave
Auto-Save Changes in the Workflow Composer
2 parents 0011c47 + 0b10474 commit 45a9c44

File tree

8 files changed

+181
-29
lines changed

8 files changed

+181
-29
lines changed

CHANGELOG.rst

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ Added
2020

2121
Contributed by @Jappzy and @cded from @Bitovi
2222

23+
* Added an optional auto-save capability in the workflow composer. #965, #993
24+
25+
Contributed by @Jappzy and @cded from @Bitovi
26+
2327
Changed
2428
~~~~~~~
2529
* Updated nodejs from `14.16.1` to `14.20.1`, fixing the local build under ARM processor architecture. #880

apps/st2-workflows/store.js

+21
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,18 @@ const flowReducer = (state = {}, input) => {
273273
};
274274
}
275275

276+
case 'PUSH_WARNING': {
277+
const { message, link, source } = input;
278+
279+
return {
280+
...state,
281+
notifications: [
282+
...notifications.filter(n => !source || n.source !== source),
283+
{ type: 'warning', message, source, link, id: uniqueId() },
284+
],
285+
};
286+
}
287+
276288
case 'PUSH_SUCCESS': {
277289
const { message, link, source } = input;
278290

@@ -391,6 +403,15 @@ const flowReducer = (state = {}, input) => {
391403
};
392404
}
393405

406+
case 'TOGGLE_AUTOSAVE': {
407+
const { autosaveEnabled } = input;
408+
409+
return {
410+
...state,
411+
autosaveEnabled,
412+
};
413+
}
414+
394415
default:
395416
return state;
396417
}

apps/st2-workflows/workflows.component.js

+35-9
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ export default class Workflows extends Component {
230230
}
231231

232232
save() {
233-
const { pack, meta, actions, workflowSource, metaSource } = this.props;
233+
const { pack, meta, actions, workflowSource, metaSource, sendSuccess, sendError } = this.props;
234234
const existingAction = actions.find(e => e.name === meta.name && e.pack === pack);
235235

236236
if (!meta.name) {
@@ -272,14 +272,31 @@ export default class Workflows extends Component {
272272
// don't need to return anything to the store. the handler will change dirty.
273273
return {};
274274
})();
275-
276-
store.dispatch({
275+
276+
const saveRes = store.dispatch({
277277
type: 'SAVE_WORKFLOW',
278278
promise,
279279
});
280+
281+
saveRes.then(({ status }) => status === 'success' ? sendSuccess('Workflow saved.') : sendError('Error saving workflow.'));
282+
280283
return promise;
281284
}
282285

286+
timer;
287+
288+
autosave(func) {
289+
func.apply(this);
290+
clearTimeout(this.timer);
291+
this.timer = setTimeout(() => {
292+
const { autosaveEnabled } = store.getState();
293+
294+
if (autosaveEnabled) {
295+
this.save();
296+
}
297+
}, 1000);
298+
}
299+
283300
style = style
284301

285302
keyHandlers = {
@@ -340,24 +357,33 @@ export default class Workflows extends Component {
340357
<div
341358
style={{ flex: 1}}
342359
>
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}>
360+
<Canvas
361+
className="canvas"
362+
location={location}
363+
match={match}
364+
dirtyflag={this.props.dirty}
365+
fetchActionscalled={e => this.props.fetchActions()}
366+
saveData={e => this.autosave(() => null)}
367+
save={this.keyHandlers.save}
368+
undo={() => this.autosave(() => this.keyHandlers.undo())}
369+
redo={() => this.autosave(() => this.keyHandlers.redo())}
370+
>
344371
<Toolbar>
345-
<ToolbarButton key="undo" icon="icon-redirect" title="Undo" errorMessage="Could not undo." onClick={() => undo()} />
346-
<ToolbarButton key="redo" icon="icon-redirect2" title="Redo" errorMessage="Could not redo." onClick={() => redo()} />
372+
<ToolbarButton key="undo" icon="icon-redirect" title="Undo" errorMessage="Could not undo." onClick={() => this.autosave(() => undo())} />
373+
<ToolbarButton key="redo" icon="icon-redirect2" title="Redo" errorMessage="Could not redo." onClick={() => this.autosave(() => redo())} />
347374
<ToolbarButton
348375
key="rearrange"
349376
icon="icon-arrange"
350377
title="Rearrange tasks"
351378
successMessage="Rearrange complete."
352379
errorMessage="Error rearranging workflows."
353-
onClick={() => layout()}
380+
onClick={() => this.autosave(() => layout())}
354381
/>
355382
<ToolbarButton
356383
key="save"
357384
className={cx(dirty && 'glow')}
358385
icon="icon-save"
359386
title="Save workflow"
360-
successMessage="Workflow saved."
361387
errorMessage="Error saving workflow."
362388
onClick={() => this.save()}
363389
/>
@@ -391,7 +417,7 @@ export default class Workflows extends Component {
391417
</Toolbar>
392418
</Canvas>
393419
</div>
394-
{ !isCollapsed.details && <Details className="details" actions={actions} /> }
420+
{ !isCollapsed.details && <Details className="details" actions={actions} onChange={() => this.autosave(() => null)} /> }
395421
</div>
396422
</div>
397423

modules/st2flow-canvas/index.js

+30-8
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ import { PropTypes } from 'prop-types';
2929
import cx from 'classnames';
3030
import fp from 'lodash/fp';
3131
import { uniqueId, uniq } from 'lodash';
32+
import isEqual from 'lodash/isEqual';
3233

3334
import Notifications from '@stackstorm/st2flow-notifications';
34-
import {HotKeys} from 'react-hotkeys';
35+
import { HotKeys } from 'react-hotkeys';
3536

3637
import { BoundingBox } from './routing-graph';
3738
import Task from './task';
@@ -46,6 +47,8 @@ import PoissonRectangleSampler from './poisson-rect';
4647

4748
import { origin } from './const';
4849

50+
import store from '../../apps/st2-workflows/store';
51+
4952
import style from './style.css';
5053
type DOMMatrix = {
5154
m11: number,
@@ -257,8 +260,10 @@ export default class Canvas extends Component {
257260
this.handleUpdate();
258261
}
259262

260-
componentDidUpdate() {
263+
componentDidUpdate(prevProps) {
261264
this.handleUpdate();
265+
266+
this.handleAutoSaveUpdates(prevProps);
262267
}
263268

264269
componentWillUnmount() {
@@ -387,11 +392,26 @@ export default class Canvas extends Component {
387392
// finally, place the unplaced tasks. using handleTaskMove will also ensure
388393
// that the placement gets set on the model and the YAML.
389394
needsCoords.forEach(({task, transitionsTo}) => {
390-
this.handleTaskMove(task, sampler.getNext(task.name, transitionsTo),true);
395+
this.handleTaskMove(task, sampler.getNext(task.name, transitionsTo));
391396
});
392397
}
393398
}
394399

400+
handleAutoSaveUpdates(prevProps) {
401+
const {saveData, transitions, tasks} = this.props;
402+
const { autosaveEnabled } = store.getState();
403+
404+
if (autosaveEnabled) {
405+
if(!isEqual(prevProps.transitions, transitions)) {
406+
saveData();
407+
}
408+
409+
if(!isEqual(prevProps.tasks, tasks)) {
410+
this.props.saveData();
411+
}
412+
}
413+
}
414+
395415
handleMouseWheel = (e: Wheel): ?false => {
396416
// considerations on scale factor (BM, 2019-02-07)
397417
// on Chrome Mac and Safari Mac:
@@ -576,16 +596,18 @@ export default class Canvas extends Component {
576596
return false;
577597
}
578598

579-
handleTaskMove = async (task: TaskRefInterface, points: CanvasPoint,autoSave) => {
599+
handleTaskMove = async (task: TaskRefInterface, points: CanvasPoint) => {
580600
const x = points.x;
581601
const y = points.y;
582602
const coords = {x, y};
583603
this.props.issueModelCommand('updateTask', task, { coords });
604+
605+
const { autosaveEnabled } = store.getState();
584606

585-
if(autoSave && !this.props.dirtyflag) {
586-
await this.props.fetchActionscalled();
607+
if (autosaveEnabled && this.props.dirtyflag) {
587608
this.props.saveData();
588-
}
609+
await this.props.fetchActionscalled();
610+
}
589611

590612
}
591613

@@ -807,7 +829,7 @@ export default class Canvas extends Component {
807829
task={task}
808830
selected={task.name === navigation.task && !selectedTransitionGroups.length}
809831
scale={scale}
810-
onMove={(...a) => this.handleTaskMove(task, ...a,false)}
832+
onMove={(...a) => this.handleTaskMove(task, ...a)}
811833
onConnect={(...a) => this.handleTaskConnect(task, ...a)}
812834
onClick={() => this.handleTaskSelect(task)}
813835
onDelete={() => this.handleTaskDelete(task)}

modules/st2flow-details/index.js

+32-4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import TaskDetails from './task-details';
3232
import TaskList from './task-list';
3333

3434
import style from './style.css';
35+
import store from '../../apps/st2-workflows/store';
3536

3637
@connect(
3738
editorConnect
@@ -91,6 +92,8 @@ export default class Details extends Component<{
9192
navigate: PropTypes.func,
9293

9394
actions: PropTypes.array,
95+
96+
onChange: PropTypes.func,
9497
}
9598

9699
sections = [{
@@ -111,11 +114,20 @@ export default class Details extends Component<{
111114
this.props.navigate({ toTasks: undefined, task: undefined });
112115
}
113116

117+
toggleAutosave = (autosaveEnabled) => {
118+
store.dispatch({
119+
type: 'TOGGLE_AUTOSAVE',
120+
autosaveEnabled,
121+
});
122+
}
123+
114124
render() {
115-
const { actions, navigation, navigate } = this.props;
125+
const { actions, navigation, navigate, onChange } = this.props;
116126

117127
const { type = 'metadata', asCode } = navigation;
118128

129+
const { autosaveEnabled } = store.getState();
130+
119131
return (
120132
<div className={cx(this.props.className, this.style.component, asCode && 'code')}>
121133
<Toolbar>
@@ -131,20 +143,36 @@ export default class Details extends Component<{
131143
);
132144
})
133145
}
146+
<div
147+
style={{display: 'flex'}} title="Automatically save the workflow on every change"
148+
>
149+
<input
150+
id='autosave-checkbox'
151+
name='autosave-checkbox'
152+
type='checkbox'
153+
onChange={(e) => {
154+
this.toggleAutosave(e.target.checked);
155+
onChange();
156+
}}
157+
className={cx(style.autosave)}
158+
defaultChecked={autosaveEnabled}
159+
/>
160+
<label id='autosave-checkbox__label' htmlFor='autosave-checkbox' className={cx(style.autosave)}>Autosave</label>
161+
</div>
134162
<ToolbarButton className={cx(style.code, 'icon-code')} selected={asCode} onClick={() => navigate({ asCode: !asCode })} />
135163
</Toolbar>
136164
{
137165
type === 'metadata' && (
138166
asCode
139-
&& <MetaEditor />
167+
&& <MetaEditor onChange={() => onChange()} />
140168
// $FlowFixMe Model is populated via decorator
141-
|| <Meta />
169+
|| <Meta onChange={() => onChange()} />
142170
)
143171
}
144172
{
145173
type === 'execution' && (
146174
asCode
147-
&& <WorkflowEditor selectedTaskName={navigation.task} onTaskSelect={this.handleTaskSelect} />
175+
&& <WorkflowEditor selectedTaskName={navigation.task} onTaskSelect={this.handleTaskSelect} onChange={() => onChange()} />
148176
|| navigation.task
149177
// $FlowFixMe ^^
150178
&& <TaskDetails onBack={this.handleBack} selected={navigation.task} actions={actions} />

modules/st2flow-details/meta-panel.js

+50-7
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,30 @@ export default class Meta extends Component {
9696
actions: PropTypes.array,
9797
vars: PropTypes.array,
9898
setVars: PropTypes.func,
99+
100+
onChange: PropTypes.func,
99101
}
100102

101-
componentDidUpdate() {
103+
componentDidUpdate(prevProps) {
102104
const { meta, setMeta } = this.props;
103105

104106
if (!meta.runner_type) {
105107
setMeta('runner_type', default_runner_type);
106108
}
109+
110+
this.handleAutoSaveUpdates(prevProps);
111+
}
112+
113+
handleAutoSaveUpdates(prevProps) {
114+
const { meta, vars, onChange } = this.props;
115+
116+
if(prevProps.meta !== meta) {
117+
onChange();
118+
}
119+
120+
if(prevProps.vars !== vars) {
121+
onChange();
122+
}
107123
}
108124

109125
handleSectionSwitch(section: string) {
@@ -191,12 +207,39 @@ export default class Meta extends Component {
191207
</Toolbar>,
192208
section === 'meta' && (
193209
<Panel key="meta">
194-
<EnumField name="Runner Type" value={meta.runner_type} spec={{enum: [ ...new Set([ 'mistral-v2', 'orquesta' ]) ], default: default_runner_type}} onChange={(v) => setMeta('runner_type', v)} />
195-
<EnumField name="Pack" value={pack} spec={{enum: packs}} onChange={(v) => setPack(v)} />
196-
<StringField name="Name" value={meta.name} onChange={(v) => this.setMetaNew('name', v || '')} />
197-
<StringField name="Description" value={meta.description} onChange={(v) => setMeta('description', v)} />
198-
<BooleanField name="Enabled" value={meta.enabled} spec={{}} onChange={(v) => setMeta('enabled', v)} />
199-
<StringField name="Entry point" value={meta.entry_point !=='undefined' ? meta.entry_point:`workflows/${meta.name}.yaml`} onChange={(v) => setMeta('entry_point', v || '')} />
210+
<EnumField
211+
name="Runner Type"
212+
value={meta.runner_type}
213+
spec={{enum: [ ...new Set([ 'mistral-v2', 'orquesta' ]) ], default: default_runner_type}}
214+
onChange={(v) => setMeta('runner_type', v)}
215+
/>
216+
<EnumField
217+
name="Pack"
218+
value={pack}
219+
spec={{enum: packs}}
220+
onChange={(v) => setPack(v)}
221+
/>
222+
<StringField
223+
name="Name"
224+
value={meta.name}
225+
onChange={(v) => this.setMetaNew('name', v || '')}
226+
/>
227+
<StringField
228+
name="Description"
229+
value={meta.description}
230+
onChange={(v) => setMeta('description', v)}
231+
/>
232+
<BooleanField
233+
name="Enabled"
234+
value={meta.enabled}
235+
spec={{}}
236+
onChange={(v) => setMeta('enabled', v)}
237+
/>
238+
<StringField
239+
name="Entry point"
240+
value={meta.entry_point !=='undefined' ? meta.entry_point:`workflows/${meta.name}.yaml`}
241+
onChange={(v) => setMeta('entry_point', v || '')}
242+
/>
200243
</Panel>
201244
),
202245
section === 'parameters' && (

modules/st2flow-details/style.css

+6-1
Original file line numberDiff line numberDiff line change
@@ -396,4 +396,9 @@ limitations under the License.
396396
}
397397
.tooltip {
398398
position: relative;
399-
}
399+
}
400+
401+
.autosave {
402+
cursor: pointer;
403+
margin-left: 4px;
404+
}

0 commit comments

Comments
 (0)