mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 01:26:44 +00:00
feat(editor): Zoom into a node to open experimental embedded NDV (no-changelog) (#16912)
Co-authored-by: Milorad FIlipović <milorad@n8n.io>
This commit is contained in:
@@ -66,6 +66,7 @@ import IconLucideCode from '~icons/lucide/code';
|
||||
import IconLucideCog from '~icons/lucide/cog';
|
||||
import IconLucideContrast from '~icons/lucide/contrast';
|
||||
import IconLucideCopy from '~icons/lucide/copy';
|
||||
import IconLucideCrosshair from '~icons/lucide/crosshair';
|
||||
import IconLucideDatabase from '~icons/lucide/database';
|
||||
import IconLucideEarth from '~icons/lucide/earth';
|
||||
import IconLucideEllipsis from '~icons/lucide/ellipsis';
|
||||
@@ -460,6 +461,7 @@ export const updatedIconSet = {
|
||||
cog: IconLucideCog,
|
||||
contrast: IconLucideContrast,
|
||||
copy: IconLucideCopy,
|
||||
crosshair: IconLucideCrosshair,
|
||||
database: IconLucideDatabase,
|
||||
earth: IconLucideEarth,
|
||||
ellipsis: IconLucideEllipsis,
|
||||
|
||||
@@ -1556,6 +1556,8 @@
|
||||
"nodeView.zoomToFit": "Zoom to Fit",
|
||||
"nodeView.replaceMe": "Replace Me",
|
||||
"nodeView.setupTemplate": "Set up template",
|
||||
"nodeView.expandAllNodes": "Expand all nodes",
|
||||
"nodeView.collapseAllNodes": "Collapse all nodes",
|
||||
"nodeViewV2.showError.editingNotAllowed": "Editing is not allowed",
|
||||
"nodeViewV2.showError.failedToCreateNode": "Failed to create node",
|
||||
"contextMenu.node": "node | nodes",
|
||||
|
||||
@@ -20,7 +20,6 @@ import type {
|
||||
import { useActions } from './NodeCreator/composables/useActions';
|
||||
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useExperimentalNdvStore } from '../canvas/experimental/experimentalNdv.store';
|
||||
|
||||
type Props = {
|
||||
nodeViewScale: number;
|
||||
@@ -45,7 +44,6 @@ const uiStore = useUIStore();
|
||||
const focusPanelStore = useFocusPanelStore();
|
||||
const posthogStore = usePostHog();
|
||||
const i18n = useI18n();
|
||||
const experimentalNdvStore = useExperimentalNdvStore();
|
||||
|
||||
const { getAddedNodesAndConnections } = useActions();
|
||||
|
||||
@@ -127,20 +125,6 @@ function nodeTypeSelected(value: NodeTypeSelectedPayload[]) {
|
||||
@click="focusPanelStore.toggleFocusPanel"
|
||||
/>
|
||||
</KeyboardShortcutTooltip>
|
||||
<n8n-icon-button
|
||||
v-if="experimentalNdvStore.isEnabled"
|
||||
type="tertiary"
|
||||
size="large"
|
||||
icon="maximize-2"
|
||||
@click="experimentalNdvStore.expandAllNodes"
|
||||
/>
|
||||
<n8n-icon-button
|
||||
v-if="experimentalNdvStore.isEnabled"
|
||||
type="tertiary"
|
||||
size="large"
|
||||
icon="minimize-2"
|
||||
@click="experimentalNdvStore.collapseAllNodes"
|
||||
/>
|
||||
</div>
|
||||
<Suspense>
|
||||
<LazyNodeCreator
|
||||
|
||||
@@ -4,6 +4,7 @@ import TidyUpIcon from '@/components/TidyUpIcon.vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { Controls } from '@vue-flow/controls';
|
||||
import { computed } from 'vue';
|
||||
import { useExperimentalNdvStore } from '../../experimental/experimentalNdv.store';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -26,6 +27,8 @@ const emit = defineEmits<{
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const experimentalNdvStore = useExperimentalNdvStore();
|
||||
|
||||
const isResetZoomVisible = computed(() => props.zoom !== 1);
|
||||
|
||||
function onResetZoom() {
|
||||
@@ -109,6 +112,30 @@ function onTidyUp() {
|
||||
<TidyUpIcon />
|
||||
</N8nButton>
|
||||
</KeyboardShortcutTooltip>
|
||||
<N8nTooltip
|
||||
v-if="experimentalNdvStore.isActive(props.zoom)"
|
||||
placement="top"
|
||||
:content="i18n.baseText('nodeView.expandAllNodes')"
|
||||
>
|
||||
<N8nIconButton
|
||||
type="tertiary"
|
||||
size="large"
|
||||
icon="maximize-2"
|
||||
@click="experimentalNdvStore.expandAllNodes"
|
||||
/>
|
||||
</N8nTooltip>
|
||||
<N8nTooltip
|
||||
v-if="experimentalNdvStore.isActive(props.zoom)"
|
||||
placement="top"
|
||||
:content="i18n.baseText('nodeView.collapseAllNodes')"
|
||||
>
|
||||
<N8nIconButton
|
||||
type="tertiary"
|
||||
size="large"
|
||||
icon="minimize-2"
|
||||
@click="experimentalNdvStore.collapseAllNodes"
|
||||
/>
|
||||
</N8nTooltip>
|
||||
</Controls>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -21,5 +21,7 @@ exports[`CanvasControlButtons > should render correctly 1`] = `
|
||||
<n8n-button-stub block="false" element="button" label="" square="true" active="false" disabled="false" loading="false" outline="false" size="large" text="false" type="tertiary" data-test-id="tidy-up-button" class="iconButton el-tooltip__trigger el-tooltip__trigger"></n8n-button-stub>
|
||||
<!--teleport start-->
|
||||
<!--teleport end-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
@@ -35,6 +35,7 @@ import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import CanvasNodeTrigger from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeTrigger.vue';
|
||||
import { CONFIGURATION_NODE_OFFSET, GRID_SIZE } from '@/utils/nodeViewUtils';
|
||||
import { useExperimentalNdvStore } from '../../experimental/experimentalNdv.store';
|
||||
|
||||
type Props = NodeProps<CanvasNodeData> & {
|
||||
readOnly?: boolean;
|
||||
@@ -72,7 +73,9 @@ const props = defineProps<Props>();
|
||||
|
||||
const contextMenu = useContextMenu();
|
||||
|
||||
const { connectingHandle } = useCanvas();
|
||||
const { connectingHandle, viewport } = useCanvas();
|
||||
|
||||
const experimentalNdvStore = useExperimentalNdvStore();
|
||||
|
||||
/*
|
||||
Toolbar slot classes
|
||||
@@ -96,6 +99,10 @@ const {
|
||||
|
||||
const isDisabled = computed(() => props.data.disabled);
|
||||
|
||||
const isExperimentalEmbeddedNdvShown = computed(() =>
|
||||
experimentalNdvStore.isActive(viewport.value.zoom),
|
||||
);
|
||||
|
||||
const classes = computed(() => ({
|
||||
[style.canvasNode]: true,
|
||||
[style.showToolbar]: showToolbar.value,
|
||||
@@ -187,7 +194,9 @@ const createEndpointMappingFn =
|
||||
const offsetValue =
|
||||
position === Position.Bottom
|
||||
? `${GRID_SIZE * 2 * (1 + index * 2) + CONFIGURATION_NODE_OFFSET}px`
|
||||
: `${(100 / (endpoints.length + 1)) * (index + 1)}%`;
|
||||
: isExperimentalEmbeddedNdvShown.value && endpoints.length === 1
|
||||
? `${(1 + index) * (GRID_SIZE * 2)}px`
|
||||
: `${(100 / (endpoints.length + 1)) * (index + 1)}%`;
|
||||
|
||||
return {
|
||||
...endpoint,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { CanvasNodeRenderType } from '@/types';
|
||||
import { useCanvas } from '@/composables/useCanvas';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useExperimentalNdvStore } from '../../experimental/experimentalNdv.store';
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [];
|
||||
@@ -27,8 +28,9 @@ const { isDisabled, render, name } = useCanvasNode();
|
||||
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const experimentalNdvStore = useExperimentalNdvStore();
|
||||
|
||||
const node = computed(() => !!name.value && workflowsStore.getNodeByName(name.value));
|
||||
const node = computed(() => (name.value ? workflowsStore.getNodeByName(name.value) : null));
|
||||
const isToolNode = computed(() => !!node.value && nodeTypesStore.isToolNode(node.value.type));
|
||||
|
||||
const nodeDisabledTitle = computed(() => {
|
||||
@@ -59,6 +61,13 @@ const isDisableNodeVisible = computed(() => {
|
||||
|
||||
const isDeleteNodeVisible = computed(() => !props.readOnly);
|
||||
|
||||
const isFocusNodeVisible = computed(
|
||||
() =>
|
||||
experimentalNdvStore.isEnabled &&
|
||||
node.value !== null &&
|
||||
experimentalNdvStore.collapsedNodes[node.value.id] !== false,
|
||||
);
|
||||
|
||||
const isStickyNoteChangeColorVisible = computed(
|
||||
() => !props.readOnly && render.value.type === CanvasNodeRenderType.StickyNote,
|
||||
);
|
||||
@@ -92,6 +101,12 @@ function onMouseEnter() {
|
||||
function onMouseLeave() {
|
||||
isHovered.value = false;
|
||||
}
|
||||
|
||||
function onFocusNode() {
|
||||
if (node.value) {
|
||||
experimentalNdvStore.focusNode(node.value.id);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -139,6 +154,14 @@ function onMouseLeave() {
|
||||
:title="i18n.baseText('node.delete')"
|
||||
@click="onDeleteNode"
|
||||
/>
|
||||
<N8nIconButton
|
||||
v-if="isFocusNodeVisible"
|
||||
type="tertiary"
|
||||
size="small"
|
||||
text
|
||||
icon="crosshair"
|
||||
@click="onFocusNode"
|
||||
/>
|
||||
<CanvasNodeStickyColorSelector
|
||||
v-if="isStickyNoteChangeColorVisible"
|
||||
v-model:visible="isStickyColorSelectorOpen"
|
||||
|
||||
@@ -26,6 +26,7 @@ const {
|
||||
outputs,
|
||||
connections,
|
||||
isDisabled,
|
||||
isReadOnly,
|
||||
isSelected,
|
||||
hasPinnedData,
|
||||
executionStatus,
|
||||
@@ -136,6 +137,8 @@ function onActivate(event: MouseEvent) {
|
||||
:node-id="id"
|
||||
:class="classes"
|
||||
:style="styles"
|
||||
:is-read-only="isReadOnly"
|
||||
:is-configurable="renderOptions.configurable ?? false"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
|
||||
@@ -6,7 +6,11 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const { nodeId, noWheel } = defineProps<{ nodeId: string; noWheel?: boolean }>();
|
||||
const { nodeId, noWheel, isReadOnly } = defineProps<{
|
||||
nodeId: string;
|
||||
noWheel?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
}>();
|
||||
|
||||
defineSlots<{ actions?: {} }>();
|
||||
|
||||
@@ -30,7 +34,7 @@ function handleValueChanged(parameterData: IUpdateInformation) {
|
||||
:active-node="activeNode"
|
||||
push-ref=""
|
||||
:foreign-credentials="[]"
|
||||
:read-only="false"
|
||||
:read-only="isReadOnly"
|
||||
:block-u-i="false"
|
||||
:executable="false"
|
||||
:input-size="0"
|
||||
|
||||
@@ -9,7 +9,11 @@ import { N8nIcon, N8nIconButton } from '@n8n/design-system';
|
||||
import { useVueFlow } from '@vue-flow/core';
|
||||
import { watchOnce } from '@vueuse/core';
|
||||
|
||||
const { nodeId } = defineProps<{ nodeId: string }>();
|
||||
const { nodeId, isReadOnly, isConfigurable } = defineProps<{
|
||||
nodeId: string;
|
||||
isReadOnly?: boolean;
|
||||
isConfigurable: boolean;
|
||||
}>();
|
||||
|
||||
const experimentalNdvStore = useExperimentalNdvStore();
|
||||
const isExpanded = computed(() => !experimentalNdvStore.collapsedNodes[nodeId]);
|
||||
@@ -22,7 +26,7 @@ const nodeType = computed(() => {
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const vf = useVueFlow(workflowsStore.workflowId);
|
||||
const vf = useVueFlow();
|
||||
|
||||
const isMoving = ref(false);
|
||||
|
||||
@@ -65,7 +69,10 @@ function handleToggleExpand() {
|
||||
<div
|
||||
ref="container"
|
||||
:class="[$style.component, isExpanded ? $style.expanded : $style.collapsed]"
|
||||
:style="{ '--zoom': `${1 / experimentalNdvStore.maxCanvasZoom}` }"
|
||||
:style="{
|
||||
'--zoom': `${1 / experimentalNdvStore.maxCanvasZoom}`,
|
||||
'--node-width-scaler': isConfigurable ? 1 : 1.5,
|
||||
}"
|
||||
>
|
||||
<template v-if="isOnceVisible">
|
||||
<ExperimentalCanvasNodeSettings
|
||||
@@ -75,6 +82,7 @@ function handleToggleExpand() {
|
||||
:no-wheel="
|
||||
!isMoving /* to not interrupt panning while allowing scroll of the settings pane, allow wheel event while panning */
|
||||
"
|
||||
:is-read-only="isReadOnly"
|
||||
>
|
||||
<template #actions>
|
||||
<N8nIconButton
|
||||
@@ -107,20 +115,17 @@ function handleToggleExpand() {
|
||||
position: relative;
|
||||
align-items: flex-start;
|
||||
justify-content: stretch;
|
||||
overflow: visible;
|
||||
border-width: 0 !important;
|
||||
outline: none;
|
||||
box-shadow: none !important;
|
||||
background-color: transparent;
|
||||
width: calc(var(--canvas-node--width) * 1.5);
|
||||
overflow: hidden;
|
||||
border-width: 1px !important;
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
width: calc(var(--canvas-node--width) * var(--node-width-scaler));
|
||||
|
||||
&.expanded {
|
||||
cursor: default;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
height: 50px;
|
||||
margin-block: calc(var(--canvas-node--width) * 0.25);
|
||||
height: calc(16px * 4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,28 +139,12 @@ function handleToggleExpand() {
|
||||
|
||||
:root .collapsedContent,
|
||||
:root .settingsView {
|
||||
border-radius: var(--border-radius-base);
|
||||
border: 1px solid var(--canvas-node--border-color, var(--color-foreground-xdark));
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
|
||||
:global(.selected) & {
|
||||
box-shadow: 0 0 0 4px var(--color-canvas-selected-transparent);
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
& > * {
|
||||
zoom: var(--zoom);
|
||||
}
|
||||
}
|
||||
|
||||
:root .settingsView {
|
||||
height: auto;
|
||||
max-height: min(200%, 300px);
|
||||
top: -10%;
|
||||
min-height: 120%;
|
||||
max-height: min(calc(var(--canvas-node--height) * 2), 300px);
|
||||
min-height: var(--spacing-3xl); // should be multiple of GRID_SIZE
|
||||
}
|
||||
|
||||
.collapsedContent {
|
||||
@@ -169,5 +158,15 @@ function handleToggleExpand() {
|
||||
background-color: var(--color-background-xlight);
|
||||
color: var(--color-text-base);
|
||||
cursor: pointer;
|
||||
|
||||
& > * {
|
||||
zoom: calc(var(--zoom) * 1.25);
|
||||
}
|
||||
}
|
||||
|
||||
.settingsView {
|
||||
& > * {
|
||||
zoom: var(--zoom);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { computed, shallowRef } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useVueFlow } from '@vue-flow/core';
|
||||
import { calculateNodeSize } from '@/utils/nodeViewUtils';
|
||||
|
||||
export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
|
||||
const workflowStore = useWorkflowsStore();
|
||||
@@ -20,7 +22,7 @@ export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
|
||||
function setNodeExpanded(nodeId: string, isExpanded?: boolean) {
|
||||
collapsedNodes.value = {
|
||||
...collapsedNodes.value,
|
||||
[nodeId]: isExpanded ?? !collapsedNodes.value[nodeId],
|
||||
[nodeId]: isExpanded === undefined ? !collapsedNodes.value[nodeId] : !isExpanded,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,6 +44,40 @@ export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
|
||||
return isEnabled.value && canvasZoom === maxCanvasZoom.value;
|
||||
}
|
||||
|
||||
function focusNode(nodeId: string) {
|
||||
const nodeToFocus = workflowStore.getNodeById(nodeId);
|
||||
|
||||
if (!nodeToFocus) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Call useVueFlow() here because having it in setup fn scope seem to cause initialization problem
|
||||
const vueFlow = useVueFlow(workflowStore.workflow.id);
|
||||
|
||||
collapsedNodes.value = workflowStore.allNodes.reduce<Partial<Record<string, boolean>>>(
|
||||
(acc, node) => {
|
||||
acc[node.id] = node.id !== nodeId;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const workflow = workflowStore.getCurrentWorkflow();
|
||||
const nodeSize = calculateNodeSize(
|
||||
workflow.getChildNodes(nodeToFocus.name, 'ALL_NON_MAIN').length > 0,
|
||||
workflow.getParentNodes(nodeToFocus.name, 'ALL_NON_MAIN').length > 0,
|
||||
workflow.getParentNodes(nodeToFocus.name, 'main').length,
|
||||
workflow.getChildNodes(nodeToFocus.name, 'main').length,
|
||||
workflow.getParentNodes(nodeToFocus.name, 'ALL_NON_MAIN').length,
|
||||
);
|
||||
|
||||
void vueFlow.setCenter(
|
||||
nodeToFocus.position[0] + (nodeSize.width * 1.5) / 2,
|
||||
nodeToFocus.position[1] + 80,
|
||||
{ duration: 200, zoom: maxCanvasZoom.value },
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
isEnabled,
|
||||
maxCanvasZoom,
|
||||
@@ -50,5 +86,6 @@ export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
|
||||
setNodeExpanded,
|
||||
expandAllNodes,
|
||||
collapseAllNodes,
|
||||
focusNode,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useUIStore } from '@/stores/ui.store';
|
||||
import { shallowRef, watch } from 'vue';
|
||||
import { computed, type ComputedRef } from 'vue';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useExperimentalNdvStore } from '@/components/canvas/experimental/experimentalNdv.store';
|
||||
|
||||
export function useLogsSelection(
|
||||
execution: ComputedRef<IExecutionResponse | undefined>,
|
||||
@@ -33,13 +34,19 @@ export function useLogsSelection(
|
||||
const uiStore = useUIStore();
|
||||
const canvasStore = useCanvasStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const experimentalNdvStore = useExperimentalNdvStore();
|
||||
|
||||
function syncSelectionToCanvasIfEnabled(value: LogEntry) {
|
||||
if (!logsStore.isLogSelectionSyncedWithCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvasEventBus.emit('nodes:select', { ids: [value.node.id], panIntoView: true });
|
||||
if (experimentalNdvStore.isEnabled) {
|
||||
canvasEventBus.emit('nodes:select', { ids: [value.node.id], panIntoView: false });
|
||||
experimentalNdvStore.focusNode(value.node.id);
|
||||
} else {
|
||||
canvasEventBus.emit('nodes:select', { ids: [value.node.id], panIntoView: true });
|
||||
}
|
||||
}
|
||||
|
||||
function select(value: LogEntry | undefined) {
|
||||
|
||||
Reference in New Issue
Block a user