mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(editor): Add experimental NDV pane in canvas (no-changelog) (#16419)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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" />
|
||||
|
||||
@@ -488,6 +488,8 @@ export const LOCAL_STORAGE_LOGS_PANEL_DETAILS_PANEL_SUB_NODE = 'N8N_LOGS_DETAILS
|
||||
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 LOCAL_STORAGE_EXPERIMENTAL_DOCKED_NODE_SETTINGS =
|
||||
'N8N_EXPERIMENTAL_DOCKED_NODE_SETTINGS';
|
||||
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';
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as promptsApi from '@n8n/rest-api-client/api/prompts';
|
||||
import { testHealthEndpoint } from '@/api/templates';
|
||||
import {
|
||||
INSECURE_CONNECTION_WARNING,
|
||||
LOCAL_STORAGE_EXPERIMENTAL_DOCKED_NODE_SETTINGS,
|
||||
LOCAL_STORAGE_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS,
|
||||
} from '@/constants';
|
||||
import { STORES } from '@n8n/stores';
|
||||
@@ -347,6 +348,15 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
||||
{ writeDefaults: false },
|
||||
);
|
||||
|
||||
/**
|
||||
* (Experimental) If set to true, show node settings for a selected node in docked pane
|
||||
*/
|
||||
const experimental__dockedNodeSettingsEnabled = useLocalStorage(
|
||||
LOCAL_STORAGE_EXPERIMENTAL_DOCKED_NODE_SETTINGS,
|
||||
false,
|
||||
{ writeDefaults: false },
|
||||
);
|
||||
|
||||
return {
|
||||
settings,
|
||||
userManagement,
|
||||
@@ -405,6 +415,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
||||
isAiCreditsEnabled,
|
||||
aiCreditsQuota,
|
||||
experimental__minZoomNodeSettingsInCanvas,
|
||||
experimental__dockedNodeSettingsEnabled,
|
||||
partialExecutionVersion,
|
||||
reset,
|
||||
getTimezones,
|
||||
|
||||
Reference in New Issue
Block a user