From eb138ecf8ddd3a4c692cbddf3079e0bcb3a3d3a8 Mon Sep 17 00:00:00 2001 From: Suguru Inoue Date: Mon, 30 Jun 2025 11:04:11 +0200 Subject: [PATCH] feat(editor): Implement some quick improvements on NDV in canvas experiment (no-changelog) (#16717) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Milorad FIlipović --- .../src/components/Node/NodeCreation.vue | 16 ++ .../editor-ui/src/components/NodeSettings.vue | 109 +++++++---- .../src/components/ParameterInputList.vue | 25 ++- .../src/components/canvas/Canvas.vue | 5 +- .../src/components/canvas/WorkflowCanvas.vue | 2 +- .../composables/useNodeSettingsInCanvas.ts | 27 --- .../nodes/render-types/CanvasNodeDefault.vue | 97 +++++----- .../CanvasNodeDefault.test.ts.snap | 10 - .../ExperimentalCanvasNodeSettings.vue | 23 +-- .../ExperimentalEmbeddedNodeDetails.vue | 173 ++++++++++++++++++ .../ExperimentalNodeDetailsDrawer.vue | 23 ++- .../experimental/experimentalNdv.store.ts | 54 ++++++ .../experimental/experimentalNdv.utils.ts | 5 + .../editor-ui/src/plugins/icons/index.ts | 2 + 14 files changed, 424 insertions(+), 147 deletions(-) delete mode 100644 packages/frontend/editor-ui/src/components/canvas/composables/useNodeSettingsInCanvas.ts rename packages/frontend/editor-ui/src/components/canvas/{ => experimental}/components/ExperimentalCanvasNodeSettings.vue (79%) create mode 100644 packages/frontend/editor-ui/src/components/canvas/experimental/components/ExperimentalEmbeddedNodeDetails.vue rename packages/frontend/editor-ui/src/components/canvas/{ => experimental}/components/ExperimentalNodeDetailsDrawer.vue (75%) create mode 100644 packages/frontend/editor-ui/src/components/canvas/experimental/experimentalNdv.store.ts create mode 100644 packages/frontend/editor-ui/src/components/canvas/experimental/experimentalNdv.utils.ts diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreation.vue b/packages/frontend/editor-ui/src/components/Node/NodeCreation.vue index d161446b1b1..db7e7dc841b 100644 --- a/packages/frontend/editor-ui/src/components/Node/NodeCreation.vue +++ b/packages/frontend/editor-ui/src/components/Node/NodeCreation.vue @@ -20,6 +20,7 @@ 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; @@ -44,6 +45,7 @@ const uiStore = useUIStore(); const focusPanelStore = useFocusPanelStore(); const posthogStore = usePostHog(); const i18n = useI18n(); +const experimentalNdvStore = useExperimentalNdvStore(); const { getAddedNodesAndConnections } = useActions(); @@ -125,6 +127,20 @@ function nodeTypeSelected(value: NodeTypeSelectedPayload[]) { @click="focusPanelStore.toggleFocusPanel" /> + + -import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; +import { useTemplateRef, computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import type { INodeTypeDescription, INodeParameters, @@ -18,10 +18,8 @@ import type { import { COMMUNITY_NODES_INSTALLATION_DOCS_URL, CUSTOM_NODES_DOCS_URL } from '@/constants'; -import NodeTitle from '@/components/NodeTitle.vue'; import ParameterInputList from '@/components/ParameterInputList.vue'; import NodeCredentials from '@/components/NodeCredentials.vue'; -import NodeSettingsTabs from '@/components/NodeSettingsTabs.vue'; import NodeWebhooks from '@/components/NodeWebhooks.vue'; import NDVSubConnections from '@/components/NDVSubConnections.vue'; import get from 'lodash/get'; @@ -42,8 +40,9 @@ import { useTelemetry } from '@/composables/useTelemetry'; import { importCurlEventBus, ndvEventBus } from '@/event-bus'; import { ProjectTypes } from '@/types/projects.types'; import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue'; +import { shouldShowParameter } from './canvas/experimental/experimentalNdv.utils'; +import { useResizeObserver } from '@vueuse/core'; import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters'; -import { N8nIconButton } from '@n8n/design-system'; const props = withDefaults( defineProps<{ @@ -57,8 +56,8 @@ const props = withDefaults( executable: boolean; inputSize: number; activeNode?: INodeUi; - canExpand?: boolean; - hideConnections?: boolean; + isEmbeddedInCanvas?: boolean; + noWheel?: boolean; }>(), { foreignCredentials: () => [], @@ -67,8 +66,8 @@ const props = withDefaults( inputSize: 0, blockUI: false, activeNode: undefined, - canExpand: false, - hideConnections: false, + isEmbeddedInCanvas: false, + noWheel: false, }, ); @@ -84,9 +83,10 @@ const emit = defineEmits<{ ]; activate: []; execute: []; - expand: []; }>(); +const slots = defineSlots<{ actions?: {} }>(); + const nodeTypesStore = useNodeTypesStore(); const ndvStore = useNDVStore(); const workflowsStore = useWorkflowsStore(); @@ -100,6 +100,17 @@ const i18n = useI18n(); const nodeSettingsParameters = useNodeSettingsParameters(); const nodeValues = nodeSettingsParameters.nodeValues; +const nodeParameterWrapper = useTemplateRef('nodeParameterWrapper'); +const shouldShowStaticScrollbar = ref(false); + +if (props.isEmbeddedInCanvas) { + useResizeObserver(nodeParameterWrapper, () => { + shouldShowStaticScrollbar.value = + (nodeParameterWrapper.value?.scrollHeight ?? 0) > + (nodeParameterWrapper.value?.offsetHeight ?? 0); + }); +} + const nodeValid = ref(true); const openPanel = ref<'params' | 'settings'>('params'); @@ -198,10 +209,12 @@ const parameters = computed(() => { const parametersSetting = computed(() => parameters.value.filter((item) => item.isNodeSetting)); -const parametersNoneSetting = computed(() => +const parametersNoneSetting = computed(() => { // The connection hint notice is visually hidden via CSS in NodeDetails.vue when the node has output connections - parameters.value.filter((item) => !item.isNodeSetting), -); + const paramsToShow = parameters.value.filter((item) => !item.isNodeSetting); + + return props.isEmbeddedInCanvas ? parameters.value.filter(shouldShowParameter) : paramsToShow; +}); const isDisplayingCredentials = computed( () => @@ -735,6 +748,12 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio nodeHelpers.displayParameter(node.value.parameters, credentialTypeDescription, '', node.value) ); } + +function handleWheelEvent(event: WheelEvent) { + if (event.ctrlKey) { + event.preventDefault(); + } +} diff --git a/packages/frontend/editor-ui/src/components/canvas/experimental/experimentalNdv.store.ts b/packages/frontend/editor-ui/src/components/canvas/experimental/experimentalNdv.store.ts new file mode 100644 index 00000000000..3618f2ee841 --- /dev/null +++ b/packages/frontend/editor-ui/src/components/canvas/experimental/experimentalNdv.store.ts @@ -0,0 +1,54 @@ +import { computed, shallowRef } from 'vue'; +import { defineStore } from 'pinia'; +import { useWorkflowsStore } from '@/stores/workflows.store'; +import { useSettingsStore } from '@/stores/settings.store'; + +export const useExperimentalNdvStore = defineStore('experimentalNdv', () => { + const workflowStore = useWorkflowsStore(); + const settingsStore = useSettingsStore(); + const isEnabled = computed( + () => + !Number.isNaN(settingsStore.experimental__minZoomNodeSettingsInCanvas) && + settingsStore.experimental__minZoomNodeSettingsInCanvas > 0, + ); + const maxCanvasZoom = computed(() => + isEnabled.value ? settingsStore.experimental__minZoomNodeSettingsInCanvas : 4, + ); + + const collapsedNodes = shallowRef>>({}); + + function setNodeExpanded(nodeId: string, isExpanded?: boolean) { + collapsedNodes.value = { + ...collapsedNodes.value, + [nodeId]: isExpanded ?? !collapsedNodes.value[nodeId], + }; + } + + function collapseAllNodes() { + collapsedNodes.value = workflowStore.allNodes.reduce>>( + (acc, node) => { + acc[node.id] = true; + return acc; + }, + {}, + ); + } + + function expandAllNodes() { + collapsedNodes.value = {}; + } + + function isActive(canvasZoom: number) { + return isEnabled.value && canvasZoom === maxCanvasZoom.value; + } + + return { + isEnabled, + maxCanvasZoom, + collapsedNodes: computed(() => collapsedNodes.value), + isActive, + setNodeExpanded, + expandAllNodes, + collapseAllNodes, + }; +}); diff --git a/packages/frontend/editor-ui/src/components/canvas/experimental/experimentalNdv.utils.ts b/packages/frontend/editor-ui/src/components/canvas/experimental/experimentalNdv.utils.ts new file mode 100644 index 00000000000..a5e2f02d093 --- /dev/null +++ b/packages/frontend/editor-ui/src/components/canvas/experimental/experimentalNdv.utils.ts @@ -0,0 +1,5 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export function shouldShowParameter(item: INodeProperties): boolean { + return item.name.match(/resource|authentication|operation/i) === null; +} diff --git a/packages/frontend/editor-ui/src/plugins/icons/index.ts b/packages/frontend/editor-ui/src/plugins/icons/index.ts index c1925f1edb2..15190e52519 100644 --- a/packages/frontend/editor-ui/src/plugins/icons/index.ts +++ b/packages/frontend/editor-ui/src/plugins/icons/index.ts @@ -43,6 +43,7 @@ import { faCogs, faComment, faComments, + faCompress, faClipboardList, faClock, faClone, @@ -247,6 +248,7 @@ export const FontAwesomePlugin: Plugin = { addIcon(faCogs); addIcon(faComment); addIcon(faComments); + addIcon(faCompress); addIcon(faClipboardList); addIcon(faClock); addIcon(faClone);