feat(editor): Add experimental NDV pane in canvas (no-changelog) (#16419)

This commit is contained in:
Suguru Inoue
2025-06-19 09:18:05 +02:00
committed by GitHub
parent c0d1ff6e4c
commit d0eb7a45ad
8 changed files with 141 additions and 10 deletions

View File

@@ -54,6 +54,7 @@ import { ProjectTypes } from '@/types/projects.types';
import { updateDynamicConnections } from '@/utils/nodeSettingsUtils';
import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue';
import { useCanvasOperations } from '@/composables/useCanvasOperations';
import { N8nIconButton } from '@n8n/design-system';
const props = withDefaults(
defineProps<{
@@ -67,6 +68,8 @@ const props = withDefaults(
executable: boolean;
inputSize: number;
activeNode?: INodeUi;
canExpand?: boolean;
hideConnections?: boolean;
}>(),
{
foreignCredentials: () => [],
@@ -74,6 +77,9 @@ const props = withDefaults(
executable: true,
inputSize: 0,
blockUI: false,
activeNode: undefined,
canExpand: false,
hideConnections: false,
},
);
@@ -85,6 +91,7 @@ const emit = defineEmits<{
openConnectionNodeCreator: [nodeName: string, connectionType: NodeConnectionType];
activate: [];
execute: [];
expand: [];
}>();
const nodeTypesStore = useNodeTypesStore();
@@ -977,7 +984,9 @@ onMounted(() => {
populateSettings();
setNodeValues();
props.eventBus?.on('openSettings', openSettings);
nodeHelpers.updateNodeParameterIssues(node.value as INodeUi, props.nodeType);
if (node.value !== null) {
nodeHelpers.updateNodeParameterIssues(node.value, props.nodeType);
}
importCurlEventBus.on('setHttpNodeParameters', setHttpNodeParameters);
ndvEventBus.on('updateParameterValue', valueChanged);
});
@@ -1019,9 +1028,9 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
:read-only="isReadOnly"
@update:model-value="nameChanged"
></NodeTitle>
<div v-if="isExecutable">
<div v-if="isExecutable || props.canExpand" :class="$style.headerActions">
<NodeExecuteButton
v-if="!blockUI && node && nodeValid"
v-if="isExecutable && !blockUI && node && nodeValid"
data-test-id="node-execute-button"
:node-name="node.name"
:disabled="outputPanelEditMode.enabled && !isTriggerNode"
@@ -1032,6 +1041,16 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
@stop-execution="onStopExecution"
@value-changed="valueChanged"
/>
<N8nIconButton
v-if="props.canExpand"
icon="expand"
type="secondary"
text
size="mini"
icon-size="large"
aria-label="Expand"
@click="emit('expand')"
/>
</div>
</div>
<NodeSettingsTabs
@@ -1174,7 +1193,7 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
</div>
</div>
<NDVSubConnections
v-if="node"
v-if="node && !props.hideConnections"
ref="subConnections"
:root-node="node"
@switch-selected-node="onSwitchSelectedNode"
@@ -1189,6 +1208,12 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
background-color: var(--color-background-base);
}
.headerActions {
display: flex;
gap: var(--spacing-4xs);
align-items: center;
}
.warningIcon {
color: var(--color-text-lighter);
font-size: var(--font-size-2xl);

View File

@@ -53,7 +53,7 @@ import CanvasBackground from './elements/background/CanvasBackground.vue';
import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
import Edge from './elements/edges/CanvasEdge.vue';
import Node from './elements/nodes/CanvasNode.vue';
import { useViewportAutoAdjust } from '@/components/canvas/composables/useViewportAutoAdjust';
import { useViewportAutoAdjust } from './composables/useViewportAutoAdjust';
import { isOutsideSelected } from '@/utils/htmlUtils';
const $style = useCssModule();

View File

@@ -9,6 +9,8 @@ import { createEventBus } from '@n8n/utils/event-bus';
import type { CanvasEventBusEvents } from '@/types';
import { useVueFlow } from '@vue-flow/core';
import { throttledRef } from '@vueuse/core';
import { useSettingsStore } from '@/stores/settings.store';
import ExperimentalNodeDetailsDrawer from './components/ExperimentalNodeDetailsDrawer.vue';
defineOptions({
inheritAttrs: false,
@@ -34,8 +36,9 @@ const props = withDefaults(
);
const $style = useCssModule();
const settingsStore = useSettingsStore();
const { onNodesInitialized } = useVueFlow({ id: props.id });
const { onNodesInitialized, getSelectedNodes } = useVueFlow({ id: props.id });
const workflow = toRef(props, 'workflow');
const workflowObject = toRef(props, 'workflowObject');
@@ -79,12 +82,16 @@ const mappedConnectionsThrottled = throttledRef(mappedConnections, 200);
/>
</div>
<slot />
<ExperimentalNodeDetailsDrawer
v-if="settingsStore.experimental__dockedNodeSettingsEnabled && !props.readOnly"
:selected-nodes="getSelectedNodes"
/>
</div>
</template>
<style lang="scss" module>
.wrapper {
display: block;
display: flex;
position: relative;
width: 100%;
height: 100%;
@@ -96,5 +103,7 @@ const mappedConnectionsThrottled = throttledRef(mappedConnections, 200);
height: 100%;
position: relative;
display: block;
align-items: stretch;
justify-content: stretch;
}
</style>

View File

@@ -1,15 +1,20 @@
<script setup lang="ts">
import NodeSettings from '@/components/NodeSettings.vue';
import { useCanvasOperations } from '@/composables/useCanvasOperations';
import { type IUpdateInformation } from '@/Interface';
import { useNDVStore } from '@/stores/ndv.store';
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 { nodeId, canOpenNdv } = defineProps<{ nodeId: string; canOpenNdv?: boolean }>();
const settingsEventBus = createEventBus();
const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const { setActiveNodeName } = useNDVStore();
const { renameNode } = useCanvasOperations();
const activeNode = computed(() => workflowsStore.getNodeById(nodeId));
const activeNodeType = computed(() => {
@@ -18,10 +23,23 @@ const activeNodeType = computed(() => {
}
return null;
});
function handleOpenNdv() {
if (activeNode.value) {
setActiveNodeName(activeNode.value.name);
}
}
function handleValueChanged(parameterData: IUpdateInformation) {
if (parameterData.name === 'name' && parameterData.oldValue) {
void renameNode(parameterData.oldValue as string, parameterData.value as string);
}
}
</script>
<template>
<NodeSettings
:can-expand="canOpenNdv"
:event-bus="settingsEventBus"
:dragging="false"
:active-node="activeNode"
@@ -32,5 +50,8 @@ const activeNodeType = computed(() => {
:block-u-i="false"
:executable="false"
:input-size="0"
hide-connections
@expand="handleOpenNdv"
@value-changed="handleValueChanged"
/>
</template>

View File

@@ -0,0 +1,63 @@
<script setup lang="ts">
import { type CanvasNode } from '@/types';
import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue';
import { N8nText } from '@n8n/design-system';
import { computed, watch, ref } from 'vue';
const { selectedNodes } = defineProps<{ selectedNodes: CanvasNode[] }>();
const content = computed(() =>
selectedNodes.length > 1
? `${selectedNodes.length} nodes selected`
: selectedNodes.length > 0
? selectedNodes[0]
: undefined,
);
const lastContent = ref<string | CanvasNode | undefined>();
// Sync lastContent to be "last defined content" (for drawer animation)
watch(
content,
(newContent) => {
if (newContent !== undefined) {
lastContent.value = newContent;
}
},
{ immediate: true },
);
</script>
<template>
<div :class="[$style.component, content === undefined ? $style.closed : '']">
<N8nText v-if="typeof lastContent === 'string'" color="text-base">
{{ lastContent }}
</N8nText>
<ExperimentalCanvasNodeSettings
v-else-if="lastContent !== undefined"
:node-id="lastContent.id"
can-open-ndv
/>
</div>
</template>
<style lang="scss" module>
.component {
position: absolute;
right: 0;
z-index: 10;
flex-grow: 0;
flex-shrink: 0;
border-left: var(--border-base);
background-color: var(--color-background-xlight);
width: #{$node-creator-width};
height: 100%;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease;
&.closed {
transform: translateX(100%);
}
}
</style>

View File

@@ -7,7 +7,7 @@ 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';
import ExperimentalCanvasNodeSettings from '../../../components/ExperimentalCanvasNodeSettings.vue';
const $style = useCssModule();
const i18n = useI18n();
@@ -153,7 +153,7 @@ function onActivate(event: MouseEvent) {
@contextmenu="openContextMenu"
@dblclick.stop="onActivate"
>
<CanvasNodeNodeSettings v-if="nodeSettingsZoom !== undefined" :node-id="id" />
<ExperimentalCanvasNodeSettings 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" />