mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +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 { updateDynamicConnections } from '@/utils/nodeSettingsUtils';
|
||||||
import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue';
|
import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue';
|
||||||
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
||||||
|
import { N8nIconButton } from '@n8n/design-system';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -67,6 +68,8 @@ const props = withDefaults(
|
|||||||
executable: boolean;
|
executable: boolean;
|
||||||
inputSize: number;
|
inputSize: number;
|
||||||
activeNode?: INodeUi;
|
activeNode?: INodeUi;
|
||||||
|
canExpand?: boolean;
|
||||||
|
hideConnections?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
foreignCredentials: () => [],
|
foreignCredentials: () => [],
|
||||||
@@ -74,6 +77,9 @@ const props = withDefaults(
|
|||||||
executable: true,
|
executable: true,
|
||||||
inputSize: 0,
|
inputSize: 0,
|
||||||
blockUI: false,
|
blockUI: false,
|
||||||
|
activeNode: undefined,
|
||||||
|
canExpand: false,
|
||||||
|
hideConnections: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -85,6 +91,7 @@ const emit = defineEmits<{
|
|||||||
openConnectionNodeCreator: [nodeName: string, connectionType: NodeConnectionType];
|
openConnectionNodeCreator: [nodeName: string, connectionType: NodeConnectionType];
|
||||||
activate: [];
|
activate: [];
|
||||||
execute: [];
|
execute: [];
|
||||||
|
expand: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
@@ -977,7 +984,9 @@ onMounted(() => {
|
|||||||
populateSettings();
|
populateSettings();
|
||||||
setNodeValues();
|
setNodeValues();
|
||||||
props.eventBus?.on('openSettings', openSettings);
|
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);
|
importCurlEventBus.on('setHttpNodeParameters', setHttpNodeParameters);
|
||||||
ndvEventBus.on('updateParameterValue', valueChanged);
|
ndvEventBus.on('updateParameterValue', valueChanged);
|
||||||
});
|
});
|
||||||
@@ -1019,9 +1028,9 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
|||||||
:read-only="isReadOnly"
|
:read-only="isReadOnly"
|
||||||
@update:model-value="nameChanged"
|
@update:model-value="nameChanged"
|
||||||
></NodeTitle>
|
></NodeTitle>
|
||||||
<div v-if="isExecutable">
|
<div v-if="isExecutable || props.canExpand" :class="$style.headerActions">
|
||||||
<NodeExecuteButton
|
<NodeExecuteButton
|
||||||
v-if="!blockUI && node && nodeValid"
|
v-if="isExecutable && !blockUI && node && nodeValid"
|
||||||
data-test-id="node-execute-button"
|
data-test-id="node-execute-button"
|
||||||
:node-name="node.name"
|
:node-name="node.name"
|
||||||
:disabled="outputPanelEditMode.enabled && !isTriggerNode"
|
:disabled="outputPanelEditMode.enabled && !isTriggerNode"
|
||||||
@@ -1032,6 +1041,16 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
|||||||
@stop-execution="onStopExecution"
|
@stop-execution="onStopExecution"
|
||||||
@value-changed="valueChanged"
|
@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>
|
||||||
</div>
|
</div>
|
||||||
<NodeSettingsTabs
|
<NodeSettingsTabs
|
||||||
@@ -1174,7 +1193,7 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NDVSubConnections
|
<NDVSubConnections
|
||||||
v-if="node"
|
v-if="node && !props.hideConnections"
|
||||||
ref="subConnections"
|
ref="subConnections"
|
||||||
:root-node="node"
|
:root-node="node"
|
||||||
@switch-selected-node="onSwitchSelectedNode"
|
@switch-selected-node="onSwitchSelectedNode"
|
||||||
@@ -1189,6 +1208,12 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
|||||||
background-color: var(--color-background-base);
|
background-color: var(--color-background-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.headerActions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-4xs);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.warningIcon {
|
.warningIcon {
|
||||||
color: var(--color-text-lighter);
|
color: var(--color-text-lighter);
|
||||||
font-size: var(--font-size-2xl);
|
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 CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
|
||||||
import Edge from './elements/edges/CanvasEdge.vue';
|
import Edge from './elements/edges/CanvasEdge.vue';
|
||||||
import Node from './elements/nodes/CanvasNode.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';
|
import { isOutsideSelected } from '@/utils/htmlUtils';
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { createEventBus } from '@n8n/utils/event-bus';
|
|||||||
import type { CanvasEventBusEvents } from '@/types';
|
import type { CanvasEventBusEvents } from '@/types';
|
||||||
import { useVueFlow } from '@vue-flow/core';
|
import { useVueFlow } from '@vue-flow/core';
|
||||||
import { throttledRef } from '@vueuse/core';
|
import { throttledRef } from '@vueuse/core';
|
||||||
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
import ExperimentalNodeDetailsDrawer from './components/ExperimentalNodeDetailsDrawer.vue';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
@@ -34,8 +36,9 @@ const props = withDefaults(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const $style = useCssModule();
|
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 workflow = toRef(props, 'workflow');
|
||||||
const workflowObject = toRef(props, 'workflowObject');
|
const workflowObject = toRef(props, 'workflowObject');
|
||||||
@@ -79,12 +82,16 @@ const mappedConnectionsThrottled = throttledRef(mappedConnections, 200);
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
|
<ExperimentalNodeDetailsDrawer
|
||||||
|
v-if="settingsStore.experimental__dockedNodeSettingsEnabled && !props.readOnly"
|
||||||
|
:selected-nodes="getSelectedNodes"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.wrapper {
|
.wrapper {
|
||||||
display: block;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -96,5 +103,7 @@ const mappedConnectionsThrottled = throttledRef(mappedConnections, 200);
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: stretch;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import NodeSettings from '@/components/NodeSettings.vue';
|
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 { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
import { createEventBus } from '@n8n/utils/event-bus';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const { nodeId } = defineProps<{ nodeId: string }>();
|
const { nodeId, canOpenNdv } = defineProps<{ nodeId: string; canOpenNdv?: boolean }>();
|
||||||
|
|
||||||
const settingsEventBus = createEventBus();
|
const settingsEventBus = createEventBus();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
const { setActiveNodeName } = useNDVStore();
|
||||||
|
const { renameNode } = useCanvasOperations();
|
||||||
|
|
||||||
const activeNode = computed(() => workflowsStore.getNodeById(nodeId));
|
const activeNode = computed(() => workflowsStore.getNodeById(nodeId));
|
||||||
const activeNodeType = computed(() => {
|
const activeNodeType = computed(() => {
|
||||||
@@ -18,10 +23,23 @@ const activeNodeType = computed(() => {
|
|||||||
}
|
}
|
||||||
return null;
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NodeSettings
|
<NodeSettings
|
||||||
|
:can-expand="canOpenNdv"
|
||||||
:event-bus="settingsEventBus"
|
:event-bus="settingsEventBus"
|
||||||
:dragging="false"
|
:dragging="false"
|
||||||
:active-node="activeNode"
|
:active-node="activeNode"
|
||||||
@@ -32,5 +50,8 @@ const activeNodeType = computed(() => {
|
|||||||
:block-u-i="false"
|
:block-u-i="false"
|
||||||
:executable="false"
|
:executable="false"
|
||||||
:input-size="0"
|
:input-size="0"
|
||||||
|
hide-connections
|
||||||
|
@expand="handleOpenNdv"
|
||||||
|
@value-changed="handleValueChanged"
|
||||||
/>
|
/>
|
||||||
</template>
|
</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 type { CanvasNodeDefaultRender } from '@/types';
|
||||||
import { useCanvas } from '@/composables/useCanvas';
|
import { useCanvas } from '@/composables/useCanvas';
|
||||||
import { useNodeSettingsInCanvas } from '@/components/canvas/composables/useNodeSettingsInCanvas';
|
import { useNodeSettingsInCanvas } from '@/components/canvas/composables/useNodeSettingsInCanvas';
|
||||||
import CanvasNodeNodeSettings from './parts/CanvasNodeNodeSettings.vue';
|
import ExperimentalCanvasNodeSettings from '../../../components/ExperimentalCanvasNodeSettings.vue';
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -153,7 +153,7 @@ function onActivate(event: MouseEvent) {
|
|||||||
@contextmenu="openContextMenu"
|
@contextmenu="openContextMenu"
|
||||||
@dblclick.stop="onActivate"
|
@dblclick.stop="onActivate"
|
||||||
>
|
>
|
||||||
<CanvasNodeNodeSettings v-if="nodeSettingsZoom !== undefined" :node-id="id" />
|
<ExperimentalCanvasNodeSettings v-if="nodeSettingsZoom !== undefined" :node-id="id" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<CanvasNodeTooltip v-if="renderOptions.tooltip" :visible="showTooltip" />
|
<CanvasNodeTooltip v-if="renderOptions.tooltip" :visible="showTooltip" />
|
||||||
<NodeIcon :icon-source="iconSource" :size="iconSize" :shrink="false" :disabled="isDisabled" />
|
<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_WORKFLOW_LIST_PREFERENCES_KEY = 'N8N_WORKFLOWS_LIST_PREFERENCES';
|
||||||
export const LOCAL_STORAGE_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS =
|
export const LOCAL_STORAGE_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS =
|
||||||
'N8N_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 BASE_NODE_SURVEY_URL = 'https://n8n-community.typeform.com/to/BvmzxqYv#nodename=';
|
||||||
export const COMMUNITY_PLUS_DOCS_URL =
|
export const COMMUNITY_PLUS_DOCS_URL =
|
||||||
'https://docs.n8n.io/hosting/community-edition-features/#registered-community-edition';
|
'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 { testHealthEndpoint } from '@/api/templates';
|
||||||
import {
|
import {
|
||||||
INSECURE_CONNECTION_WARNING,
|
INSECURE_CONNECTION_WARNING,
|
||||||
|
LOCAL_STORAGE_EXPERIMENTAL_DOCKED_NODE_SETTINGS,
|
||||||
LOCAL_STORAGE_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS,
|
LOCAL_STORAGE_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { STORES } from '@n8n/stores';
|
import { STORES } from '@n8n/stores';
|
||||||
@@ -347,6 +348,15 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
|||||||
{ writeDefaults: false },
|
{ 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 {
|
return {
|
||||||
settings,
|
settings,
|
||||||
userManagement,
|
userManagement,
|
||||||
@@ -405,6 +415,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
|||||||
isAiCreditsEnabled,
|
isAiCreditsEnabled,
|
||||||
aiCreditsQuota,
|
aiCreditsQuota,
|
||||||
experimental__minZoomNodeSettingsInCanvas,
|
experimental__minZoomNodeSettingsInCanvas,
|
||||||
|
experimental__dockedNodeSettingsEnabled,
|
||||||
partialExecutionVersion,
|
partialExecutionVersion,
|
||||||
reset,
|
reset,
|
||||||
getTimezones,
|
getTimezones,
|
||||||
|
|||||||
Reference in New Issue
Block a user