From c57e6972497821b9b2d008d177b9c0ee8c1b9c0b Mon Sep 17 00:00:00 2001 From: Suguru Inoue Date: Thu, 5 Jun 2025 09:39:01 +0200 Subject: [PATCH] feat(editor): Experimental feature flag to show node settings in the canvas (no-changelog) (#15925) --- .../editor-ui/src/components/NodeSettings.vue | 3 +- .../composables/useNodeSettingsInCanvas.ts | 29 +++++++++++ .../nodes/render-types/CanvasNodeDefault.vue | 51 ++++++++++++++----- .../CanvasNodeDefault.test.ts.snap | 10 ++++ .../parts/CanvasNodeNodeSettings.vue | 36 +++++++++++++ packages/frontend/editor-ui/src/constants.ts | 2 + .../editor-ui/src/stores/settings.store.ts | 15 +++++- 7 files changed, 132 insertions(+), 14 deletions(-) create mode 100644 packages/frontend/editor-ui/src/components/canvas/composables/useNodeSettingsInCanvas.ts create mode 100644 packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeNodeSettings.vue diff --git a/packages/frontend/editor-ui/src/components/NodeSettings.vue b/packages/frontend/editor-ui/src/components/NodeSettings.vue index 94d851724c..8e1767529b 100644 --- a/packages/frontend/editor-ui/src/components/NodeSettings.vue +++ b/packages/frontend/editor-ui/src/components/NodeSettings.vue @@ -65,6 +65,7 @@ const props = withDefaults( blockUI: boolean; executable: boolean; inputSize: number; + activeNode?: INodeUi; }>(), { foreignCredentials: () => [], @@ -129,7 +130,7 @@ const isHomeProjectTeam = computed( const isReadOnly = computed( () => props.readOnly || (hasForeignCredential.value && !isHomeProjectTeam.value), ); -const node = computed(() => ndvStore.activeNode); +const node = computed(() => props.activeNode ?? ndvStore.activeNode); const isTriggerNode = computed(() => !!node.value && nodeTypesStore.isTriggerNode(node.value.type)); diff --git a/packages/frontend/editor-ui/src/components/canvas/composables/useNodeSettingsInCanvas.ts b/packages/frontend/editor-ui/src/components/canvas/composables/useNodeSettingsInCanvas.ts new file mode 100644 index 0000000000..37d8918c92 --- /dev/null +++ b/packages/frontend/editor-ui/src/components/canvas/composables/useNodeSettingsInCanvas.ts @@ -0,0 +1,29 @@ +import { useCanvasOperations } from '@/composables/useCanvasOperations'; +import { useSettingsStore } from '@/stores/settings.store'; +import { useVueFlow } from '@vue-flow/core'; +import { useDebounce } from '@vueuse/core'; +import { computed, type ComputedRef } from 'vue'; +import { useRouter } from 'vue-router'; + +export function useNodeSettingsInCanvas(): ComputedRef { + const settingsStore = useSettingsStore(); + + if ( + Number.isNaN(settingsStore.experimental__minZoomNodeSettingsInCanvas) || + settingsStore.experimental__minZoomNodeSettingsInCanvas <= 0 + ) { + return computed(() => undefined); + } + + const router = useRouter(); + const { editableWorkflow } = useCanvasOperations({ router }); + const viewFlow = useVueFlow({ id: editableWorkflow.value.id }); + const zoom = computed(() => viewFlow.viewport.value.zoom); + const debouncedZoom = useDebounce(zoom, 100); + + return computed(() => + debouncedZoom.value > settingsStore.experimental__minZoomNodeSettingsInCanvas + ? debouncedZoom.value + : undefined, + ); +} diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue index 708029c5a9..1011c87da9 100644 --- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue +++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue @@ -6,6 +6,8 @@ import { useCanvasNode } from '@/composables/useCanvasNode'; import { NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants'; import type { CanvasNodeDefaultRender } from '@/types'; import { useCanvas } from '@/composables/useCanvas'; +import { useNodeSettingsInCanvas } from '@/components/canvas/composables/useNodeSettingsInCanvas'; +import CanvasNodeNodeSettings from './parts/CanvasNodeNodeSettings.vue'; const $style = useCssModule(); const i18n = useI18n(); @@ -48,6 +50,8 @@ const { const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']); +const nodeSettingsZoom = useNodeSettingsInCanvas(); + const classes = computed(() => { return { [$style.node]: true, @@ -62,6 +66,7 @@ const classes = computed(() => { [$style.configuration]: renderOptions.value.configuration, [$style.trigger]: renderOptions.value.trigger, [$style.warning]: renderOptions.value.dirtiness !== undefined, + [$style.settingsView]: nodeSettingsZoom.value !== undefined, }; }); @@ -79,6 +84,10 @@ const styles = computed(() => { stylesObject['--configurable-node--input-count'] = nonMainInputs.value.length + spacerCount; } + if (nodeSettingsZoom.value !== undefined) { + stylesObject['--zoom'] = nodeSettingsZoom.value; + } + stylesObject['--canvas-node--main-input-count'] = mainInputs.value.length; stylesObject['--canvas-node--main-output-count'] = mainOutputs.value.length; @@ -143,19 +152,22 @@ function onActivate(event: MouseEvent) { @contextmenu="openContextMenu" @dblclick.stop="onActivate" > - - - - -
-
- {{ label }} + +
@@ -193,6 +205,21 @@ function onActivate(event: MouseEvent) { var(--border-radius-large) var(--trigger-node--border-radius); } + &.settingsView { + /*margin-top: calc(var(--canvas-node--width) * 0.8);*/ + height: calc(var(--canvas-node--height) * 2.4) !important; + width: calc(var(--canvas-node--width) * 1.6) !important; + align-items: flex-start; + justify-content: stretch; + overflow: auto; + border-radius: var(--border-radius-large) !important; + + & > * { + zoom: calc(1 / var(--zoom, 1)); + width: 100% !important; + } + } + /** * Node types */ diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/__snapshots__/CanvasNodeDefault.test.ts.snap b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/__snapshots__/CanvasNodeDefault.test.ts.snap index cd5075b450..7607434296 100644 --- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/__snapshots__/CanvasNodeDefault.test.ts.snap +++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/__snapshots__/CanvasNodeDefault.test.ts.snap @@ -6,6 +6,7 @@ exports[`CanvasNodeDefault > configurable > should render configurable node corr data-test-id="canvas-configurable-node" style="--configurable-node--input-count: 0; --canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;" > +
configurable > should render configurable node corr Test Node Subtitle
+ `; @@ -51,6 +53,7 @@ exports[`CanvasNodeDefault > configuration > should render configurable configur data-test-id="canvas-configurable-node" style="--configurable-node--input-count: 0; --canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;" > +
configuration > should render configurable configur Test Node Subtitle
+ `; @@ -96,6 +100,7 @@ exports[`CanvasNodeDefault > configuration > should render configuration node co data-test-id="canvas-configuration-node" style="--canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;" > +
configuration > should render configuration node co Test Node Subtitle
+ `; @@ -141,6 +147,7 @@ exports[`CanvasNodeDefault > should render node correctly 1`] = ` data-test-id="canvas-default-node" style="--canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;" > +
should render node correctly 1`] = ` Test Node Subtitle
+ `; @@ -186,6 +194,7 @@ exports[`CanvasNodeDefault > trigger > should render trigger node correctly 1`] data-test-id="canvas-trigger-node" style="--canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;" > +
trigger > should render trigger node correctly 1`] Test Node Subtitle
+ `; diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeNodeSettings.vue b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeNodeSettings.vue new file mode 100644 index 0000000000..c7ea10273e --- /dev/null +++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeNodeSettings.vue @@ -0,0 +1,36 @@ + + + diff --git a/packages/frontend/editor-ui/src/constants.ts b/packages/frontend/editor-ui/src/constants.ts index 52f5023a5d..cc6756fc3b 100644 --- a/packages/frontend/editor-ui/src/constants.ts +++ b/packages/frontend/editor-ui/src/constants.ts @@ -490,6 +490,8 @@ export const LOCAL_STORAGE_LOGS_PANEL_OPEN = 'N8N_LOGS_PANEL_OPEN'; export const LOCAL_STORAGE_LOGS_SYNC_SELECTION = 'N8N_LOGS_SYNC_SELECTION'; export const LOCAL_STORAGE_LOGS_PANEL_DETAILS_PANEL = 'N8N_LOGS_DETAILS_PANEL'; export const LOCAL_STORAGE_WORKFLOW_LIST_PREFERENCES_KEY = 'N8N_WORKFLOWS_LIST_PREFERENCES'; +export const LOCAL_STORAGE_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS = + 'N8N_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS'; export const BASE_NODE_SURVEY_URL = 'https://n8n-community.typeform.com/to/BvmzxqYv#nodename='; export const COMMUNITY_PLUS_DOCS_URL = 'https://docs.n8n.io/hosting/community-edition-features/#registered-community-edition'; diff --git a/packages/frontend/editor-ui/src/stores/settings.store.ts b/packages/frontend/editor-ui/src/stores/settings.store.ts index 54521bcc61..9619f5baa0 100644 --- a/packages/frontend/editor-ui/src/stores/settings.store.ts +++ b/packages/frontend/editor-ui/src/stores/settings.store.ts @@ -7,7 +7,10 @@ import * as ldapApi from '@/api/ldap'; import * as settingsApi from '@/api/settings'; import { testHealthEndpoint } from '@/api/templates'; import type { ILdapConfig } from '@/Interface'; -import { INSECURE_CONNECTION_WARNING } from '@/constants'; +import { + INSECURE_CONNECTION_WARNING, + LOCAL_STORAGE_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS, +} from '@/constants'; import { STORES } from '@n8n/stores'; import { UserManagementAuthenticationMethod } from '@/Interface'; import type { IDataObject, WorkflowSettings } from 'n8n-workflow'; @@ -381,6 +384,15 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => { settings.value = {} as FrontendSettings; }; + /** + * (Experimental) Minimum zoom level of the canvas to render node settings in place of nodes, without opening NDV + */ + const experimental__minZoomNodeSettingsInCanvas = useLocalStorage( + LOCAL_STORAGE_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS, + 0, + { writeDefaults: false }, + ); + return { settings, userManagement, @@ -445,6 +457,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => { isAiCreditsEnabled, aiCreditsQuota, isNewLogsEnabled, + experimental__minZoomNodeSettingsInCanvas, reset, testLdapConnection, getLdapConfig,