feat(editor): Enable saving workflow when node details view is open (#5856)

* feat(editor): Enable saving workflow when node details view is open
*  Preventing event propagation
*  Move save event handler to `NodeDetailsView`
*  Added e2e tests
* 👕 Fixing linting and removing unnecessary event logic

---------

Co-authored-by: Oleg Ivaniv <oleg@n8n.io>
This commit is contained in:
Milorad FIlipović 2023-03-31 13:59:09 +02:00 committed by GitHub
parent 83aef17120
commit 0a59002ef8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 63 additions and 13 deletions

View file

@ -90,6 +90,14 @@ describe('NDV', () => {
}); });
}); });
it('should save workflow using keyboard shortcut from NDV', () => {
workflowPage.actions.addNodeToCanvas('Manual');
workflowPage.actions.addNodeToCanvas('Set', true, true);
ndv.getters.container().should('be.visible');
workflowPage.actions.saveWorkflowUsingKeyboardShortcut();
workflowPage.getters.isWorkflowSaved();
})
describe('test output schema view', () => { describe('test output schema view', () => {
const schemaKeys = ['id', 'name', 'email', 'notes', 'country', 'created', 'objectValue', 'prop1', 'prop2']; const schemaKeys = ['id', 'name', 'email', 'notes', 'country', 'created', 'objectValue', 'prop1', 'prop2'];
function setupSchemaWorkflow() { function setupSchemaWorkflow() {

View file

@ -28,7 +28,13 @@
</div> </div>
</n8n-tooltip> </n8n-tooltip>
<div class="data-display" v-if="activeNode"> <div
v-if="activeNode"
class="data-display"
ref="container"
@keydown.capture="onKeyDown"
tabindex="0"
>
<div @click="close" :class="$style.modalBackground"></div> <div @click="close" :class="$style.modalBackground"></div>
<NDVDraggablePanels <NDVDraggablePanels
:isTriggerNode="isTriggerNode" :isTriggerNode="isTriggerNode"
@ -156,6 +162,7 @@ import { useNDVStore } from '@/stores/ndv';
import { useNodeTypesStore } from '@/stores/nodeTypes'; import { useNodeTypesStore } from '@/stores/nodeTypes';
import { useUIStore } from '@/stores/ui'; import { useUIStore } from '@/stores/ui';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import useDeviceSupport from '@/composables/useDeviceSupport';
export default mixins( export default mixins(
externalHooks, externalHooks,
@ -184,6 +191,11 @@ export default mixins(
default: false, default: false,
}, },
}, },
setup() {
return {
...useDeviceSupport(),
};
},
data() { data() {
return { return {
settingsEventBus: new Vue(), settingsEventBus: new Vue(),
@ -469,6 +481,16 @@ export default mixins(
}, },
}, },
methods: { methods: {
onKeyDown(e: KeyboardEvent) {
if (e.key === 's' && this.isCtrlKeyPressed(e)) {
e.stopPropagation();
e.preventDefault();
if (this.readOnly) return;
this.$emit('saveKeyboardShortcut', e);
}
},
onInputItemHover(e: { itemIndex: number; outputIndex: number } | null) { onInputItemHover(e: { itemIndex: number; outputIndex: number } | null) {
if (!this.inputNodeName) { if (!this.inputNodeName) {
return; return;

View file

@ -40,6 +40,7 @@
"generic.unsavedWork.confirmMessage.confirmButtonText": "Save", "generic.unsavedWork.confirmMessage.confirmButtonText": "Save",
"generic.unsavedWork.confirmMessage.cancelButtonText": "Leave without saving", "generic.unsavedWork.confirmMessage.cancelButtonText": "Leave without saving",
"generic.workflow": "Workflow", "generic.workflow": "Workflow",
"generic.workflowSaved": "Workflow changes saved",
"generic.editor": "Editor", "generic.editor": "Editor",
"about.aboutN8n": "About n8n", "about.aboutN8n": "About n8n",
"about.close": "Close", "about.close": "Close",

View file

@ -92,6 +92,7 @@
:isProductionExecutionPreview="isProductionExecutionPreview" :isProductionExecutionPreview="isProductionExecutionPreview"
@valueChanged="valueChanged" @valueChanged="valueChanged"
@stopExecution="stopExecution" @stopExecution="stopExecution"
@saveKeyboardShortcut="onSaveKeyboardShortcut"
/> />
<node-creation <node-creation
v-if="!isReadOnly" v-if="!isReadOnly"
@ -752,9 +753,24 @@ export default mixins(
return uniqueName; return uniqueName;
}, },
async onSaveKeyboardShortcut() { async onSaveKeyboardShortcut(e: KeyboardEvent) {
const saved = await this.saveCurrentWorkflow(); let saved = await this.saveCurrentWorkflow();
if (saved) await this.settingsStore.fetchPromptsData(); if (saved) await this.settingsStore.fetchPromptsData();
if (this.activeNode) {
// If NDV is open, save will not work from editable input fields
// so don't show success message if this is true
if (e.target instanceof HTMLInputElement) {
saved = e.target.readOnly;
} else {
saved = true;
}
if (saved) {
this.$showMessage({
title: this.$locale.baseText('generic.workflowSaved'),
type: 'success',
});
}
}
}, },
showTriggerCreator(source: NodeCreatorOpenSource) { showTriggerCreator(source: NodeCreatorOpenSource) {
if (this.createNodeActive) return; if (this.createNodeActive) return;
@ -994,6 +1010,19 @@ export default mixins(
} }
}, },
async keyDown(e: KeyboardEvent) { async keyDown(e: KeyboardEvent) {
if (e.key === 's' && this.isCtrlKeyPressed(e)) {
e.stopPropagation();
e.preventDefault();
if (this.isReadOnly) {
return;
}
this.callDebounced('onSaveKeyboardShortcut', { debounceTime: 1000 }, e);
return;
}
// @ts-ignore // @ts-ignore
const path = e.path || (e.composedPath && e.composedPath()); const path = e.path || (e.composedPath && e.composedPath());
@ -1082,16 +1111,6 @@ export default mixins(
title: this.$locale.baseText('nodeView.showMessage.keyDown.title'), title: this.$locale.baseText('nodeView.showMessage.keyDown.title'),
type: 'success', type: 'success',
}); });
} else if (e.key === 's' && this.isCtrlKeyPressed(e)) {
// Save workflow
e.stopPropagation();
e.preventDefault();
if (this.isReadOnly) {
return;
}
this.callDebounced('onSaveKeyboardShortcut', { debounceTime: 1000 });
} else if (e.key === 'Enter') { } else if (e.key === 'Enter') {
// Activate the last selected node // Activate the last selected node
const lastSelectedNode = this.lastSelectedNode; const lastSelectedNode = this.lastSelectedNode;