mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
feat(editor): Merge experimental params pane into focus pane (no-changelog) (#18337)
This commit is contained in:
@@ -16,7 +16,8 @@ import { startCompletion } from '@codemirror/autocomplete';
|
|||||||
import type { EditorState, SelectionRange } from '@codemirror/state';
|
import type { EditorState, SelectionRange } from '@codemirror/state';
|
||||||
import type { IDataObject } from 'n8n-workflow';
|
import type { IDataObject } from 'n8n-workflow';
|
||||||
import { createEventBus, type EventBus } from '@n8n/utils/event-bus';
|
import { createEventBus, type EventBus } from '@n8n/utils/event-bus';
|
||||||
import { CanvasKey, ExpressionLocalResolveContextSymbol } from '@/constants';
|
import { CanvasKey } from '@/constants';
|
||||||
|
import { useIsInExperimentalNdv } from '@/components/canvas/experimental/composables/useIsInExperimentalNdv';
|
||||||
|
|
||||||
const isFocused = ref(false);
|
const isFocused = ref(false);
|
||||||
const segments = ref<Segment[]>([]);
|
const segments = ref<Segment[]>([]);
|
||||||
@@ -56,8 +57,7 @@ const ndvStore = useNDVStore();
|
|||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
|
||||||
const canvas = inject(CanvasKey, undefined);
|
const canvas = inject(CanvasKey, undefined);
|
||||||
const expressionLocalResolveCtx = inject(ExpressionLocalResolveContextSymbol, undefined);
|
const isInExperimentalNdv = useIsInExperimentalNdv();
|
||||||
const isInExperimentalNdv = computed(() => expressionLocalResolveCtx?.value !== undefined);
|
|
||||||
|
|
||||||
const isDragging = computed(() => ndvStore.isDraggableDragging);
|
const isDragging = computed(() => ndvStore.isDraggableDragging);
|
||||||
const isOutputPopoverVisible = computed(
|
const isOutputPopoverVisible = computed(
|
||||||
@@ -236,7 +236,7 @@ defineExpose({ focus, select });
|
|||||||
:segments="segments"
|
:segments="segments"
|
||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
:virtual-ref="container"
|
:virtual-ref="container"
|
||||||
:append-to="isInExperimentalNdv ? '#canvas' : undefined"
|
:append-to="isInExperimentalNdv ? 'body' : undefined"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -26,12 +26,16 @@ import {
|
|||||||
import { useEnvironmentsStore } from '@/stores/environments.ee.store';
|
import { useEnvironmentsStore } from '@/stores/environments.ee.store';
|
||||||
import { htmlEditorEventBus } from '@/event-bus';
|
import { htmlEditorEventBus } from '@/event-bus';
|
||||||
import { hasFocusOnInput, isFocusableEl } from '@/utils/typesUtils';
|
import { hasFocusOnInput, isFocusableEl } from '@/utils/typesUtils';
|
||||||
import type { ResizeData, TargetNodeParameterContext } from '@/Interface';
|
import type { INodeUi, ResizeData, TargetNodeParameterContext } from '@/Interface';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useThrottleFn } from '@vueuse/core';
|
import { useThrottleFn } from '@vueuse/core';
|
||||||
import { useStyles } from '@/composables/useStyles';
|
|
||||||
import { useExecutionData } from '@/composables/useExecutionData';
|
import { useExecutionData } from '@/composables/useExecutionData';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import ExperimentalNodeDetailsDrawer from '@/components/canvas/experimental/components/ExperimentalNodeDetailsDrawer.vue';
|
||||||
|
import { useExperimentalNdvStore } from '@/components/canvas/experimental/experimentalNdv.store';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { useVueFlow } from '@vue-flow/core';
|
||||||
|
import ExperimentalFocusPanelHeader from '@/components/canvas/experimental/components/ExperimentalFocusPanelHeader.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'FocusPanel' });
|
defineOptions({ name: 'FocusPanel' });
|
||||||
|
|
||||||
@@ -56,8 +60,10 @@ const nodeTypesStore = useNodeTypesStore();
|
|||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const nodeSettingsParameters = useNodeSettingsParameters();
|
const nodeSettingsParameters = useNodeSettingsParameters();
|
||||||
const environmentsStore = useEnvironmentsStore();
|
const environmentsStore = useEnvironmentsStore();
|
||||||
|
const experimentalNdvStore = useExperimentalNdvStore();
|
||||||
|
const ndvStore = useNDVStore();
|
||||||
const deviceSupport = useDeviceSupport();
|
const deviceSupport = useDeviceSupport();
|
||||||
const styles = useStyles();
|
const vueFlow = useVueFlow(workflowsStore.workflowId);
|
||||||
|
|
||||||
const focusedNodeParameter = computed(() => focusPanelStore.focusedNodeParameters[0]);
|
const focusedNodeParameter = computed(() => focusPanelStore.focusedNodeParameters[0]);
|
||||||
const resolvedParameter = computed(() =>
|
const resolvedParameter = computed(() =>
|
||||||
@@ -99,23 +105,28 @@ const isDisplayed = computed(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const node = computed<INodeUi | undefined>(() => {
|
||||||
|
if (!experimentalNdvStore.isNdvInFocusPanelEnabled || resolvedParameter.value) {
|
||||||
|
return resolvedParameter.value?.node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = vueFlow.getSelectedNodes.value[0]?.id;
|
||||||
|
|
||||||
|
return selected ? workflowsStore.allNodes.find((n) => n.id === selected) : undefined;
|
||||||
|
});
|
||||||
|
const multipleNodesSelected = computed(() => vueFlow.getSelectedNodes.value.length > 1);
|
||||||
|
|
||||||
const isExecutable = computed(() => {
|
const isExecutable = computed(() => {
|
||||||
if (!resolvedParameter.value) return false;
|
if (!node.value) return false;
|
||||||
|
|
||||||
if (!isDisplayed.value) return false;
|
if (!isDisplayed.value) return false;
|
||||||
|
|
||||||
const foreignCredentials = nodeHelpers.getForeignCredentialsIfSharingEnabled(
|
const foreignCredentials = nodeHelpers.getForeignCredentialsIfSharingEnabled(
|
||||||
resolvedParameter.value.node.credentials,
|
node.value.credentials,
|
||||||
);
|
|
||||||
return nodeHelpers.isNodeExecutable(
|
|
||||||
resolvedParameter.value.node,
|
|
||||||
!props.isCanvasReadOnly,
|
|
||||||
foreignCredentials,
|
|
||||||
);
|
);
|
||||||
|
return nodeHelpers.isNodeExecutable(node.value, !props.isCanvasReadOnly, foreignCredentials);
|
||||||
});
|
});
|
||||||
|
|
||||||
const node = computed(() => resolvedParameter.value?.node);
|
|
||||||
|
|
||||||
const { workflowRunData } = useExecutionData({ node });
|
const { workflowRunData } = useExecutionData({ node });
|
||||||
|
|
||||||
const hasNodeRun = computed(() => {
|
const hasNodeRun = computed(() => {
|
||||||
@@ -275,6 +286,11 @@ function optionSelected(command: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeFocusPanel() {
|
function closeFocusPanel() {
|
||||||
|
if (experimentalNdvStore.isNdvInFocusPanelEnabled && resolvedParameter.value) {
|
||||||
|
focusPanelStore.unsetParameters();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
telemetry.track('User closed focus panel', {
|
telemetry.track('User closed focus panel', {
|
||||||
source: 'closeIcon',
|
source: 'closeIcon',
|
||||||
parameters: focusPanelStore.focusedNodeParametersInTelemetryFormat,
|
parameters: focusPanelStore.focusedNodeParametersInTelemetryFormat,
|
||||||
@@ -354,6 +370,12 @@ function onResize(event: ResizeData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onResizeThrottle = useThrottleFn(onResize, 10);
|
const onResizeThrottle = useThrottleFn(onResize, 10);
|
||||||
|
|
||||||
|
function onOpenNdv() {
|
||||||
|
if (node.value) {
|
||||||
|
ndvStore.setActiveNodeName(node.value.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -362,14 +384,23 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
|
|||||||
:width="focusPanelWidth"
|
:width="focusPanelWidth"
|
||||||
:supported-directions="['left']"
|
:supported-directions="['left']"
|
||||||
:min-width="300"
|
:min-width="300"
|
||||||
:max-width="1000"
|
:max-width="experimentalNdvStore.isNdvInFocusPanelEnabled ? undefined : 1000"
|
||||||
:grid-size="8"
|
:grid-size="8"
|
||||||
:style="{ width: `${focusPanelWidth}px`, zIndex: styles.APP_Z_INDEXES.FOCUS_PANEL }"
|
:style="{ width: `${focusPanelWidth}px` }"
|
||||||
@resize="onResizeThrottle"
|
@resize="onResizeThrottle"
|
||||||
>
|
>
|
||||||
<div :class="$style.container">
|
<div :class="$style.container">
|
||||||
|
<ExperimentalFocusPanelHeader
|
||||||
|
v-if="experimentalNdvStore.isNdvInFocusPanelEnabled && node && !multipleNodesSelected"
|
||||||
|
:node="node"
|
||||||
|
:parameter="resolvedParameter?.parameter"
|
||||||
|
:is-executable="isExecutable"
|
||||||
|
@execute="onExecute"
|
||||||
|
@open-ndv="onOpenNdv"
|
||||||
|
@clear-parameter="closeFocusPanel"
|
||||||
|
/>
|
||||||
<div v-if="resolvedParameter" :class="$style.content">
|
<div v-if="resolvedParameter" :class="$style.content">
|
||||||
<div :class="$style.tabHeader">
|
<div v-if="!experimentalNdvStore.isNdvInFocusPanelEnabled" :class="$style.tabHeader">
|
||||||
<div :class="$style.tabHeaderText">
|
<div :class="$style.tabHeaderText">
|
||||||
<N8nText color="text-dark" size="small">
|
<N8nText color="text-dark" size="small">
|
||||||
{{ resolvedParameter.parameter.displayName }}
|
{{ resolvedParameter.parameter.displayName }}
|
||||||
@@ -513,6 +544,12 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ExperimentalNodeDetailsDrawer
|
||||||
|
v-else-if="node && experimentalNdvStore.isNdvInFocusPanelEnabled"
|
||||||
|
:node="node"
|
||||||
|
:nodes="vueFlow.getSelectedNodes.value"
|
||||||
|
@open-ndv="onOpenNdv"
|
||||||
|
/>
|
||||||
<div v-else :class="[$style.content, $style.emptyContent]">
|
<div v-else :class="[$style.content, $style.emptyContent]">
|
||||||
<div :class="$style.emptyText">
|
<div :class="$style.emptyText">
|
||||||
<div :class="$style.focusParameterWrapper">
|
<div :class="$style.focusParameterWrapper">
|
||||||
@@ -552,11 +589,14 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
|
|||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.wrapper {
|
.wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row nowrap;
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
border-left: 1px solid var(--color-foreground-base);
|
border-left: 1px solid var(--color-foreground-base);
|
||||||
background: var(--color-background-xlight);
|
background: var(--color-background-xlight);
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { useAssistantStore } from '@/stores/assistant.store';
|
|||||||
type Props = {
|
type Props = {
|
||||||
nodeViewScale: number;
|
nodeViewScale: number;
|
||||||
createNodeActive?: boolean;
|
createNodeActive?: boolean;
|
||||||
|
focusPanelActive: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
@@ -135,7 +136,14 @@ function onAskAssistantButtonClick() {
|
|||||||
:shortcut="{ keys: ['f'], shiftKey: true }"
|
:shortcut="{ keys: ['f'], shiftKey: true }"
|
||||||
placement="left"
|
placement="left"
|
||||||
>
|
>
|
||||||
<n8n-icon-button type="tertiary" size="large" icon="panel-right" @click="toggleFocusPanel" />
|
<n8n-icon-button
|
||||||
|
type="tertiary"
|
||||||
|
size="large"
|
||||||
|
icon="panel-right"
|
||||||
|
:class="focusPanelActive ? $style.activeButton : ''"
|
||||||
|
:active="focusPanelActive"
|
||||||
|
@click="toggleFocusPanel"
|
||||||
|
/>
|
||||||
</KeyboardShortcutTooltip>
|
</KeyboardShortcutTooltip>
|
||||||
<n8n-tooltip v-if="assistantStore.canShowAssistantButtonsOnCanvas" placement="left">
|
<n8n-tooltip v-if="assistantStore.canShowAssistantButtonsOnCanvas" placement="left">
|
||||||
<template #content> {{ i18n.baseText('aiAssistant.tooltip') }}</template>
|
<template #content> {{ i18n.baseText('aiAssistant.tooltip') }}</template>
|
||||||
@@ -185,4 +193,8 @@ function onAskAssistantButtonClick() {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.activeButton {
|
||||||
|
background-color: var(--button-hover-background-color) !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ describe('NodeDetailsView', () => {
|
|||||||
|
|
||||||
test('should unregister keydown listener on unmount', async () => {
|
test('should unregister keydown listener on unmount', async () => {
|
||||||
const { pinia, workflowObject, nodeName } = await createPiniaStore(false);
|
const { pinia, workflowObject, nodeName } = await createPiniaStore(false);
|
||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore(pinia);
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(NodeDetailsView, {
|
const renderComponent = createComponentRenderer(NodeDetailsView, {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -799,6 +799,7 @@ onBeforeUnmount(() => {
|
|||||||
:executable="!readOnly"
|
:executable="!readOnly"
|
||||||
:input-size="inputSize"
|
:input-size="inputSize"
|
||||||
:class="$style.settings"
|
:class="$style.settings"
|
||||||
|
is-ndv-v2
|
||||||
@execute="onNodeExecute"
|
@execute="onNodeExecute"
|
||||||
@stop-execution="onStopExecution"
|
@stop-execution="onStopExecution"
|
||||||
@activate="onWorkflowActivate"
|
@activate="onWorkflowActivate"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import type {
|
|||||||
IUpdateInformation,
|
IUpdateInformation,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
|
|
||||||
import { BASE_NODE_SURVEY_URL, NDV_UI_OVERHAUL_EXPERIMENT } from '@/constants';
|
import { BASE_NODE_SURVEY_URL } from '@/constants';
|
||||||
|
|
||||||
import ParameterInputList from '@/components/ParameterInputList.vue';
|
import ParameterInputList from '@/components/ParameterInputList.vue';
|
||||||
import NodeCredentials from '@/components/NodeCredentials.vue';
|
import NodeCredentials from '@/components/NodeCredentials.vue';
|
||||||
@@ -50,7 +50,6 @@ import { useTelemetry } from '@/composables/useTelemetry';
|
|||||||
import { importCurlEventBus, ndvEventBus } from '@/event-bus';
|
import { importCurlEventBus, ndvEventBus } from '@/event-bus';
|
||||||
import { ProjectTypes } from '@/types/projects.types';
|
import { ProjectTypes } from '@/types/projects.types';
|
||||||
import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue';
|
import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
|
||||||
import { useResizeObserver } from '@vueuse/core';
|
import { useResizeObserver } from '@vueuse/core';
|
||||||
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
|
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
|
||||||
import { N8nBlockUi, N8nIcon, N8nNotice, N8nText } from '@n8n/design-system';
|
import { N8nBlockUi, N8nIcon, N8nNotice, N8nText } from '@n8n/design-system';
|
||||||
@@ -75,12 +74,20 @@ const props = withDefaults(
|
|||||||
subTitle?: string;
|
subTitle?: string;
|
||||||
extraTabsClassName?: string;
|
extraTabsClassName?: string;
|
||||||
extraParameterWrapperClassName?: string;
|
extraParameterWrapperClassName?: string;
|
||||||
|
isNdvV2?: boolean;
|
||||||
|
hideExecute?: boolean;
|
||||||
|
hideDocs?: boolean;
|
||||||
|
hideSubConnections?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
inputSize: 0,
|
inputSize: 0,
|
||||||
activeNode: undefined,
|
activeNode: undefined,
|
||||||
isEmbeddedInCanvas: false,
|
isEmbeddedInCanvas: false,
|
||||||
subTitle: undefined,
|
subTitle: undefined,
|
||||||
|
isNdvV2: false,
|
||||||
|
hideExecute: false,
|
||||||
|
hideDocs: true,
|
||||||
|
hideSubConnections: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -108,7 +115,6 @@ const ndvStore = useNDVStore();
|
|||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const credentialsStore = useCredentialsStore();
|
const credentialsStore = useCredentialsStore();
|
||||||
const historyStore = useHistoryStore();
|
const historyStore = useHistoryStore();
|
||||||
const posthogStore = usePostHog();
|
|
||||||
|
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
@@ -252,13 +258,6 @@ const credentialOwnerName = computed(() => {
|
|||||||
return credentialsStore.getCredentialOwnerName(credential);
|
return credentialsStore.getCredentialOwnerName(credential);
|
||||||
});
|
});
|
||||||
|
|
||||||
const isNDVV2 = computed(() =>
|
|
||||||
posthogStore.isVariantEnabled(
|
|
||||||
NDV_UI_OVERHAUL_EXPERIMENT.name,
|
|
||||||
NDV_UI_OVERHAUL_EXPERIMENT.variant,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const featureRequestUrl = computed(() => {
|
const featureRequestUrl = computed(() => {
|
||||||
if (!nodeType.value) {
|
if (!nodeType.value) {
|
||||||
return '';
|
return '';
|
||||||
@@ -611,7 +610,7 @@ function handleSelectAction(params: INodeParameters) {
|
|||||||
<slot name="actions" />
|
<slot name="actions" />
|
||||||
</template>
|
</template>
|
||||||
</ExperimentalEmbeddedNdvHeader>
|
</ExperimentalEmbeddedNdvHeader>
|
||||||
<div v-else-if="!isNDVV2" :class="$style.header">
|
<div v-else-if="!isNdvV2" :class="$style.header">
|
||||||
<div class="header-side-menu">
|
<div class="header-side-menu">
|
||||||
<NodeTitle
|
<NodeTitle
|
||||||
v-if="node"
|
v-if="node"
|
||||||
@@ -648,9 +647,10 @@ function handleSelectAction(params: INodeParameters) {
|
|||||||
:node-name="node.name"
|
:node-name="node.name"
|
||||||
:node-type="nodeType"
|
:node-type="nodeType"
|
||||||
:execute-button-tooltip="executeButtonTooltip"
|
:execute-button-tooltip="executeButtonTooltip"
|
||||||
:hide-execute="!isExecutable || blockUI || !node || !nodeValid"
|
:hide-execute="props.hideExecute || !isExecutable || blockUI || !node || !nodeValid"
|
||||||
:disable-execute="outputPanelEditMode.enabled && !isTriggerNode"
|
:disable-execute="outputPanelEditMode.enabled && !isTriggerNode"
|
||||||
:hide-tabs="!nodeValid"
|
:hide-tabs="!nodeValid"
|
||||||
|
:hide-docs="props.hideDocs"
|
||||||
:push-ref="pushRef"
|
:push-ref="pushRef"
|
||||||
@execute="onNodeExecute"
|
@execute="onNodeExecute"
|
||||||
@stop-execution="onStopExecution"
|
@stop-execution="onStopExecution"
|
||||||
@@ -666,7 +666,7 @@ function handleSelectAction(params: INodeParameters) {
|
|||||||
:class="[
|
:class="[
|
||||||
'node-parameters-wrapper',
|
'node-parameters-wrapper',
|
||||||
shouldShowStaticScrollbar ? 'with-static-scrollbar' : '',
|
shouldShowStaticScrollbar ? 'with-static-scrollbar' : '',
|
||||||
{ 'ndv-v2': isNDVV2 },
|
{ 'ndv-v2': isNdvV2 },
|
||||||
extraParameterWrapperClassName ?? '',
|
extraParameterWrapperClassName ?? '',
|
||||||
]"
|
]"
|
||||||
data-test-id="node-parameters"
|
data-test-id="node-parameters"
|
||||||
@@ -782,7 +782,7 @@ function handleSelectAction(params: INodeParameters) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isNDVV2 && featureRequestUrl && !isEmbeddedInCanvas"
|
v-if="isNdvV2 && featureRequestUrl && !isEmbeddedInCanvas"
|
||||||
:class="$style.featureRequest"
|
:class="$style.featureRequest"
|
||||||
>
|
>
|
||||||
<a target="_blank" @click="onFeatureRequestClick">
|
<a target="_blank" @click="onFeatureRequestClick">
|
||||||
@@ -792,7 +792,7 @@ function handleSelectAction(params: INodeParameters) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NDVSubConnections
|
<NDVSubConnections
|
||||||
v-if="node && !props.isEmbeddedInCanvas"
|
v-if="node && !hideSubConnections"
|
||||||
ref="subConnections"
|
ref="subConnections"
|
||||||
:root-node="node"
|
:root-node="node"
|
||||||
@switch-selected-node="onSwitchSelectedNode"
|
@switch-selected-node="onSwitchSelectedNode"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { NodeSettingsTab } from '@/types/nodeSettings';
|
|||||||
type Props = {
|
type Props = {
|
||||||
nodeName: string;
|
nodeName: string;
|
||||||
hideExecute: boolean;
|
hideExecute: boolean;
|
||||||
|
hideDocs: boolean;
|
||||||
hideTabs: boolean;
|
hideTabs: boolean;
|
||||||
disableExecute: boolean;
|
disableExecute: boolean;
|
||||||
executeButtonTooltip: string;
|
executeButtonTooltip: string;
|
||||||
@@ -30,7 +31,7 @@ const emit = defineEmits<{
|
|||||||
<div :class="$style.header">
|
<div :class="$style.header">
|
||||||
<NodeSettingsTabs
|
<NodeSettingsTabs
|
||||||
v-if="!hideTabs"
|
v-if="!hideTabs"
|
||||||
hide-docs
|
:hide-docs="hideDocs"
|
||||||
:model-value="selectedTab"
|
:model-value="selectedTab"
|
||||||
:node-type="nodeType"
|
:node-type="nodeType"
|
||||||
:push-ref="pushRef"
|
:push-ref="pushRef"
|
||||||
@@ -61,11 +62,14 @@ const emit = defineEmits<{
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
padding-right: var(--spacing-s);
|
|
||||||
|
|
||||||
border-bottom: var(--border-base);
|
border-bottom: var(--border-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.execute {
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { computed } from 'vue';
|
|||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { AI_TRANSFORM_NODE_TYPE } from '@/constants';
|
import { AI_TRANSFORM_NODE_TYPE } from '@/constants';
|
||||||
import { getParameterTypeOption } from '@/utils/nodeSettingsUtils';
|
import { getParameterTypeOption } from '@/utils/nodeSettingsUtils';
|
||||||
|
import { useIsInExperimentalNdv } from '@/components/canvas/experimental/composables/useIsInExperimentalNdv';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
parameter: INodeProperties;
|
parameter: INodeProperties;
|
||||||
@@ -51,13 +52,14 @@ const isHtmlEditor = computed(
|
|||||||
const shouldShowExpressionSelector = computed(
|
const shouldShowExpressionSelector = computed(
|
||||||
() => !props.parameter.noDataExpression && props.showExpressionSelector && !props.isReadOnly,
|
() => !props.parameter.noDataExpression && props.showExpressionSelector && !props.isReadOnly,
|
||||||
);
|
);
|
||||||
|
const isInEmbeddedNdv = useIsInExperimentalNdv();
|
||||||
|
|
||||||
const canBeOpenedInFocusPanel = computed(
|
const canBeOpenedInFocusPanel = computed(
|
||||||
() =>
|
() =>
|
||||||
!props.parameter.isNodeSetting &&
|
!props.parameter.isNodeSetting &&
|
||||||
!props.isReadOnly &&
|
!props.isReadOnly &&
|
||||||
!props.isContentOverridden &&
|
!props.isContentOverridden &&
|
||||||
activeNode.value && // checking that it's inside ndv
|
(activeNode.value || isInEmbeddedNdv.value) && // checking that it's inside ndv
|
||||||
(props.parameter.type === 'string' || props.parameter.type === 'json'),
|
(props.parameter.type === 'string' || props.parameter.type === 'json'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -922,7 +922,7 @@ provide(CanvasKey, {
|
|||||||
snap-to-grid
|
snap-to-grid
|
||||||
:snap-grid="[GRID_SIZE, GRID_SIZE]"
|
:snap-grid="[GRID_SIZE, GRID_SIZE]"
|
||||||
:min-zoom="0"
|
:min-zoom="0"
|
||||||
:max-zoom="experimentalNdvStore.isEnabled ? experimentalNdvStore.maxCanvasZoom : 4"
|
:max-zoom="experimentalNdvStore.isZoomedViewEnabled ? experimentalNdvStore.maxCanvasZoom : 4"
|
||||||
:selection-key-code="selectionKeyCode"
|
:selection-key-code="selectionKeyCode"
|
||||||
:zoom-activation-key-code="panningKeyCode"
|
:zoom-activation-key-code="panningKeyCode"
|
||||||
:pan-activation-key-code="panningKeyCode"
|
:pan-activation-key-code="panningKeyCode"
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ 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 './experimental/components/ExperimentalNodeDetailsDrawer.vue';
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
@@ -36,9 +34,8 @@ const props = withDefaults(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
const settingsStore = useSettingsStore();
|
|
||||||
|
|
||||||
const { onNodesInitialized, getSelectedNodes } = useVueFlow(props.id);
|
const { onNodesInitialized } = useVueFlow(props.id);
|
||||||
|
|
||||||
const workflow = toRef(props, 'workflow');
|
const workflow = toRef(props, 'workflow');
|
||||||
const workflowObject = toRef(props, 'workflowObject');
|
const workflowObject = toRef(props, 'workflowObject');
|
||||||
@@ -83,10 +80,6 @@ const mappedConnectionsThrottled = throttledRef(mappedConnections, 200);
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
<ExperimentalNodeDetailsDrawer
|
|
||||||
v-if="settingsStore.experimental__dockedNodeSettingsEnabled && !props.readOnly"
|
|
||||||
:selected-nodes="getSelectedNodes"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const experimentalNdvStore = useExperimentalNdvStore();
|
|||||||
|
|
||||||
const isExperimentalNdvActive = computed(() => experimentalNdvStore.isActive(props.zoom));
|
const isExperimentalNdvActive = computed(() => experimentalNdvStore.isActive(props.zoom));
|
||||||
|
|
||||||
const isToggleZoomVisible = computed(() => experimentalNdvStore.isEnabled);
|
const isToggleZoomVisible = computed(() => experimentalNdvStore.isZoomedViewEnabled);
|
||||||
|
|
||||||
const isResetZoomVisible = computed(() => !isToggleZoomVisible.value && props.zoom !== 1);
|
const isResetZoomVisible = computed(() => !isToggleZoomVisible.value && props.zoom !== 1);
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ const isDisableNodeVisible = computed(() => {
|
|||||||
|
|
||||||
const isDeleteNodeVisible = computed(() => !props.readOnly);
|
const isDeleteNodeVisible = computed(() => !props.readOnly);
|
||||||
|
|
||||||
const isFocusNodeVisible = computed(() => experimentalNdvStore.isEnabled);
|
const isFocusNodeVisible = computed(() => experimentalNdvStore.isZoomedViewEnabled);
|
||||||
|
|
||||||
const isStickyNoteChangeColorVisible = computed(
|
const isStickyNoteChangeColorVisible = computed(
|
||||||
() => !props.readOnly && render.value.type === CanvasNodeRenderType.StickyNote,
|
() => !props.readOnly && render.value.type === CanvasNodeRenderType.StickyNote,
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import { useUIStore } from '@/stores/ui.store';
|
|||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const { nodeId, isReadOnly, subTitle } = defineProps<{
|
const { nodeId, isReadOnly, subTitle, isEmbeddedInCanvas } = defineProps<{
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
subTitle?: string;
|
subTitle?: string;
|
||||||
|
isEmbeddedInCanvas?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineSlots<{ actions?: {} }>();
|
defineSlots<{ actions?: {} }>();
|
||||||
@@ -75,10 +76,14 @@ function handleCaptureWheelEvent(event: WheelEvent) {
|
|||||||
:read-only="isReadOnly"
|
:read-only="isReadOnly"
|
||||||
:block-u-i="blockUi"
|
:block-u-i="blockUi"
|
||||||
:executable="!isReadOnly"
|
:executable="!isReadOnly"
|
||||||
is-embedded-in-canvas
|
:is-embedded-in-canvas="isEmbeddedInCanvas"
|
||||||
:sub-title="subTitle"
|
:sub-title="subTitle"
|
||||||
extra-tabs-class-name="nodrag"
|
extra-tabs-class-name="nodrag"
|
||||||
extra-parameter-wrapper-class-name="nodrag"
|
extra-parameter-wrapper-class-name="nodrag"
|
||||||
|
is-ndv-v2
|
||||||
|
hide-execute
|
||||||
|
:hide-docs="false"
|
||||||
|
hide-sub-connections
|
||||||
@value-changed="handleValueChanged"
|
@value-changed="handleValueChanged"
|
||||||
@capture-wheel-body="handleCaptureWheelEvent"
|
@capture-wheel-body="handleCaptureWheelEvent"
|
||||||
@dblclick-header="handleDoubleClickHeader"
|
@dblclick-header="handleDoubleClickHeader"
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ defineExpose({
|
|||||||
:popper-class="$style.component"
|
:popper-class="$style.component"
|
||||||
:width="360"
|
:width="360"
|
||||||
:offset="8"
|
:offset="8"
|
||||||
append-to="#canvas"
|
append-to="body"
|
||||||
:popper-options="{
|
:popper-options="{
|
||||||
modifiers: [{ name: 'flip', enabled: false }],
|
modifiers: [{ name: 'flip', enabled: false }],
|
||||||
}"
|
}"
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ExpressionLocalResolveContextSymbol } from '@/constants';
|
import { ExpressionLocalResolveContextSymbol } from '@/constants';
|
||||||
import { useEnvironmentsStore } from '@/stores/environments.ee.store';
|
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
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 type { ExpressionLocalResolveContext } from '@/types/expressions';
|
|
||||||
import { N8nText } from '@n8n/design-system';
|
import { N8nText } from '@n8n/design-system';
|
||||||
import { useVueFlow } from '@vue-flow/core';
|
import { useVueFlow } from '@vue-flow/core';
|
||||||
import { watchOnce } from '@vueuse/core';
|
import { watchOnce } from '@vueuse/core';
|
||||||
@@ -12,11 +10,11 @@ import { computed, provide, ref } from 'vue';
|
|||||||
import { useExperimentalNdvStore } from '../experimentalNdv.store';
|
import { useExperimentalNdvStore } from '../experimentalNdv.store';
|
||||||
import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue';
|
import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue';
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import type { Workflow } from 'n8n-workflow';
|
|
||||||
import NodeIcon from '@/components/NodeIcon.vue';
|
import NodeIcon from '@/components/NodeIcon.vue';
|
||||||
import { getNodeSubTitleText } from '@/components/canvas/experimental/experimentalNdv.utils';
|
import { getNodeSubTitleText } from '@/components/canvas/experimental/experimentalNdv.utils';
|
||||||
import ExperimentalEmbeddedNdvActions from '@/components/canvas/experimental/components/ExperimentalEmbeddedNdvActions.vue';
|
import ExperimentalEmbeddedNdvActions from '@/components/canvas/experimental/components/ExperimentalEmbeddedNdvActions.vue';
|
||||||
import { useCanvas } from '@/composables/useCanvas';
|
import { useCanvas } from '@/composables/useCanvas';
|
||||||
|
import { useExpressionResolveCtx } from '@/components/canvas/experimental/composables/useExpressionResolveCtx';
|
||||||
|
|
||||||
const { nodeId, isReadOnly } = defineProps<{
|
const { nodeId, isReadOnly } = defineProps<{
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@@ -56,54 +54,10 @@ const subTitle = computed(() =>
|
|||||||
? getNodeSubTitleText(node.value, nodeType.value, !isExpanded.value, i18n)
|
? getNodeSubTitleText(node.value, nodeType.value, !isExpanded.value, i18n)
|
||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
const expressionResolveCtx = computed<ExpressionLocalResolveContext | undefined>(() => {
|
|
||||||
if (!node.value) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const runIndex = 0; // not changeable for now
|
|
||||||
const execution = workflowsStore.workflowExecutionData;
|
|
||||||
const nodeName = node.value.name;
|
|
||||||
|
|
||||||
function findInputNode(): ExpressionLocalResolveContext['inputNode'] {
|
|
||||||
const taskData = (execution?.data?.resultData.runData[nodeName] ?? [])[runIndex];
|
|
||||||
const source = taskData?.source[0];
|
|
||||||
|
|
||||||
if (source) {
|
|
||||||
return {
|
|
||||||
name: source.previousNode,
|
|
||||||
branchIndex: source.previousNodeOutput ?? 0,
|
|
||||||
runIndex: source.previousNodeRun ?? 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputs = workflowObject.value.getParentNodesByDepth(nodeName, 1);
|
|
||||||
|
|
||||||
if (inputs.length > 0) {
|
|
||||||
return {
|
|
||||||
name: inputs[0].name,
|
|
||||||
branchIndex: inputs[0].indicies[0] ?? 0,
|
|
||||||
runIndex: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
localResolve: true,
|
|
||||||
envVars: useEnvironmentsStore().variablesAsObject,
|
|
||||||
workflow: workflowObject.value,
|
|
||||||
execution,
|
|
||||||
nodeName,
|
|
||||||
additionalKeys: {},
|
|
||||||
inputNode: findInputNode(),
|
|
||||||
connections: workflowsStore.connectionsBySourceNode,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const maxHeightOnFocus = computed(() => vf.dimensions.value.height * 0.8);
|
const maxHeightOnFocus = computed(() => vf.dimensions.value.height * 0.8);
|
||||||
const workflowObject = computed(() => workflowsStore.workflowObject as Workflow);
|
|
||||||
|
const expressionResolveCtx = useExpressionResolveCtx(node);
|
||||||
|
|
||||||
function handleToggleExpand() {
|
function handleToggleExpand() {
|
||||||
experimentalNdvStore.setNodeExpanded(nodeId);
|
experimentalNdvStore.setNodeExpanded(nodeId);
|
||||||
@@ -143,6 +97,7 @@ watchOnce(isVisible, (visible) => {
|
|||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
:sub-title="subTitle"
|
:sub-title="subTitle"
|
||||||
:input-node-name="expressionResolveCtx?.inputNode?.name"
|
:input-node-name="expressionResolveCtx?.inputNode?.name"
|
||||||
|
is-embedded-in-canvas
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<ExperimentalEmbeddedNdvActions
|
<ExperimentalEmbeddedNdvActions
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import NodeExecuteButton from '@/components/NodeExecuteButton.vue';
|
||||||
|
import NodeIcon from '@/components/NodeIcon.vue';
|
||||||
|
import { type INodeUi } from '@/Interface';
|
||||||
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { N8nIconButton, N8nText } from '@n8n/design-system';
|
||||||
|
import { type INodeProperties } from 'n8n-workflow';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const { node, parameter, isExecutable } = defineProps<{
|
||||||
|
node: INodeUi;
|
||||||
|
parameter?: INodeProperties;
|
||||||
|
isExecutable: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
const nodeType = computed(() => nodeTypesStore.getNodeType(node.type, node.typeVersion));
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
execute: [];
|
||||||
|
openNdv: [];
|
||||||
|
clearParameter: [];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<N8nText tag="div" size="small" bold :class="$style.component">
|
||||||
|
<NodeIcon :node-type="nodeType" :size="16" />
|
||||||
|
<div :class="$style.breadcrumbs">
|
||||||
|
<template v-if="parameter">
|
||||||
|
<N8nText size="small" color="text-base" bold>
|
||||||
|
{{ node.name }}
|
||||||
|
</N8nText>
|
||||||
|
<N8nText size="small" color="text-light">/</N8nText>
|
||||||
|
{{ parameter.displayName }}
|
||||||
|
</template>
|
||||||
|
<template v-else>{{ node.name }}</template>
|
||||||
|
</div>
|
||||||
|
<N8nIconButton
|
||||||
|
v-if="parameter"
|
||||||
|
icon="x"
|
||||||
|
size="small"
|
||||||
|
type="tertiary"
|
||||||
|
text
|
||||||
|
@click="emit('clearParameter')"
|
||||||
|
/>
|
||||||
|
<N8nIconButton
|
||||||
|
v-else
|
||||||
|
icon="maximize-2"
|
||||||
|
size="small"
|
||||||
|
type="tertiary"
|
||||||
|
text
|
||||||
|
@click="emit('openNdv')"
|
||||||
|
/>
|
||||||
|
<NodeExecuteButton
|
||||||
|
v-if="isExecutable"
|
||||||
|
data-test-id="node-execute-button"
|
||||||
|
:node-name="node.name"
|
||||||
|
:tooltip="`Execute ${node.name}`"
|
||||||
|
size="small"
|
||||||
|
icon="play"
|
||||||
|
:square="true"
|
||||||
|
:hide-label="true"
|
||||||
|
telemetry-source="focus"
|
||||||
|
@execute="emit('execute')"
|
||||||
|
/>
|
||||||
|
</N8nText>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.component {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-2xs);
|
||||||
|
gap: var(--spacing-2xs);
|
||||||
|
border-bottom: var(--border-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-4xs);
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,50 +1,25 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type CanvasNode } from '@/types';
|
import { useExpressionResolveCtx } from '@/components/canvas/experimental/composables/useExpressionResolveCtx';
|
||||||
import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue';
|
import { ExpressionLocalResolveContextSymbol } from '@/constants';
|
||||||
|
import { type INodeUi } from '@/Interface';
|
||||||
import { N8nText } from '@n8n/design-system';
|
import { N8nText } from '@n8n/design-system';
|
||||||
import { computed, watch, ref } from 'vue';
|
import { type GraphNode } from '@vue-flow/core';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { computed, provide } from 'vue';
|
||||||
|
import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue';
|
||||||
|
|
||||||
const { selectedNodes } = defineProps<{ selectedNodes: CanvasNode[] }>();
|
const { node, nodes } = defineProps<{ node: INodeUi; nodes: GraphNode[] }>();
|
||||||
|
|
||||||
const content = computed(() =>
|
const emit = defineEmits<{ openNdv: [] }>();
|
||||||
selectedNodes.length > 1
|
|
||||||
? `${selectedNodes.length} nodes selected`
|
|
||||||
: selectedNodes.length > 0
|
|
||||||
? selectedNodes[0]
|
|
||||||
: undefined,
|
|
||||||
);
|
|
||||||
const lastContent = ref<string | CanvasNode | undefined>();
|
|
||||||
const { setActiveNodeName } = useNDVStore();
|
|
||||||
|
|
||||||
function handleOpenNdv() {
|
const expressionResolveCtx = useExpressionResolveCtx(computed(() => node));
|
||||||
if (typeof content.value === 'object' && content.value.data) {
|
|
||||||
setActiveNodeName(content.value.data.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync lastContent to be "last defined content" (for drawer animation)
|
provide(ExpressionLocalResolveContextSymbol, expressionResolveCtx);
|
||||||
watch(
|
|
||||||
content,
|
|
||||||
(newContent) => {
|
|
||||||
if (newContent !== undefined) {
|
|
||||||
lastContent.value = newContent;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.component, content === undefined ? $style.closed : '']">
|
<div :class="$style.component">
|
||||||
<N8nText v-if="typeof lastContent === 'string'" color="text-base">
|
<N8nText v-if="nodes.length > 1" color="text-base"> {{ nodes.length }} nodes selected </N8nText>
|
||||||
{{ lastContent }}
|
<ExperimentalCanvasNodeSettings v-else-if="node" :key="node.id" :node-id="node.id">
|
||||||
</N8nText>
|
|
||||||
<ExperimentalCanvasNodeSettings
|
|
||||||
v-else-if="lastContent !== undefined"
|
|
||||||
:key="lastContent.id"
|
|
||||||
:node-id="lastContent.id"
|
|
||||||
>
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<N8nIconButton
|
<N8nIconButton
|
||||||
icon="maximize-2"
|
icon="maximize-2"
|
||||||
@@ -53,7 +28,7 @@ watch(
|
|||||||
size="mini"
|
size="mini"
|
||||||
icon-size="large"
|
icon-size="large"
|
||||||
aria-label="Expand"
|
aria-label="Expand"
|
||||||
@click="handleOpenNdv"
|
@click="emit('openNdv')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ExperimentalCanvasNodeSettings>
|
</ExperimentalCanvasNodeSettings>
|
||||||
@@ -62,22 +37,10 @@ watch(
|
|||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.component {
|
.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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: transform 0.2s ease;
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
&.closed {
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import type { INodeUi } from '@/Interface';
|
||||||
|
import useEnvironmentsStore from '@/stores/environments.ee.store';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import type { ExpressionLocalResolveContext } from '@/types/expressions';
|
||||||
|
import type { Workflow } from 'n8n-workflow';
|
||||||
|
import { computed, type ComputedRef } from 'vue';
|
||||||
|
|
||||||
|
export function useExpressionResolveCtx(node: ComputedRef<INodeUi | null | undefined>) {
|
||||||
|
const environmentsStore = useEnvironmentsStore();
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
const workflowObject = computed(() => workflowsStore.workflowObject as Workflow);
|
||||||
|
|
||||||
|
return computed<ExpressionLocalResolveContext | undefined>(() => {
|
||||||
|
if (!node.value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const runIndex = 0; // not changeable for now
|
||||||
|
const execution = workflowsStore.workflowExecutionData;
|
||||||
|
const nodeName = node.value.name;
|
||||||
|
|
||||||
|
function findInputNode(): ExpressionLocalResolveContext['inputNode'] {
|
||||||
|
const taskData = (execution?.data?.resultData.runData[nodeName] ?? [])[runIndex];
|
||||||
|
const source = taskData?.source[0];
|
||||||
|
|
||||||
|
if (source) {
|
||||||
|
return {
|
||||||
|
name: source.previousNode,
|
||||||
|
branchIndex: source.previousNodeOutput ?? 0,
|
||||||
|
runIndex: source.previousNodeRun ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputs = workflowObject.value.getParentNodesByDepth(nodeName, 1);
|
||||||
|
|
||||||
|
if (inputs.length > 0) {
|
||||||
|
return {
|
||||||
|
name: inputs[0].name,
|
||||||
|
branchIndex: inputs[0].indicies[0] ?? 0,
|
||||||
|
runIndex: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
localResolve: true,
|
||||||
|
envVars: environmentsStore.variablesAsObject,
|
||||||
|
workflow: workflowObject.value,
|
||||||
|
execution,
|
||||||
|
nodeName,
|
||||||
|
additionalKeys: {},
|
||||||
|
inputNode: findInputNode(),
|
||||||
|
connections: workflowsStore.connectionsBySourceNode,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ExpressionLocalResolveContextSymbol } from '@/constants';
|
||||||
|
import { computed, inject } from 'vue';
|
||||||
|
|
||||||
|
export function useIsInExperimentalNdv() {
|
||||||
|
const expressionLocalResolveCtx = inject(ExpressionLocalResolveContextSymbol, undefined);
|
||||||
|
|
||||||
|
// This condition is correct as long as ExpressionLocalResolveContext is used only in experimental NDV
|
||||||
|
return computed(() => expressionLocalResolveCtx?.value !== undefined);
|
||||||
|
}
|
||||||
@@ -12,17 +12,22 @@ import {
|
|||||||
} from '@vue-flow/core';
|
} from '@vue-flow/core';
|
||||||
import { CanvasNodeRenderType, type CanvasNodeData } from '@/types';
|
import { CanvasNodeRenderType, type CanvasNodeData } from '@/types';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
import { CANVAS_ZOOMED_VIEW_EXPERIMENT } from '@/constants';
|
import { CANVAS_ZOOMED_VIEW_EXPERIMENT, NDV_IN_FOCUS_PANEL_EXPERIMENT } from '@/constants';
|
||||||
|
|
||||||
export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
|
export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
|
||||||
const workflowStore = useWorkflowsStore();
|
const workflowStore = useWorkflowsStore();
|
||||||
const postHogStore = usePostHog();
|
const postHogStore = usePostHog();
|
||||||
const isEnabled = computed(
|
const isZoomedViewEnabled = computed(
|
||||||
() =>
|
() =>
|
||||||
postHogStore.getVariant(CANVAS_ZOOMED_VIEW_EXPERIMENT.name) ===
|
postHogStore.getVariant(CANVAS_ZOOMED_VIEW_EXPERIMENT.name) ===
|
||||||
CANVAS_ZOOMED_VIEW_EXPERIMENT.variant,
|
CANVAS_ZOOMED_VIEW_EXPERIMENT.variant,
|
||||||
);
|
);
|
||||||
const maxCanvasZoom = computed(() => (isEnabled.value ? 2 : 4));
|
const isNdvInFocusPanelEnabled = computed(
|
||||||
|
() =>
|
||||||
|
postHogStore.getVariant(NDV_IN_FOCUS_PANEL_EXPERIMENT.name) ===
|
||||||
|
NDV_IN_FOCUS_PANEL_EXPERIMENT.variant,
|
||||||
|
);
|
||||||
|
const maxCanvasZoom = computed(() => (isZoomedViewEnabled.value ? 2 : 4));
|
||||||
|
|
||||||
const previousViewport = ref<ViewportTransform>();
|
const previousViewport = ref<ViewportTransform>();
|
||||||
const collapsedNodes = shallowRef<Partial<Record<string, boolean>>>({});
|
const collapsedNodes = shallowRef<Partial<Record<string, boolean>>>({});
|
||||||
@@ -50,7 +55,7 @@ export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isActive(canvasZoom: number) {
|
function isActive(canvasZoom: number) {
|
||||||
return isEnabled.value && Math.abs(canvasZoom - maxCanvasZoom.value) < 0.000001;
|
return isZoomedViewEnabled.value && Math.abs(canvasZoom - maxCanvasZoom.value) < 0.000001;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setNodeNameToBeFocused(nodeName: string) {
|
function setNodeNameToBeFocused(nodeName: string) {
|
||||||
@@ -139,7 +144,8 @@ export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isEnabled,
|
isZoomedViewEnabled,
|
||||||
|
isNdvInFocusPanelEnabled,
|
||||||
maxCanvasZoom,
|
maxCanvasZoom,
|
||||||
previousZoom: computed(() => previousViewport.value),
|
previousZoom: computed(() => previousViewport.value),
|
||||||
collapsedNodes: computed(() => collapsedNodes.value),
|
collapsedNodes: computed(() => collapsedNodes.value),
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ import { isChatNode } from '@/utils/aiUtils';
|
|||||||
import cloneDeep from 'lodash/cloneDeep';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
import { useExperimentalNdvStore } from '@/components/canvas/experimental/experimentalNdv.store';
|
import { useExperimentalNdvStore } from '@/components/canvas/experimental/experimentalNdv.store';
|
||||||
|
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
||||||
|
|
||||||
type AddNodeData = Partial<INodeUi> & {
|
type AddNodeData = Partial<INodeUi> & {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -158,6 +159,7 @@ export function useCanvasOperations() {
|
|||||||
const projectsStore = useProjectsStore();
|
const projectsStore = useProjectsStore();
|
||||||
const logsStore = useLogsStore();
|
const logsStore = useLogsStore();
|
||||||
const experimentalNdvStore = useExperimentalNdvStore();
|
const experimentalNdvStore = useExperimentalNdvStore();
|
||||||
|
const focusPanelStore = useFocusPanelStore();
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -793,7 +795,13 @@ export function useCanvasOperations() {
|
|||||||
void externalHooks.run('nodeView.addNodeButton', { nodeTypeName: nodeData.type });
|
void externalHooks.run('nodeView.addNodeButton', { nodeTypeName: nodeData.type });
|
||||||
|
|
||||||
if (options.openNDV && !preventOpeningNDV) {
|
if (options.openNDV && !preventOpeningNDV) {
|
||||||
if (experimentalNdvStore.isEnabled) {
|
if (
|
||||||
|
experimentalNdvStore.isNdvInFocusPanelEnabled &&
|
||||||
|
focusPanelStore.focusPanelActive &&
|
||||||
|
focusPanelStore.focusedNodeParameters.length === 0
|
||||||
|
) {
|
||||||
|
// Do nothing. The added node get selected and the details are shown in the focus panel
|
||||||
|
} else if (experimentalNdvStore.isZoomedViewEnabled) {
|
||||||
experimentalNdvStore.setNodeNameToBeFocused(nodeData.name);
|
experimentalNdvStore.setNodeNameToBeFocused(nodeData.name);
|
||||||
} else {
|
} else {
|
||||||
ndvStore.setActiveNodeName(nodeData.name);
|
ndvStore.setActiveNodeName(nodeData.name);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ const APP_Z_INDEXES = {
|
|||||||
APP_SIDEBAR: 999,
|
APP_SIDEBAR: 999,
|
||||||
CANVAS_SELECT_BOX: 100,
|
CANVAS_SELECT_BOX: 100,
|
||||||
TOP_BANNERS: 999,
|
TOP_BANNERS: 999,
|
||||||
FOCUS_PANEL: 1600,
|
|
||||||
NODE_CREATOR: 1700,
|
NODE_CREATOR: 1700,
|
||||||
NDV: 1800,
|
NDV: 1800,
|
||||||
MODALS: 2000,
|
MODALS: 2000,
|
||||||
|
|||||||
@@ -506,8 +506,6 @@ export const LOCAL_STORAGE_LOGS_SYNC_SELECTION = 'N8N_LOGS_SYNC_SELECTION_ENABLE
|
|||||||
export const LOCAL_STORAGE_LOGS_PANEL_DETAILS_PANEL = 'N8N_LOGS_DETAILS_PANEL';
|
export const LOCAL_STORAGE_LOGS_PANEL_DETAILS_PANEL = 'N8N_LOGS_DETAILS_PANEL';
|
||||||
export const LOCAL_STORAGE_LOGS_PANEL_DETAILS_PANEL_SUB_NODE = 'N8N_LOGS_DETAILS_PANEL_SUB_NODE';
|
export const LOCAL_STORAGE_LOGS_PANEL_DETAILS_PANEL_SUB_NODE = 'N8N_LOGS_DETAILS_PANEL_SUB_NODE';
|
||||||
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_DOCKED_NODE_SETTINGS =
|
|
||||||
'N8N_EXPERIMENTAL_DOCKED_NODE_SETTINGS';
|
|
||||||
export const LOCAL_STORAGE_READ_WHATS_NEW_ARTICLES = 'N8N_READ_WHATS_NEW_ARTICLES';
|
export const LOCAL_STORAGE_READ_WHATS_NEW_ARTICLES = 'N8N_READ_WHATS_NEW_ARTICLES';
|
||||||
export const LOCAL_STORAGE_DISMISSED_WHATS_NEW_CALLOUT = 'N8N_DISMISSED_WHATS_NEW_CALLOUT';
|
export const LOCAL_STORAGE_DISMISSED_WHATS_NEW_CALLOUT = 'N8N_DISMISSED_WHATS_NEW_CALLOUT';
|
||||||
export const LOCAL_STORAGE_NDV_PANEL_WIDTH = 'N8N_NDV_PANEL_WIDTH';
|
export const LOCAL_STORAGE_NDV_PANEL_WIDTH = 'N8N_NDV_PANEL_WIDTH';
|
||||||
@@ -756,6 +754,12 @@ export const CANVAS_ZOOMED_VIEW_EXPERIMENT = {
|
|||||||
variant: 'variant',
|
variant: 'variant',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const NDV_IN_FOCUS_PANEL_EXPERIMENT = {
|
||||||
|
name: 'ndv_in_focus_panel',
|
||||||
|
control: 'control',
|
||||||
|
variant: 'variant',
|
||||||
|
};
|
||||||
|
|
||||||
export const NDV_UI_OVERHAUL_EXPERIMENT = {
|
export const NDV_UI_OVERHAUL_EXPERIMENT = {
|
||||||
name: '029_ndv_ui_overhaul',
|
name: '029_ndv_ui_overhaul',
|
||||||
control: 'control',
|
control: 'control',
|
||||||
|
|||||||
@@ -147,6 +147,10 @@ export const useFocusPanelStore = defineStore(STORES.FOCUS_PANEL, () => {
|
|||||||
_setOptions({ isActive: false });
|
_setOptions({ isActive: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unsetParameters() {
|
||||||
|
_setOptions({ parameters: [] });
|
||||||
|
}
|
||||||
|
|
||||||
function toggleFocusPanel() {
|
function toggleFocusPanel() {
|
||||||
_setOptions({ isActive: !focusPanelActive.value });
|
_setOptions({ isActive: !focusPanelActive.value });
|
||||||
}
|
}
|
||||||
@@ -193,5 +197,6 @@ export const useFocusPanelStore = defineStore(STORES.FOCUS_PANEL, () => {
|
|||||||
toggleFocusPanel,
|
toggleFocusPanel,
|
||||||
onNewWorkflowSave,
|
onNewWorkflowSave,
|
||||||
updateWidth,
|
updateWidth,
|
||||||
|
unsetParameters,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,10 +10,7 @@ import * as eventsApi from '@n8n/rest-api-client/api/events';
|
|||||||
import * as settingsApi from '@n8n/rest-api-client/api/settings';
|
import * as settingsApi from '@n8n/rest-api-client/api/settings';
|
||||||
import * as moduleSettingsApi from '@n8n/rest-api-client/api/module-settings';
|
import * as moduleSettingsApi from '@n8n/rest-api-client/api/module-settings';
|
||||||
import { testHealthEndpoint } from '@n8n/rest-api-client/api/templates';
|
import { testHealthEndpoint } from '@n8n/rest-api-client/api/templates';
|
||||||
import {
|
import { INSECURE_CONNECTION_WARNING } from '@/constants';
|
||||||
INSECURE_CONNECTION_WARNING,
|
|
||||||
LOCAL_STORAGE_EXPERIMENTAL_DOCKED_NODE_SETTINGS,
|
|
||||||
} from '@/constants';
|
|
||||||
import { STORES } from '@n8n/stores';
|
import { STORES } from '@n8n/stores';
|
||||||
import { UserManagementAuthenticationMethod } from '@/Interface';
|
import { UserManagementAuthenticationMethod } from '@/Interface';
|
||||||
import type { IDataObject, WorkflowSettings } from 'n8n-workflow';
|
import type { IDataObject, WorkflowSettings } from 'n8n-workflow';
|
||||||
@@ -315,15 +312,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
|||||||
moduleSettings.value = fetched;
|
moduleSettings.value = fetched;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* (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,
|
||||||
@@ -380,7 +368,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
|||||||
isAskAiEnabled,
|
isAskAiEnabled,
|
||||||
isAiCreditsEnabled,
|
isAiCreditsEnabled,
|
||||||
aiCreditsQuota,
|
aiCreditsQuota,
|
||||||
experimental__dockedNodeSettingsEnabled,
|
|
||||||
partialExecutionVersion,
|
partialExecutionVersion,
|
||||||
reset,
|
reset,
|
||||||
getTimezones,
|
getTimezones,
|
||||||
|
|||||||
@@ -2160,6 +2160,7 @@ onBeforeUnmount(() => {
|
|||||||
v-if="!isCanvasReadOnly"
|
v-if="!isCanvasReadOnly"
|
||||||
:create-node-active="nodeCreatorStore.isCreateNodeActive"
|
:create-node-active="nodeCreatorStore.isCreateNodeActive"
|
||||||
:node-view-scale="viewportTransform.zoom"
|
:node-view-scale="viewportTransform.zoom"
|
||||||
|
:focus-panel-active="focusPanelStore.focusPanelActive"
|
||||||
@toggle-node-creator="onToggleNodeCreator"
|
@toggle-node-creator="onToggleNodeCreator"
|
||||||
@add-nodes="onAddNodesAndConnections"
|
@add-nodes="onAddNodesAndConnections"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user