mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
feat(editor): Add node context menu (#7620)

This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
data-test-id="canvas-node"
|
||||
:ref="data.name"
|
||||
:data-name="data.name"
|
||||
@contextmenu="(e: MouseEvent) => openContextMenu(e, 'node-right-click')"
|
||||
>
|
||||
<div class="select-background" v-show="isSelected"></div>
|
||||
<div
|
||||
@@ -13,6 +14,7 @@
|
||||
'node-default': true,
|
||||
'touch-active': isTouchActive,
|
||||
'is-touch-device': isTouchDevice,
|
||||
'menu-open': isContextMenuOpen,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
@@ -35,7 +37,7 @@
|
||||
:class="{ 'node-info-icon': true, 'shift-icon': shiftOutputCount }"
|
||||
>
|
||||
<div v-if="hasIssues" class="node-issues" data-test-id="node-issues">
|
||||
<n8n-tooltip placement="bottom">
|
||||
<n8n-tooltip :show-after="500" placement="bottom">
|
||||
<template #content>
|
||||
<titled-list :title="`${$locale.baseText('node.issues')}:`" :items="nodeIssues" />
|
||||
</template>
|
||||
@@ -70,6 +72,7 @@
|
||||
<div class="node-trigger-tooltip__wrapper">
|
||||
<n8n-tooltip
|
||||
placement="top"
|
||||
:show-after="500"
|
||||
:visible="showTriggerNodeTooltip"
|
||||
popper-class="node-trigger-tooltip__wrapper--item"
|
||||
>
|
||||
@@ -102,48 +105,22 @@
|
||||
</div>
|
||||
|
||||
<div class="node-options no-select-on-click" v-if="!isReadOnly" v-show="!hideActions">
|
||||
<div
|
||||
v-touch:tap="deleteNode"
|
||||
class="option"
|
||||
:title="$locale.baseText('node.deleteNode')"
|
||||
data-test-id="delete-node-button"
|
||||
>
|
||||
<font-awesome-icon icon="trash" />
|
||||
</div>
|
||||
<div
|
||||
v-touch:tap="disableNode"
|
||||
class="option"
|
||||
:title="$locale.baseText('node.activateDeactivateNode')"
|
||||
data-test-id="disable-node-button"
|
||||
>
|
||||
<font-awesome-icon :icon="nodeDisabledIcon" />
|
||||
</div>
|
||||
<div
|
||||
v-touch:tap="duplicateNode"
|
||||
class="option"
|
||||
:title="$locale.baseText('node.duplicateNode')"
|
||||
v-if="isDuplicatable"
|
||||
data-test-id="duplicate-node-button"
|
||||
>
|
||||
<font-awesome-icon icon="clone" />
|
||||
</div>
|
||||
<div
|
||||
v-touch:tap="setNodeActive"
|
||||
class="option touch"
|
||||
:title="$locale.baseText('node.editNode')"
|
||||
data-test-id="activate-node-button"
|
||||
>
|
||||
<font-awesome-icon class="execute-icon" icon="cog" />
|
||||
</div>
|
||||
<div
|
||||
v-touch:tap="executeNode"
|
||||
class="option"
|
||||
:title="$locale.baseText('node.executeNode')"
|
||||
v-if="!workflowRunning && !isConfigNode"
|
||||
<n8n-icon-button
|
||||
data-test-id="execute-node-button"
|
||||
>
|
||||
<font-awesome-icon class="execute-icon" icon="play-circle" />
|
||||
</div>
|
||||
type="tertiary"
|
||||
text
|
||||
icon="play"
|
||||
:disabled="workflowRunning || isConfigNode"
|
||||
:title="$locale.baseText('node.executeNode')"
|
||||
@click="executeNode"
|
||||
/>
|
||||
<n8n-icon-button
|
||||
data-test-id="overflow-node-button"
|
||||
type="tertiary"
|
||||
text
|
||||
icon="ellipsis-h"
|
||||
@click="(e: MouseEvent) => openContextMenu(e, 'node-button')"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
:class="{
|
||||
@@ -208,9 +185,14 @@ import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { EnableNodeToggleCommand } from '@/models/history';
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
import { type ContextMenuTarget, useContextMenu } from '@/composables';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Node',
|
||||
setup() {
|
||||
const contextMenu = useContextMenu();
|
||||
return { contextMenu };
|
||||
},
|
||||
mixins: [externalHooks, nodeBase, nodeHelpers, workflowHelpers, pinData, debounceHelper],
|
||||
components: {
|
||||
TitledList,
|
||||
@@ -542,6 +524,13 @@ export default defineComponent({
|
||||
!this.dragging
|
||||
);
|
||||
},
|
||||
isContextMenuOpen(): boolean {
|
||||
return (
|
||||
this.contextMenu.isOpen.value &&
|
||||
this.contextMenu.target.value.source === 'node-button' &&
|
||||
this.contextMenu.target.value.node.name === this.data?.name
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
isActive(newValue, oldValue) {
|
||||
@@ -667,27 +656,6 @@ export default defineComponent({
|
||||
workflow_id: this.workflowsStore.workflowId,
|
||||
});
|
||||
},
|
||||
async deleteNode() {
|
||||
this.$telemetry.track('User clicked node hover button', {
|
||||
node_type: this.data.type,
|
||||
button_name: 'delete',
|
||||
workflow_id: this.workflowsStore.workflowId,
|
||||
});
|
||||
|
||||
// Wait a tick else vue causes problems because the data is gone
|
||||
await this.$nextTick();
|
||||
this.$emit('removeNode', this.data.name);
|
||||
},
|
||||
async duplicateNode() {
|
||||
this.$telemetry.track('User clicked node hover button', {
|
||||
node_type: this.data.type,
|
||||
button_name: 'duplicate',
|
||||
workflow_id: this.workflowsStore.workflowId,
|
||||
});
|
||||
// Wait a tick else vue causes problems because the data is gone
|
||||
await this.$nextTick();
|
||||
this.$emit('duplicateNode', this.data.name);
|
||||
},
|
||||
|
||||
onClick(event: MouseEvent) {
|
||||
void this.callDebounced('onClickDebounced', { debounceTime: 50, trailing: true }, event);
|
||||
@@ -714,11 +682,20 @@ export default defineComponent({
|
||||
}, 2000);
|
||||
}
|
||||
},
|
||||
openContextMenu(event: MouseEvent, source: ContextMenuTarget['source']) {
|
||||
if (this.data) {
|
||||
this.contextMenu.open(event, { source, node: this.data });
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.context-menu {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.node-wrapper {
|
||||
--node-width: 100px;
|
||||
/*
|
||||
@@ -792,13 +769,11 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
&.touch-active,
|
||||
&:hover {
|
||||
.node-execute {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.menu-open {
|
||||
.node-options {
|
||||
display: initial;
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -860,19 +835,27 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.node-options {
|
||||
display: none;
|
||||
--node-options-height: 26px;
|
||||
:deep(.button) {
|
||||
--button-font-color: var(--color-text-light);
|
||||
}
|
||||
position: absolute;
|
||||
top: -25px;
|
||||
left: -10px;
|
||||
width: calc(var(--node-width) + 20px);
|
||||
height: 26px;
|
||||
font-size: 0.9em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-2xs);
|
||||
transition: opacity 100ms ease-in;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
top: calc(-1 * (var(--node-options-height) + var(--spacing-4xs)));
|
||||
left: 0;
|
||||
width: var(--node-width);
|
||||
height: var(--node-options-height);
|
||||
font-size: var(--font-size-s);
|
||||
z-index: 10;
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
|
||||
.option {
|
||||
width: 28px;
|
||||
display: inline-block;
|
||||
|
||||
&.touch {
|
||||
@@ -885,8 +868,7 @@ export default defineComponent({
|
||||
|
||||
.execute-icon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
font-size: 1.2em;
|
||||
font-size: var(----font-size-xl);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user