mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Automatically tidy up workflows (#13471)
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M1.6.13c-.18-.17-.47-.18-.62 0L.56.57.14.98c-.2.15-.18.44 0 .62l3.63 3.6c.1.1.1.27 0 .37-.2.2-.53.52-.93.94-.56.57-.12 1.62.22 2.11.05.07.12.1.2.1.05-.01.1-.04.15-.08l5.23-5.22c.1-.1.1-.26-.02-.34-.5-.34-1.55-.78-2.12-.22-.42.4-.75.73-.94.93-.1.1-.27.1-.37 0L1.6.13ZM9.5 3.9c.07-.09.2-.1.3-.04l6.07 3.44c.15.08.18.29.05.4l-1.21 1.22a.26.26 0 0 1-.26.07l-2.18-.64a.26.26 0 0 0-.32.33l.76 2.02c.04.1.01.2-.06.27L7.7 15.92a.26.26 0 0 1-.41-.05L3.83 9.8a.26.26 0 0 1 .04-.3l5.62-5.6Z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -18,7 +18,17 @@ import { useVueFlow, VueFlow, PanelPosition, MarkerType } from '@vue-flow/core';
|
||||
import { MiniMap } from '@vue-flow/minimap';
|
||||
import Node from './elements/nodes/CanvasNode.vue';
|
||||
import Edge from './elements/edges/CanvasEdge.vue';
|
||||
import { computed, onMounted, onUnmounted, provide, ref, toRef, useCssModule, watch } from 'vue';
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
provide,
|
||||
ref,
|
||||
toRef,
|
||||
useCssModule,
|
||||
watch,
|
||||
} from 'vue';
|
||||
import type { EventBus } from '@n8n/utils/event-bus';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import { useDeviceSupport } from '@n8n/composables/useDeviceSupport';
|
||||
@@ -37,6 +47,7 @@ import CanvasBackground from './elements/background/CanvasBackground.vue';
|
||||
import { useCanvasTraversal } from '@/composables/useCanvasTraversal';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
import { useCanvasNodeHover } from '@/composables/useCanvasNodeHover';
|
||||
import { useCanvasLayout } from '@/composables/useCanvasLayout';
|
||||
|
||||
const $style = useCssModule();
|
||||
|
||||
@@ -139,6 +150,7 @@ const {
|
||||
getDownstreamNodes,
|
||||
getUpstreamNodes,
|
||||
} = useCanvasTraversal(vueFlow);
|
||||
const { layout } = useCanvasLayout({ id: props.id });
|
||||
|
||||
const isPaneReady = ref(false);
|
||||
|
||||
@@ -245,38 +257,43 @@ function selectUpstreamNodes(id: string) {
|
||||
onSelectNodes({ ids: [...upstreamNodes.map((node) => node.id), id] });
|
||||
}
|
||||
|
||||
const keyMap = computed(() => ({
|
||||
ctrl_c: emitWithSelectedNodes((ids) => emit('copy:nodes', ids)),
|
||||
enter: emitWithLastSelectedNode((id) => onSetNodeActivated(id)),
|
||||
ctrl_a: () => addSelectedNodes(graphNodes.value),
|
||||
// Support both key and code for zooming in and out
|
||||
'shift_+|+|=|shift_Equal|Equal': async () => await onZoomIn(),
|
||||
'shift+_|-|_|shift_Minus|Minus': async () => await onZoomOut(),
|
||||
0: async () => await onResetZoom(),
|
||||
1: async () => await onFitView(),
|
||||
ArrowUp: emitWithLastSelectedNode(selectUpperSiblingNode),
|
||||
ArrowDown: emitWithLastSelectedNode(selectLowerSiblingNode),
|
||||
ArrowLeft: emitWithLastSelectedNode(selectLeftNode),
|
||||
ArrowRight: emitWithLastSelectedNode(selectRightNode),
|
||||
shift_ArrowLeft: emitWithLastSelectedNode(selectUpstreamNodes),
|
||||
shift_ArrowRight: emitWithLastSelectedNode(selectDownstreamNodes),
|
||||
const keyMap = computed(() => {
|
||||
const readOnlyKeymap = {
|
||||
ctrl_c: emitWithSelectedNodes((ids) => emit('copy:nodes', ids)),
|
||||
enter: emitWithLastSelectedNode((id) => onSetNodeActivated(id)),
|
||||
ctrl_a: () => addSelectedNodes(graphNodes.value),
|
||||
// Support both key and code for zooming in and out
|
||||
'shift_+|+|=|shift_Equal|Equal': async () => await onZoomIn(),
|
||||
'shift+_|-|_|shift_Minus|Minus': async () => await onZoomOut(),
|
||||
0: async () => await onResetZoom(),
|
||||
1: async () => await onFitView(),
|
||||
ArrowUp: emitWithLastSelectedNode(selectUpperSiblingNode),
|
||||
ArrowDown: emitWithLastSelectedNode(selectLowerSiblingNode),
|
||||
ArrowLeft: emitWithLastSelectedNode(selectLeftNode),
|
||||
ArrowRight: emitWithLastSelectedNode(selectRightNode),
|
||||
shift_ArrowLeft: emitWithLastSelectedNode(selectUpstreamNodes),
|
||||
shift_ArrowRight: emitWithLastSelectedNode(selectDownstreamNodes),
|
||||
};
|
||||
|
||||
...(props.readOnly
|
||||
? {}
|
||||
: {
|
||||
ctrl_x: emitWithSelectedNodes((ids) => emit('cut:nodes', ids)),
|
||||
'delete|backspace': emitWithSelectedNodes((ids) => emit('delete:nodes', ids)),
|
||||
ctrl_d: emitWithSelectedNodes((ids) => emit('duplicate:nodes', ids)),
|
||||
d: emitWithSelectedNodes((ids) => emit('update:nodes:enabled', ids)),
|
||||
p: emitWithSelectedNodes((ids) => emit('update:nodes:pin', ids, 'keyboard-shortcut')),
|
||||
f2: emitWithLastSelectedNode((id) => emit('update:node:name', id)),
|
||||
tab: () => emit('create:node', 'tab'),
|
||||
shift_s: () => emit('create:sticky'),
|
||||
ctrl_alt_n: () => emit('create:workflow'),
|
||||
ctrl_enter: () => emit('run:workflow'),
|
||||
ctrl_s: () => emit('save:workflow'),
|
||||
}),
|
||||
}));
|
||||
if (props.readOnly) return readOnlyKeymap;
|
||||
|
||||
const fullKeymap = {
|
||||
...readOnlyKeymap,
|
||||
ctrl_x: emitWithSelectedNodes((ids) => emit('cut:nodes', ids)),
|
||||
'delete|backspace': emitWithSelectedNodes((ids) => emit('delete:nodes', ids)),
|
||||
ctrl_d: emitWithSelectedNodes((ids) => emit('duplicate:nodes', ids)),
|
||||
d: emitWithSelectedNodes((ids) => emit('update:nodes:enabled', ids)),
|
||||
p: emitWithSelectedNodes((ids) => emit('update:nodes:pin', ids, 'keyboard-shortcut')),
|
||||
f2: emitWithLastSelectedNode((id) => emit('update:node:name', id)),
|
||||
tab: () => emit('create:node', 'tab'),
|
||||
shift_s: () => emit('create:sticky'),
|
||||
ctrl_alt_n: () => emit('create:workflow'),
|
||||
ctrl_enter: () => emit('run:workflow'),
|
||||
ctrl_s: () => emit('save:workflow'),
|
||||
shift_alt_t: onTidyUp,
|
||||
};
|
||||
return fullKeymap;
|
||||
});
|
||||
|
||||
useKeybindings(keyMap, { disabled: disableKeyBindings });
|
||||
|
||||
@@ -589,7 +606,7 @@ function onOpenNodeContextMenu(
|
||||
contextMenu.open(event, { source, nodeId: id });
|
||||
}
|
||||
|
||||
function onContextMenuAction(action: ContextMenuAction, nodeIds: string[]) {
|
||||
async function onContextMenuAction(action: ContextMenuAction, nodeIds: string[]) {
|
||||
switch (action) {
|
||||
case 'add_node':
|
||||
return emit('create:node', 'context_menu');
|
||||
@@ -617,6 +634,20 @@ function onContextMenuAction(action: ContextMenuAction, nodeIds: string[]) {
|
||||
return emit('update:node:name', nodeIds[0]);
|
||||
case 'change_color':
|
||||
return props.eventBus.emit('nodes:action', { ids: nodeIds, action: 'update:sticky:color' });
|
||||
case 'tidy_up':
|
||||
return await onTidyUp();
|
||||
}
|
||||
}
|
||||
|
||||
async function onTidyUp() {
|
||||
const applyOnSelection = selectedNodes.value.length > 1;
|
||||
const { nodes } = layout(applyOnSelection ? 'selection' : 'all');
|
||||
|
||||
onUpdateNodesPosition(nodes.map((node) => ({ id: node.id, position: { x: node.x, y: node.y } })));
|
||||
|
||||
if (!applyOnSelection) {
|
||||
await nextTick();
|
||||
await onFitView();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -840,6 +871,7 @@ provide(CanvasKey, {
|
||||
@zoom-in="onZoomIn"
|
||||
@zoom-out="onZoomOut"
|
||||
@reset-zoom="onResetZoom"
|
||||
@tidy-up="onTidyUp"
|
||||
/>
|
||||
|
||||
<Suspense>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Controls } from '@vue-flow/controls';
|
||||
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
|
||||
import TidyUpIcon from '@/components/TidyUpIcon.vue';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
|
||||
@@ -18,6 +19,7 @@ const emit = defineEmits<{
|
||||
'zoom-in': [];
|
||||
'zoom-out': [];
|
||||
'zoom-to-fit': [];
|
||||
'tidy-up': [];
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
@@ -39,6 +41,10 @@ function onZoomOut() {
|
||||
function onZoomToFit() {
|
||||
emit('zoom-to-fit');
|
||||
}
|
||||
|
||||
function onTidyUp() {
|
||||
emit('tidy-up');
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Controls :show-zoom="false" :show-fit-view="false">
|
||||
@@ -85,9 +91,37 @@ function onZoomToFit() {
|
||||
@click="onResetZoom"
|
||||
/>
|
||||
</KeyboardShortcutTooltip>
|
||||
<KeyboardShortcutTooltip
|
||||
:label="i18n.baseText('nodeView.tidyUp')"
|
||||
:shortcut="{ shiftKey: true, altKey: true, keys: ['T'] }"
|
||||
>
|
||||
<N8nButton
|
||||
square
|
||||
type="tertiary"
|
||||
size="large"
|
||||
data-test-id="tidy-up-button"
|
||||
:class="$style.iconButton"
|
||||
@click="onTidyUp"
|
||||
>
|
||||
<TidyUpIcon />
|
||||
</N8nButton>
|
||||
</KeyboardShortcutTooltip>
|
||||
</Controls>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.iconButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.vue-flow__controls {
|
||||
display: flex;
|
||||
|
||||
@@ -20,6 +20,10 @@ exports[`CanvasControlButtons > should render correctly 1`] = `
|
||||
</button>
|
||||
<!--teleport start-->
|
||||
<!--teleport end-->
|
||||
<!--v-if-->
|
||||
<!--v-if--><button class="button button tertiary large square iconButton el-tooltip__trigger el-tooltip__trigger iconButton el-tooltip__trigger el-tooltip__trigger" aria-live="polite" data-test-id="tidy-up-button">
|
||||
<!--v-if--><span><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M1.6.13c-.18-.17-.47-.18-.62 0L.56.57.14.98c-.2.15-.18.44 0 .62l3.63 3.6c.1.1.1.27 0 .37-.2.2-.53.52-.93.94-.56.57-.12 1.62.22 2.11.05.07.12.1.2.1.05-.01.1-.04.15-.08l5.23-5.22c.1-.1.1-.26-.02-.34-.5-.34-1.55-.78-2.12-.22-.42.4-.75.73-.94.93-.1.1-.27.1-.37 0L1.6.13ZM9.5 3.9c.07-.09.2-.1.3-.04l6.07 3.44c.15.08.18.29.05.4l-1.21 1.22a.26.26 0 0 1-.26.07l-2.18-.64a.26.26 0 0 0-.32.33l.76 2.02c.04.1.01.2-.06.27L7.7 15.92a.26.26 0 0 1-.41-.05L3.83 9.8a.26.26 0 0 1 .04-.3l5.62-5.6Z"></path></svg></span>
|
||||
</button>
|
||||
<!--teleport start-->
|
||||
<!--teleport end-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user