feat(editor): Experimental feature flag to show node settings in the canvas (no-changelog) (#15925)

This commit is contained in:
Suguru Inoue
2025-06-05 09:39:01 +02:00
committed by GitHub
parent d59b9b528e
commit c57e697249
7 changed files with 132 additions and 14 deletions

View File

@@ -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));

View File

@@ -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<number | undefined> {
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,
);
}

View File

@@ -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"
>
<CanvasNodeTooltip v-if="renderOptions.tooltip" :visible="showTooltip" />
<NodeIcon :icon-source="iconSource" :size="iconSize" :shrink="false" :disabled="isDisabled" />
<CanvasNodeStatusIcons v-if="!isDisabled" :class="$style.statusIcons" />
<CanvasNodeDisabledStrikeThrough v-if="isStrikethroughVisible" />
<div :class="$style.description">
<div v-if="label" :class="$style.label">
{{ label }}
<CanvasNodeNodeSettings v-if="nodeSettingsZoom !== undefined" :node-id="id" />
<template v-else>
<CanvasNodeTooltip v-if="renderOptions.tooltip" :visible="showTooltip" />
<NodeIcon :icon-source="iconSource" :size="iconSize" :shrink="false" :disabled="isDisabled" />
<CanvasNodeStatusIcons v-if="!isDisabled" :class="$style.statusIcons" />
<CanvasNodeDisabledStrikeThrough v-if="isStrikethroughVisible" />
<div :class="$style.description">
<div v-if="label" :class="$style.label">
{{ label }}
</div>
<div v-if="isDisabled" :class="$style.disabledLabel">
({{ i18n.baseText('node.disabled') }})
</div>
<div v-if="subtitle" :class="$style.subtitle">{{ subtitle }}</div>
</div>
<div v-if="isDisabled" :class="$style.disabledLabel">
({{ i18n.baseText('node.disabled') }})
</div>
<div v-if="subtitle" :class="$style.subtitle">{{ subtitle }}</div>
</div>
</template>
</div>
</template>
@@ -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
*/

View File

@@ -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;"
>
<!--v-if-->
<div
class="n8n-node-icon"
@@ -42,6 +43,7 @@ exports[`CanvasNodeDefault > configurable > should render configurable node corr
Test Node Subtitle
</div>
</div>
</div>
`;
@@ -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;"
>
<!--v-if-->
<div
class="n8n-node-icon"
@@ -87,6 +90,7 @@ exports[`CanvasNodeDefault > configuration > should render configurable configur
Test Node Subtitle
</div>
</div>
</div>
`;
@@ -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;"
>
<!--v-if-->
<div
class="n8n-node-icon"
@@ -132,6 +137,7 @@ exports[`CanvasNodeDefault > configuration > should render configuration node co
Test Node Subtitle
</div>
</div>
</div>
`;
@@ -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;"
>
<!--v-if-->
<div
class="n8n-node-icon"
@@ -177,6 +184,7 @@ exports[`CanvasNodeDefault > should render node correctly 1`] = `
Test Node Subtitle
</div>
</div>
</div>
`;
@@ -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;"
>
<!--v-if-->
<div
class="n8n-node-icon"
@@ -222,5 +231,6 @@ exports[`CanvasNodeDefault > trigger > should render trigger node correctly 1`]
Test Node Subtitle
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import NodeSettings from '@/components/NodeSettings.vue';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { createEventBus } from '@n8n/utils/event-bus';
import { computed } from 'vue';
const { nodeId } = defineProps<{ nodeId: string }>();
const settingsEventBus = createEventBus();
const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const activeNode = computed(() => workflowsStore.getNodeById(nodeId));
const activeNodeType = computed(() => {
if (activeNode.value) {
return nodeTypesStore.getNodeType(activeNode.value.type, activeNode.value.typeVersion);
}
return null;
});
</script>
<template>
<NodeSettings
:event-bus="settingsEventBus"
:dragging="false"
:active-node="activeNode"
:node-type="activeNodeType"
push-ref=""
:foreign-credentials="[]"
:read-only="false"
:block-u-i="false"
:executable="false"
:input-size="0"
/>
</template>

View File

@@ -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';

View File

@@ -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,