mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(editor): NDV in focus panel experiment feedback (no-changelog) (#19304)
This commit is contained in:
@@ -86,6 +86,7 @@ import IconLucideEarth from '~icons/lucide/earth';
|
||||
import IconLucideEllipsis from '~icons/lucide/ellipsis';
|
||||
import IconLucideEllipsisVertical from '~icons/lucide/ellipsis-vertical';
|
||||
import IconLucideEqual from '~icons/lucide/equal';
|
||||
import IconLucideExpand from '~icons/lucide/expand';
|
||||
import IconLucideExternalLink from '~icons/lucide/external-link';
|
||||
import IconLucideEye from '~icons/lucide/eye';
|
||||
import IconLucideEyeOff from '~icons/lucide/eye-off';
|
||||
@@ -501,6 +502,7 @@ export const updatedIconSet = {
|
||||
ellipsis: IconLucideEllipsis,
|
||||
'ellipsis-vertical': IconLucideEllipsisVertical,
|
||||
equal: IconLucideEqual,
|
||||
expand: IconLucideExpand,
|
||||
'external-link': IconLucideExternalLink,
|
||||
eye: IconLucideEye,
|
||||
'eye-off': IconLucideEyeOff,
|
||||
|
||||
@@ -1533,6 +1533,8 @@
|
||||
"nodeView.focusPanel.noExecutionData": "Execute previous node for autocomplete",
|
||||
"nodeView.focusPanel.noParameters.title": "Show a node parameter here, to iterate easily",
|
||||
"nodeView.focusPanel.noParameters.subtitle": "For example, keep your prompt always visible so you can run the workflow while tweaking it",
|
||||
"nodeView.focusPanel.v2.noParameters.title": "Select a node to edit it here",
|
||||
"nodeView.focusPanel.v2.noParameters.subtitle": "Or show single node parameter you’d like to iterate on by clicking this button next to it:",
|
||||
"nodeView.focusPanel.missingParameter": "This parameter is no longer visible on the node. A related parameter was likely changed, removing this one.",
|
||||
"nodeView.itLooksLikeYouHaveBeenEditingSomething": "It looks like you made some edits. If you leave before saving, your changes will be lost.",
|
||||
"nodeView.loadingTemplate": "Loading template",
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import type { IExecutionResponse, INodeUi, IWorkflowDb } from '@/Interface';
|
||||
import { CanvasNodeRenderType } from '@/types';
|
||||
import type { FrontendSettings } from '@n8n/api-types';
|
||||
import type { ExpressionLocalResolveContext } from '@/types/expressions';
|
||||
|
||||
export const mockNode = ({
|
||||
id = uuid(),
|
||||
@@ -69,22 +70,7 @@ export const mockNodeTypeDescription = ({
|
||||
description,
|
||||
webhooks,
|
||||
eventTriggerDescription,
|
||||
}: {
|
||||
name?: INodeTypeDescription['name'];
|
||||
displayName?: INodeTypeDescription['displayName'];
|
||||
icon?: INodeTypeDescription['icon'];
|
||||
version?: INodeTypeDescription['version'];
|
||||
credentials?: INodeTypeDescription['credentials'];
|
||||
inputs?: INodeTypeDescription['inputs'];
|
||||
outputs?: INodeTypeDescription['outputs'];
|
||||
codex?: INodeTypeDescription['codex'];
|
||||
properties?: INodeTypeDescription['properties'];
|
||||
group?: INodeTypeDescription['group'];
|
||||
hidden?: INodeTypeDescription['hidden'];
|
||||
description?: INodeTypeDescription['description'];
|
||||
webhooks?: INodeTypeDescription['webhooks'];
|
||||
eventTriggerDescription?: INodeTypeDescription['eventTriggerDescription'];
|
||||
} = {}) =>
|
||||
}: Partial<INodeTypeDescription> = {}) =>
|
||||
mock<INodeTypeDescription>({
|
||||
name,
|
||||
icon,
|
||||
@@ -294,3 +280,21 @@ export function createTestWorkflowExecutionResponse(
|
||||
...data,
|
||||
};
|
||||
}
|
||||
|
||||
export function createTestExpressionLocalResolveContext(
|
||||
data: Partial<ExpressionLocalResolveContext> = {},
|
||||
): ExpressionLocalResolveContext {
|
||||
const workflow = data.workflow ?? createTestWorkflowObject();
|
||||
|
||||
return {
|
||||
localResolve: true,
|
||||
workflow,
|
||||
nodeName: 'n0',
|
||||
inputNode: { name: 'n1', runIndex: 0, branchIndex: 0 },
|
||||
envVars: {},
|
||||
additionalKeys: {},
|
||||
connections: workflow.connectionsBySourceNode,
|
||||
execution: null,
|
||||
...data,
|
||||
};
|
||||
}
|
||||
|
||||
134
packages/frontend/editor-ui/src/components/FocusPanel.test.ts
Normal file
134
packages/frontend/editor-ui/src/components/FocusPanel.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { createCanvasGraphNode } from '@/__tests__/data';
|
||||
import { createTestNode, createTestWorkflow, mockNodeTypeDescription } from '@/__tests__/mocks';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { SET_NODE_TYPE } from '@/constants';
|
||||
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { useVueFlow } from '@vue-flow/core';
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { reactive } from 'vue';
|
||||
import { useExperimentalNdvStore } from './canvas/experimental/experimentalNdv.store';
|
||||
import FocusPanel from './FocusPanel.vue';
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({}),
|
||||
useRoute: () => reactive({}),
|
||||
RouterLink: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('FocusPanel', () => {
|
||||
const renderComponent = createComponentRenderer(FocusPanel, {
|
||||
props: {
|
||||
isCanvasReadOnly: false,
|
||||
},
|
||||
});
|
||||
|
||||
const parameter0: INodeProperties = {
|
||||
displayName: 'P0',
|
||||
name: 'p0',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '',
|
||||
validateType: 'string',
|
||||
};
|
||||
const parameter1: INodeProperties = {
|
||||
displayName: 'P1',
|
||||
name: 'p1',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '',
|
||||
validateType: 'string',
|
||||
};
|
||||
|
||||
let experimentalNdvStore: ReturnType<typeof mockedStore<typeof useExperimentalNdvStore>>;
|
||||
let focusPanelStore: ReturnType<typeof useFocusPanelStore>;
|
||||
let nodeTypesStore: ReturnType<typeof useNodeTypesStore>;
|
||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
const pinia = setActivePinia(createTestingPinia({ stubActions: false }));
|
||||
|
||||
localStorage.clear();
|
||||
|
||||
nodeTypesStore = useNodeTypesStore(pinia);
|
||||
nodeTypesStore.setNodeTypes([
|
||||
mockNodeTypeDescription({
|
||||
name: SET_NODE_TYPE,
|
||||
properties: [parameter0, parameter1],
|
||||
}),
|
||||
]);
|
||||
workflowsStore = useWorkflowsStore(pinia);
|
||||
workflowsStore.setWorkflow(createTestWorkflow({ id: 'w0' }));
|
||||
workflowsStore.setNodes([
|
||||
createTestNode({ id: 'n0', name: 'N0', parameters: { p0: 'v0' }, type: SET_NODE_TYPE }),
|
||||
]);
|
||||
focusPanelStore = useFocusPanelStore(pinia);
|
||||
focusPanelStore.toggleFocusPanel();
|
||||
});
|
||||
|
||||
describe('when experimental NDV is enabled', () => {
|
||||
beforeEach(() => {
|
||||
experimentalNdvStore = mockedStore(useExperimentalNdvStore);
|
||||
experimentalNdvStore.isNdvInFocusPanelEnabled = true;
|
||||
});
|
||||
|
||||
it('should render empty state when neither a node nor a parameter is selected', async () => {
|
||||
const rendered = renderComponent({});
|
||||
|
||||
expect(await rendered.findByText('Select a node to edit it here'));
|
||||
});
|
||||
|
||||
it('should render the parameter focus input when a parameter is selected', async () => {
|
||||
const rendered = renderComponent({});
|
||||
|
||||
focusPanelStore.openWithFocusedNodeParameter({
|
||||
nodeId: 'n0',
|
||||
parameter: parameter0,
|
||||
parameterPath: 'parameters.p0',
|
||||
});
|
||||
|
||||
expect(await rendered.findByTestId('focus-parameter')).toBeInTheDocument();
|
||||
expect(rendered.getByText('N0')).toBeInTheDocument(); // title in header
|
||||
expect(rendered.getByText('P0')).toBeInTheDocument(); // title in header
|
||||
expect(rendered.getByDisplayValue('v0')).toBeInTheDocument(); // current value of the parameter
|
||||
});
|
||||
|
||||
it('should render node parameters when a node is selected on canvas', async () => {
|
||||
const graphNode = createCanvasGraphNode({ id: 'n0' });
|
||||
const vueFlow = useVueFlow('w0');
|
||||
const rendered = renderComponent({});
|
||||
|
||||
vueFlow.addNodes([graphNode]);
|
||||
vueFlow.addSelectedNodes([graphNode]);
|
||||
|
||||
expect(await rendered.findByTestId('node-parameters')).toBeInTheDocument();
|
||||
expect(rendered.getByText('N0')).toBeInTheDocument(); // title in header
|
||||
expect(rendered.getByText('P0')).toBeInTheDocument(); // parameter 0
|
||||
expect(rendered.getByText('P1')).toBeInTheDocument(); // parameter 1
|
||||
});
|
||||
|
||||
it('should render the parameters when a node is selected on canvas and a parameter is selected', async () => {
|
||||
const graphNode = createCanvasGraphNode({ id: 'n0' });
|
||||
const vueFlow = useVueFlow('w0');
|
||||
const rendered = renderComponent({});
|
||||
|
||||
vueFlow.addNodes([graphNode]);
|
||||
vueFlow.addSelectedNodes([graphNode]);
|
||||
|
||||
focusPanelStore.openWithFocusedNodeParameter({
|
||||
nodeId: 'n0',
|
||||
parameter: parameter0,
|
||||
parameterPath: 'parameters.p0',
|
||||
});
|
||||
|
||||
expect(await rendered.findByTestId('focus-parameter')).toBeInTheDocument();
|
||||
expect(rendered.getByText('N0')).toBeInTheDocument(); // title in header
|
||||
expect(rendered.getByText('P0')).toBeInTheDocument(); // title in header
|
||||
expect(rendered.getByDisplayValue('v0')).toBeInTheDocument(); // current value of the parameter
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -38,6 +38,7 @@ import { useVueFlow } from '@vue-flow/core';
|
||||
import ExperimentalFocusPanelHeader from '@/components/canvas/experimental/components/ExperimentalFocusPanelHeader.vue';
|
||||
import { useTelemetryContext } from '@/composables/useTelemetryContext';
|
||||
import { type ContextMenuAction } from '@/composables/useContextMenuItems';
|
||||
import { type CanvasNode, CanvasNodeRenderType } from '@/types';
|
||||
|
||||
defineOptions({ name: 'FocusPanel' });
|
||||
|
||||
@@ -112,9 +113,11 @@ const node = computed<INodeUi | undefined>(() => {
|
||||
return resolvedParameter.value?.node;
|
||||
}
|
||||
|
||||
const selected = vueFlow.getSelectedNodes.value[0]?.id;
|
||||
const selected: CanvasNode | undefined = vueFlow.getSelectedNodes.value[0];
|
||||
|
||||
return selected ? workflowsStore.allNodes.find((n) => n.id === selected) : undefined;
|
||||
return selected?.data?.render.type === CanvasNodeRenderType.Default
|
||||
? workflowsStore.allNodes.find((n) => n.id === selected.id)
|
||||
: undefined;
|
||||
});
|
||||
const multipleNodesSelected = computed(() => vueFlow.getSelectedNodes.value.length > 1);
|
||||
|
||||
@@ -209,6 +212,18 @@ const isNodeExecuting = computed(() => workflowsStore.isNodeExecuting(node.value
|
||||
|
||||
const selectedNodeIds = computed(() => vueFlow.getSelectedNodes.value.map((n) => n.id));
|
||||
|
||||
const emptyTitle = computed(() =>
|
||||
experimentalNdvStore.isNdvInFocusPanelEnabled
|
||||
? locale.baseText('nodeView.focusPanel.v2.noParameters.title')
|
||||
: locale.baseText('nodeView.focusPanel.noParameters.title'),
|
||||
);
|
||||
|
||||
const emptySubtitle = computed(() =>
|
||||
experimentalNdvStore.isNdvInFocusPanelEnabled
|
||||
? locale.baseText('nodeView.focusPanel.v2.noParameters.subtitle')
|
||||
: locale.baseText('nodeView.focusPanel.noParameters.subtitle'),
|
||||
);
|
||||
|
||||
const { resolvedExpression } = useResolvedExpression({
|
||||
expression,
|
||||
additionalData: resolvedAdditionalExpressionData,
|
||||
@@ -407,7 +422,16 @@ function onOpenNdv() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="focusPanelActive" ref="wrapper" :class="$style.wrapper" @keydown.stop>
|
||||
<div
|
||||
v-if="focusPanelActive"
|
||||
ref="wrapper"
|
||||
data-test-id="focus-panel"
|
||||
:class="[
|
||||
$style.wrapper,
|
||||
{ [$style.isNdvInFocusPanelEnabled]: experimentalNdvStore.isNdvInFocusPanelEnabled },
|
||||
]"
|
||||
@keydown.stop
|
||||
>
|
||||
<N8nResizeWrapper
|
||||
:width="focusPanelWidth"
|
||||
:supported-directions="['left']"
|
||||
@@ -427,7 +451,7 @@ function onOpenNdv() {
|
||||
@open-ndv="onOpenNdv"
|
||||
@clear-parameter="closeFocusPanel"
|
||||
/>
|
||||
<div v-if="resolvedParameter" :class="$style.content">
|
||||
<div v-if="resolvedParameter" :class="$style.content" data-test-id="focus-parameter">
|
||||
<div v-if="!experimentalNdvStore.isNdvInFocusPanelEnabled" :class="$style.tabHeader">
|
||||
<div :class="$style.tabHeaderText">
|
||||
<N8nText color="text-dark" size="small">
|
||||
@@ -576,37 +600,38 @@ function onOpenNdv() {
|
||||
v-else-if="node && experimentalNdvStore.isNdvInFocusPanelEnabled"
|
||||
:node="node"
|
||||
:node-ids="selectedNodeIds"
|
||||
:is-read-only="isReadOnly"
|
||||
@open-ndv="onOpenNdv"
|
||||
@context-menu-action="(action, nodeIds) => emit('contextMenuAction', action, nodeIds)"
|
||||
/>
|
||||
<div v-else :class="[$style.content, $style.emptyContent]">
|
||||
<div :class="$style.emptyText">
|
||||
<div :class="$style.focusParameterWrapper">
|
||||
<div :class="$style.iconWrapper">
|
||||
<N8nIcon :class="$style.forceHover" icon="panel-right" size="medium" />
|
||||
<N8nIcon
|
||||
:class="$style.pointerIcon"
|
||||
icon="mouse-pointer"
|
||||
color="text-dark"
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
<N8nIcon icon="ellipsis-vertical" size="small" color="text-base" />
|
||||
<N8nRadioButtons
|
||||
size="small"
|
||||
:model-value="'expression'"
|
||||
:disabled="true"
|
||||
:options="[
|
||||
{ label: locale.baseText('parameterInput.fixed'), value: 'fixed' },
|
||||
{ label: locale.baseText('parameterInput.expression'), value: 'expression' },
|
||||
]"
|
||||
<div :class="$style.focusParameterWrapper">
|
||||
<div :class="$style.iconWrapper">
|
||||
<N8nIcon :class="$style.forceHover" icon="panel-right" size="medium" />
|
||||
<N8nIcon
|
||||
:class="$style.pointerIcon"
|
||||
icon="mouse-pointer"
|
||||
color="text-dark"
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
<N8nIcon icon="ellipsis-vertical" size="small" color="text-base" />
|
||||
<N8nRadioButtons
|
||||
size="small"
|
||||
:model-value="'expression'"
|
||||
:disabled="true"
|
||||
:options="[
|
||||
{ label: locale.baseText('parameterInput.fixed'), value: 'fixed' },
|
||||
{ label: locale.baseText('parameterInput.expression'), value: 'expression' },
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.emptyText">
|
||||
<N8nText color="text-base" size="medium" :bold="true">
|
||||
{{ locale.baseText('nodeView.focusPanel.noParameters.title') }}
|
||||
{{ emptyTitle }}
|
||||
</N8nText>
|
||||
<N8nText color="text-base" size="small">
|
||||
{{ locale.baseText('nodeView.focusPanel.noParameters.subtitle') }}
|
||||
{{ emptySubtitle }}
|
||||
</N8nText>
|
||||
</div>
|
||||
</div>
|
||||
@@ -645,35 +670,39 @@ function onOpenNdv() {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.isNdvInFocusPanelEnabled & {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
margin: 0 var(--spacing-xl);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
.focusParameterWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-2xs);
|
||||
margin-bottom: var(--spacing-m);
|
||||
.focusParameterWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-2xs);
|
||||
margin-block: var(--spacing-m);
|
||||
|
||||
.iconWrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.iconWrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.pointerIcon {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translate(-20%, -30%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.pointerIcon {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translate(-20%, -30%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:global([class*='_disabled_']) {
|
||||
cursor: default !important;
|
||||
}
|
||||
:global([class*='_disabled_']) {
|
||||
cursor: default !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,9 @@ defineExpose({
|
||||
background-color: transparent !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
|
||||
/* Override break-all set for el-popper */
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
|
||||
@@ -824,7 +824,7 @@ function handleSelectAction(params: INodeParameters) {
|
||||
gap: var(--spacing-4xs);
|
||||
margin-top: var(--spacing-xl);
|
||||
|
||||
font-size: var(--font-size-3xs);
|
||||
font-size: var(--font-size-2xs);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
@@ -10,11 +10,17 @@ import type { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import { createMockEnterpriseSettings } from '@/__tests__/mocks';
|
||||
import {
|
||||
createTestExpressionLocalResolveContext,
|
||||
createMockEnterpriseSettings,
|
||||
createTestNode,
|
||||
createTestWorkflowObject,
|
||||
} from '@/__tests__/mocks';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import type { INodeParameterResourceLocator } from 'n8n-workflow';
|
||||
import { NodeConnectionTypes, type INodeParameterResourceLocator } from 'n8n-workflow';
|
||||
import type { IWorkflowDb, WorkflowListResource } from '@/Interface';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { ExpressionLocalResolveContextSymbol } from '@/constants';
|
||||
|
||||
function getNdvStateMock(): Partial<ReturnType<typeof useNDVStore>> {
|
||||
return {
|
||||
@@ -34,6 +40,8 @@ function getNdvStateMock(): Partial<ReturnType<typeof useNDVStore>> {
|
||||
expressionOutputItemIndex: 0,
|
||||
isTableHoverOnboarded: false,
|
||||
setHighlightDraggables: vi.fn(),
|
||||
setNDVPanelDataIsEmpty: vi.fn(),
|
||||
setNDVBranchIndex: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,6 +119,8 @@ describe('ParameterInput.vue', () => {
|
||||
expressionOutputItemIndex: 0,
|
||||
isTableHoverOnboarded: false,
|
||||
setHighlightDraggables: vi.fn(),
|
||||
setNDVPanelDataIsEmpty: vi.fn(),
|
||||
setNDVBranchIndex: vi.fn(),
|
||||
};
|
||||
mockNodeTypesState = {
|
||||
allNodeTypes: [],
|
||||
@@ -649,4 +659,59 @@ describe('ParameterInput.vue', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('data mapper', () => {
|
||||
const workflow = createTestWorkflowObject({
|
||||
nodes: [createTestNode({ name: 'n0' }), createTestNode({ name: 'n1' })],
|
||||
connections: {
|
||||
n1: {
|
||||
[NodeConnectionTypes.Main]: [[{ node: 'n0', index: 0, type: NodeConnectionTypes.Main }]],
|
||||
},
|
||||
},
|
||||
});
|
||||
const ctx = createTestExpressionLocalResolveContext({
|
||||
workflow,
|
||||
nodeName: 'n0',
|
||||
inputNode: { name: 'n1', runIndex: 0, branchIndex: 0 },
|
||||
});
|
||||
|
||||
it('should render mapper', async () => {
|
||||
const rendered = renderComponent({
|
||||
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
|
||||
props: {
|
||||
path: 'name',
|
||||
parameter: { displayName: 'Name', name: 'name', type: 'string' },
|
||||
modelValue: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(rendered.queryByTestId('ndv-input-panel')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render mapper if given node property is a node setting', async () => {
|
||||
const rendered = renderComponent({
|
||||
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
|
||||
props: {
|
||||
path: 'name',
|
||||
parameter: { displayName: 'Name', name: 'name', type: 'string', isNodeSetting: true },
|
||||
modelValue: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(rendered.queryByTestId('ndv-input-panel')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render mapper if given node property has datetime type', async () => {
|
||||
const rendered = renderComponent({
|
||||
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
|
||||
props: {
|
||||
path: 'name',
|
||||
parameter: { displayName: 'Name', name: 'name', type: 'dateTime' },
|
||||
modelValue: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(rendered.queryByTestId('ndv-input-panel')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,7 +81,13 @@ import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import { onClickOutside, useElementSize } from '@vueuse/core';
|
||||
import { captureMessage } from '@sentry/vue';
|
||||
import { isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
|
||||
import { hasFocusOnInput, isBlurrableEl, isFocusableEl, isSelectableEl } from '@/utils/typesUtils';
|
||||
import {
|
||||
hasFocusOnInput,
|
||||
isBlurrableEl,
|
||||
isEmpty,
|
||||
isFocusableEl,
|
||||
isSelectableEl,
|
||||
} from '@/utils/typesUtils';
|
||||
import { completeExpressionSyntax, shouldConvertToExpression } from '@/utils/expressions';
|
||||
import CssEditor from './CssEditor/CssEditor.vue';
|
||||
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
||||
@@ -625,6 +631,14 @@ const shouldCaptureForPosthog = computed(() => node.value?.type === AI_TRANSFORM
|
||||
|
||||
const mapperElRef = computed(() => mapperRef.value?.contentRef);
|
||||
|
||||
const isMapperAvailable = computed(
|
||||
() =>
|
||||
!props.parameter.isNodeSetting &&
|
||||
(isModelValueExpression.value ||
|
||||
props.forceShowExpression ||
|
||||
(isEmpty(props.modelValue) && props.parameter.type !== 'dateTime')),
|
||||
);
|
||||
|
||||
function isRemoteParameterOption(option: INodePropertyOptions) {
|
||||
return remoteParameterOptionsKeys.value.includes(option.name);
|
||||
}
|
||||
@@ -811,10 +825,7 @@ async function setFocus() {
|
||||
}
|
||||
|
||||
isFocused.value = true;
|
||||
|
||||
if (isModelValueExpression.value || props.forceShowExpression || props.modelValue === '') {
|
||||
isMapperShown.value = true;
|
||||
}
|
||||
isMapperShown.value = isMapperAvailable.value;
|
||||
}
|
||||
|
||||
emit('focus');
|
||||
@@ -1260,7 +1271,7 @@ onClickOutside(mapperElRef, onClickOutsideMapper);
|
||||
/>
|
||||
|
||||
<ExperimentalEmbeddedNdvMapper
|
||||
v-if="node && expressionLocalResolveCtx?.inputNode"
|
||||
v-if="isMapperAvailable && node && expressionLocalResolveCtx?.inputNode"
|
||||
ref="mapperRef"
|
||||
:workflow="expressionLocalResolveCtx?.workflow"
|
||||
:node="node"
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
updateFromAIOverrideValues,
|
||||
} from '../utils/fromAIOverrideUtils';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { inject } from 'vue';
|
||||
import { ExpressionLocalResolveContextSymbol } from '@/constants';
|
||||
|
||||
type Props = {
|
||||
parameter: INodeProperties;
|
||||
@@ -72,7 +74,16 @@ const wrapperHovered = ref(false);
|
||||
const ndvStore = useNDVStore();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const activeNode = computed(() => ndvStore.activeNode);
|
||||
const expressionLocalResolveCtx = inject(ExpressionLocalResolveContextSymbol, undefined);
|
||||
const activeNode = computed(() => {
|
||||
const ctx = expressionLocalResolveCtx?.value;
|
||||
|
||||
if (ctx) {
|
||||
return ctx.workflow.getNode(ctx.nodeName);
|
||||
}
|
||||
|
||||
return ndvStore.activeNode;
|
||||
});
|
||||
const fromAIOverride = ref<FromAIOverride | null>(makeOverrideValue(props, activeNode.value));
|
||||
|
||||
const canBeContentOverride = computed(() => {
|
||||
|
||||
@@ -142,6 +142,7 @@ type Props = {
|
||||
disableEdit?: boolean;
|
||||
disablePin?: boolean;
|
||||
compact?: boolean;
|
||||
showActionsOnHover?: boolean;
|
||||
tableHeaderBgColor?: 'base' | 'light';
|
||||
disableHoverHighlight?: boolean;
|
||||
disableSettingsHint?: boolean;
|
||||
@@ -168,6 +169,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
disableHoverHighlight: false,
|
||||
disableSettingsHint: false,
|
||||
compact: false,
|
||||
showActionsOnHover: false,
|
||||
tableHeaderBgColor: 'base',
|
||||
workflowExecution: undefined,
|
||||
disableAiContent: false,
|
||||
@@ -177,6 +179,7 @@ defineSlots<{
|
||||
content: {};
|
||||
'callout-message': {};
|
||||
header: {};
|
||||
'header-end': (props: InstanceType<typeof RunDataItemCount>['$props']) => unknown;
|
||||
'input-select': {};
|
||||
'before-data': {};
|
||||
'run-info': {};
|
||||
@@ -1373,7 +1376,11 @@ defineExpose({ enterEditMode });
|
||||
:class="[
|
||||
'run-data',
|
||||
$style.container,
|
||||
{ [$style['ndv-v2']]: isNDVV2, [$style.compact]: compact },
|
||||
{
|
||||
[$style['ndv-v2']]: isNDVV2,
|
||||
[$style.compact]: compact,
|
||||
[$style.showActionsOnHover]: showActionsOnHover,
|
||||
},
|
||||
]"
|
||||
@mouseover="activatePane"
|
||||
>
|
||||
@@ -1506,7 +1513,7 @@ defineExpose({ enterEditMode });
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RunDataItemCount v-if="props.compact" v-bind="itemsCountProps" />
|
||||
<slot name="header-end" v-bind="itemsCountProps" />
|
||||
</div>
|
||||
|
||||
<div v-show="!binaryDataDisplayVisible">
|
||||
@@ -2128,6 +2135,9 @@ defineExpose({ enterEditMode });
|
||||
.compact & {
|
||||
/* let title text alone decide the height */
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.showActionsOnHover & {
|
||||
visibility: hidden;
|
||||
|
||||
:global(.el-input__prefix) {
|
||||
@@ -2135,7 +2145,7 @@ defineExpose({ enterEditMode });
|
||||
}
|
||||
}
|
||||
|
||||
.compact:hover & {
|
||||
.showActionsOnHover:hover & {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ const emit = defineEmits<{
|
||||
<div class="schema-header-wrapper">
|
||||
<div class="schema-header" data-test-id="run-data-schema-header">
|
||||
<div class="toggle" @click.capture.stop="emit('click:toggle')">
|
||||
<N8nIcon icon="chevron-down" :class="{ 'collapse-icon': true, collapsed }" />
|
||||
<N8nIcon size="medium" icon="chevron-down" :class="{ 'collapse-icon': true, collapsed }" />
|
||||
</div>
|
||||
|
||||
<NodeIcon
|
||||
@@ -75,11 +75,13 @@ const emit = defineEmits<{
|
||||
cursor: pointer;
|
||||
}
|
||||
.toggle {
|
||||
width: 30px;
|
||||
padding-left: var(--spacing-5xs);
|
||||
padding-right: var(--spacing-3xs);
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
.collapse-icon {
|
||||
transition: transform 0.2s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
|
||||
@@ -158,7 +158,8 @@ exports[`InputPanel > should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
|
||||
|
||||
</div>
|
||||
<div
|
||||
data-v-2e5cd75c=""
|
||||
|
||||
@@ -41,6 +41,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
||||
class="collapse-icon"
|
||||
data-v-882a318e=""
|
||||
icon="chevron-down"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
@@ -285,6 +286,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
||||
class="collapse-icon collapsed"
|
||||
data-v-882a318e=""
|
||||
icon="chevron-down"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
@@ -327,6 +329,7 @@ exports[`VirtualSchema.vue > renders previous nodes schema for AI tools 1`] = `
|
||||
class="collapse-icon"
|
||||
data-v-882a318e=""
|
||||
icon="chevron-down"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
@@ -411,6 +414,7 @@ exports[`VirtualSchema.vue > renders schema for empty objects and arrays 1`] = `
|
||||
class="collapse-icon"
|
||||
data-v-882a318e=""
|
||||
icon="chevron-down"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
@@ -892,6 +896,7 @@ exports[`VirtualSchema.vue > renders schema for empty objects and arrays 1`] = `
|
||||
class="collapse-icon collapsed"
|
||||
data-v-882a318e=""
|
||||
icon="chevron-down"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
@@ -1300,6 +1305,7 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = `
|
||||
class="collapse-icon"
|
||||
data-v-882a318e=""
|
||||
icon="chevron-down"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
@@ -1703,6 +1709,7 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = `
|
||||
class="collapse-icon collapsed"
|
||||
data-v-882a318e=""
|
||||
icon="chevron-down"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
@@ -1764,6 +1771,7 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = `
|
||||
class="collapse-icon collapsed"
|
||||
data-v-882a318e=""
|
||||
icon="chevron-down"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
@@ -1833,6 +1841,7 @@ exports[`VirtualSchema.vue > renders variables and context section 1`] = `
|
||||
class="collapse-icon collapsed"
|
||||
data-v-882a318e=""
|
||||
icon="chevron-down"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
@@ -1900,6 +1909,7 @@ exports[`VirtualSchema.vue > renders variables and context section 1`] = `
|
||||
class="collapse-icon"
|
||||
data-v-882a318e=""
|
||||
icon="chevron-down"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
@@ -2655,6 +2665,7 @@ exports[`VirtualSchema.vue > should expand all nodes when searching 1`] = `
|
||||
class="collapse-icon"
|
||||
data-v-882a318e=""
|
||||
icon="chevron-down"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
@@ -2793,6 +2804,7 @@ exports[`VirtualSchema.vue > should expand all nodes when searching 1`] = `
|
||||
class="collapse-icon"
|
||||
data-v-882a318e=""
|
||||
icon="chevron-down"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -123,6 +123,7 @@ const props = withDefaults(
|
||||
executing?: boolean;
|
||||
keyBindings?: boolean;
|
||||
loading?: boolean;
|
||||
suppressInteraction?: boolean;
|
||||
}>(),
|
||||
{
|
||||
id: 'canvas',
|
||||
@@ -134,6 +135,7 @@ const props = withDefaults(
|
||||
executing: false,
|
||||
keyBindings: true,
|
||||
loading: false,
|
||||
suppressInteraction: false,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -483,6 +485,7 @@ function onFocusNode(id: string) {
|
||||
const node = vueFlow.nodeLookup.value.get(id);
|
||||
|
||||
if (node) {
|
||||
addSelectedNodes([node]);
|
||||
experimentalNdvStore.focusNode(node, {
|
||||
canvasViewport: viewport.value,
|
||||
canvasDimensions: dimensions.value,
|
||||
@@ -660,11 +663,6 @@ async function onResetZoom() {
|
||||
await onZoomTo(defaultZoom);
|
||||
}
|
||||
|
||||
function setReadonly(value: boolean) {
|
||||
setInteractive(!value);
|
||||
elementsSelectable.value = true;
|
||||
}
|
||||
|
||||
function onPaneMove({ event }: { event: unknown }) {
|
||||
// The event object is either D3ZoomEvent or WheelEvent.
|
||||
// Here I'm ignoring D3ZoomEvent because it's not necessarily followed by a moveEnd event.
|
||||
@@ -875,9 +873,16 @@ onNodesInitialized(() => {
|
||||
initialized.value = true;
|
||||
});
|
||||
|
||||
watch(() => props.readOnly, setReadonly, {
|
||||
immediate: true,
|
||||
});
|
||||
watch(
|
||||
[() => props.readOnly, () => props.suppressInteraction],
|
||||
([readOnly, suppressInteraction]) => {
|
||||
setInteractive(!readOnly && !suppressInteraction);
|
||||
elementsSelectable.value = !suppressInteraction;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
watch([nodesSelectionActive, userSelectionRect], ([isActive, rect]) =>
|
||||
emit('update:has-range-selection', isActive || (rect?.width ?? 0) > 0 || (rect?.height ?? 0) > 0),
|
||||
|
||||
@@ -25,12 +25,14 @@ const props = withDefaults(
|
||||
eventBus?: EventBus<CanvasEventBusEvents>;
|
||||
readOnly?: boolean;
|
||||
executing?: boolean;
|
||||
suppressInteraction?: boolean;
|
||||
}>(),
|
||||
{
|
||||
id: 'canvas',
|
||||
eventBus: () => createEventBus<CanvasEventBusEvents>(),
|
||||
fallbackNodes: () => [],
|
||||
showFallbackNodes: true,
|
||||
suppressInteraction: false,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -84,6 +86,7 @@ defineExpose({
|
||||
:event-bus="eventBus"
|
||||
:read-only="readOnly"
|
||||
:executing="executing"
|
||||
:suppress-interaction="suppressInteraction"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -115,6 +115,8 @@ function onFocusNode() {
|
||||
:class="classes"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
@mousedown.stop
|
||||
@click.stop
|
||||
>
|
||||
<div :class="[$style.canvasNodeToolbarItems, itemsClass]">
|
||||
<N8nTooltip
|
||||
@@ -131,7 +133,7 @@ function onFocusNode() {
|
||||
icon="node-play"
|
||||
:disabled="isExecuting || isDisabled"
|
||||
:title="i18n.baseText('node.testStep')"
|
||||
@click="executeNode"
|
||||
@click.stop="executeNode"
|
||||
/>
|
||||
</N8nTooltip>
|
||||
<N8nIconButton
|
||||
@@ -142,7 +144,7 @@ function onFocusNode() {
|
||||
size="small"
|
||||
icon="node-power"
|
||||
:title="nodeDisabledTitle"
|
||||
@click="onToggleNode"
|
||||
@click.stop="onToggleNode"
|
||||
/>
|
||||
<N8nIconButton
|
||||
v-if="isDeleteNodeVisible"
|
||||
@@ -152,7 +154,7 @@ function onFocusNode() {
|
||||
text
|
||||
icon="node-trash"
|
||||
:title="i18n.baseText('node.delete')"
|
||||
@click="onDeleteNode"
|
||||
@click.stop="onDeleteNode"
|
||||
/>
|
||||
<N8nIconButton
|
||||
v-if="isFocusNodeVisible"
|
||||
@@ -160,7 +162,7 @@ function onFocusNode() {
|
||||
size="small"
|
||||
text
|
||||
icon="crosshair"
|
||||
@click="onFocusNode"
|
||||
@click.stop="onFocusNode"
|
||||
/>
|
||||
<CanvasNodeStickyColorSelector
|
||||
v-if="isStickyNoteChangeColorVisible"
|
||||
@@ -173,7 +175,7 @@ function onFocusNode() {
|
||||
size="small"
|
||||
text
|
||||
icon="node-ellipsis"
|
||||
@click="onOpenContextMenu"
|
||||
@click.stop="onOpenContextMenu"
|
||||
/>
|
||||
</div>
|
||||
<CanvasNodeStatusIcons
|
||||
@@ -190,6 +192,7 @@ function onFocusNode() {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
cursor: default;
|
||||
|
||||
&.isExperimentalNdvActive {
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -71,8 +71,9 @@ onBeforeUnmount(() => {
|
||||
:class="$style.option"
|
||||
data-test-id="change-sticky-color"
|
||||
:title="i18n.baseText('node.changeColor')"
|
||||
@click.stop
|
||||
>
|
||||
<N8nIcon icon="palette" />
|
||||
<N8nIcon size="small" icon="palette" />
|
||||
</div>
|
||||
</template>
|
||||
<div :class="$style.content">
|
||||
|
||||
@@ -9,7 +9,7 @@ const emit = defineEmits<{ openNdv: []; toggleExpand: [] }>();
|
||||
<template>
|
||||
<div :class="$style.actions">
|
||||
<N8nIconButton
|
||||
icon="maximize-2"
|
||||
icon="expand"
|
||||
type="secondary"
|
||||
text
|
||||
size="mini"
|
||||
|
||||
@@ -3,11 +3,13 @@ import InputPanel from '@/components/InputPanel.vue';
|
||||
import { CanvasKey } from '@/constants';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { N8nPopover } from '@n8n/design-system';
|
||||
import { ElPopover } from 'element-plus';
|
||||
import { useVueFlow } from '@vue-flow/core';
|
||||
import { watchOnce } from '@vueuse/core';
|
||||
import { useCanvasStore } from '@/stores/canvas.store';
|
||||
import { onBeforeUnmount, watch } from 'vue';
|
||||
import type { Workflow } from 'n8n-workflow';
|
||||
import { computed, inject, ref, useTemplateRef } from 'vue';
|
||||
import { useElementBounding, useElementSize } from '@vueuse/core';
|
||||
|
||||
const { node, inputNodeName, visible, virtualRef } = defineProps<{
|
||||
workflow: Workflow;
|
||||
@@ -23,27 +25,52 @@ const vf = useVueFlow();
|
||||
const canvas = inject(CanvasKey, undefined);
|
||||
const isVisible = computed(() => visible && !canvas?.isPaneMoving.value);
|
||||
const isOnceVisible = ref(isVisible.value);
|
||||
const canvasStore = useCanvasStore();
|
||||
const contentElRef = computed(() => contentRef.value?.$el ?? null);
|
||||
const contentSize = useElementSize(contentElRef);
|
||||
const refBounding = useElementBounding(virtualRef);
|
||||
|
||||
watchOnce(isVisible, (value) => {
|
||||
isOnceVisible.value = isOnceVisible.value || value;
|
||||
watch(
|
||||
isVisible,
|
||||
(value) => {
|
||||
isOnceVisible.value = isOnceVisible.value || value;
|
||||
canvasStore.setSuppressInteraction(value);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
canvasStore.setSuppressInteraction(false);
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
contentRef: computed<HTMLElement>(() => contentRef.value?.$el ?? null),
|
||||
contentRef: contentElRef,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<N8nPopover
|
||||
<ElPopover
|
||||
:visible="isVisible"
|
||||
placement="left"
|
||||
placement="left-start"
|
||||
:show-arrow="false"
|
||||
:popper-class="`${$style.component} ignore-key-press-canvas`"
|
||||
:width="360"
|
||||
:offset="8"
|
||||
append-to="body"
|
||||
:popper-options="{
|
||||
modifiers: [{ name: 'flip', enabled: false }],
|
||||
modifiers: [
|
||||
{ name: 'flip', enabled: false },
|
||||
{
|
||||
// Ensures that the popover is re-positioned when the reference element is resized
|
||||
name: 'custom modifier',
|
||||
options: {
|
||||
refX: refBounding.x.value,
|
||||
refY: refBounding.y.value,
|
||||
width: contentSize.width.value,
|
||||
height: contentSize?.height.value,
|
||||
},
|
||||
},
|
||||
],
|
||||
}"
|
||||
:persistent="isOnceVisible /* works like lazy initialization */"
|
||||
virtual-triggering
|
||||
@@ -68,16 +95,19 @@ defineExpose({
|
||||
:focused-mappable-input="ndvStore.focusedMappableInput"
|
||||
node-not-run-message-variant="simple"
|
||||
/>
|
||||
</N8nPopover>
|
||||
</ElPopover>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.component {
|
||||
background-color: transparent !important;
|
||||
padding: var(--spacing-s) 0 !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
margin-top: -2px;
|
||||
|
||||
/* Override break-all set for el-popper */
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.inputPanel {
|
||||
|
||||
@@ -24,7 +24,7 @@ const emit = defineEmits<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<N8nText tag="div" size="small" bold :class="$style.component">
|
||||
<N8nText tag="header" size="small" bold :class="$style.component">
|
||||
<NodeIcon :node-type="nodeType" :size="16" />
|
||||
<div :class="$style.breadcrumbs">
|
||||
<template v-if="parameter">
|
||||
@@ -46,7 +46,7 @@ const emit = defineEmits<{
|
||||
/>
|
||||
<N8nIconButton
|
||||
v-else
|
||||
icon="maximize-2"
|
||||
icon="expand"
|
||||
size="small"
|
||||
type="tertiary"
|
||||
text
|
||||
@@ -57,6 +57,7 @@ const emit = defineEmits<{
|
||||
data-test-id="node-execute-button"
|
||||
:node-name="node.name"
|
||||
:tooltip="`Execute ${node.name}`"
|
||||
type="secondary"
|
||||
size="small"
|
||||
icon="play"
|
||||
:square="true"
|
||||
|
||||
@@ -8,7 +8,12 @@ import { computed, provide, ref, watch } from 'vue';
|
||||
import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
|
||||
const { node, nodeIds } = defineProps<{ node: INodeUi; nodeIds: string[] }>();
|
||||
const { node, nodeIds, isReadOnly } = defineProps<{
|
||||
node: INodeUi;
|
||||
nodeIds: string[];
|
||||
|
||||
isReadOnly?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
openNdv: [];
|
||||
@@ -54,19 +59,12 @@ provide(ExpressionLocalResolveContextSymbol, expressionResolveCtx);
|
||||
</li>
|
||||
</ul>
|
||||
</N8nText>
|
||||
<ExperimentalCanvasNodeSettings v-else-if="node" :key="nodeSettingsViewKey" :node-id="node.id">
|
||||
<template #actions>
|
||||
<N8nIconButton
|
||||
icon="maximize-2"
|
||||
type="secondary"
|
||||
text
|
||||
size="mini"
|
||||
icon-size="large"
|
||||
aria-label="Expand"
|
||||
@click="emit('openNdv')"
|
||||
/>
|
||||
</template>
|
||||
</ExperimentalCanvasNodeSettings>
|
||||
<ExperimentalCanvasNodeSettings
|
||||
v-else-if="node"
|
||||
:key="nodeSettingsViewKey"
|
||||
:node-id="node.id"
|
||||
:is-read-only="isReadOnly"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
|
||||
import { useTagsStore } from '@/stores/tags.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import {
|
||||
createTestExpressionLocalResolveContext,
|
||||
createTestNode,
|
||||
createTestTaskData,
|
||||
createTestWorkflow,
|
||||
@@ -1010,10 +1011,7 @@ describe(resolveParameter, () => {
|
||||
f0: '={{ $json }}',
|
||||
f1: '={{ $("n0").item.json }}',
|
||||
},
|
||||
{
|
||||
localResolve: true,
|
||||
envVars: {},
|
||||
additionalKeys: {},
|
||||
createTestExpressionLocalResolveContext({
|
||||
workflow: createTestWorkflowObject(workflowData),
|
||||
execution: createTestWorkflowExecutionResponse({
|
||||
workflowData,
|
||||
@@ -1031,7 +1029,7 @@ describe(resolveParameter, () => {
|
||||
}),
|
||||
nodeName: 'n1',
|
||||
inputNode: { name: 'n0', branchIndex: 0, runIndex: 0 },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
|
||||
@@ -10,6 +10,7 @@ import { computed, inject, ref } from 'vue';
|
||||
import { I18nT } from 'vue-i18n';
|
||||
import { PopOutWindowKey } from '@/constants';
|
||||
import { isSubNodeLog } from '../logs.utils';
|
||||
import RunDataItemCount from '@/components/RunDataItemCount.vue';
|
||||
|
||||
const { title, logEntry, paneType, collapsingTableColumnName } = defineProps<{
|
||||
title: string;
|
||||
@@ -84,6 +85,7 @@ function handleChangeDisplayMode(value: IRunDataDisplayMode) {
|
||||
:pane-type="paneType"
|
||||
:disable-run-index-selection="true"
|
||||
:compact="true"
|
||||
:show-actions-on-hover="true"
|
||||
:disable-pin="true"
|
||||
:disable-edit="true"
|
||||
:disable-hover-highlight="true"
|
||||
@@ -102,6 +104,10 @@ function handleChangeDisplayMode(value: IRunDataDisplayMode) {
|
||||
</N8nText>
|
||||
</template>
|
||||
|
||||
<template #header-end="itemCountProps">
|
||||
<RunDataItemCount v-bind="itemCountProps" />
|
||||
</template>
|
||||
|
||||
<template #no-output-data>
|
||||
<N8nText :bold="true" color="text-dark" size="large">
|
||||
{{ locale.baseText('ndv.output.noOutputData.title') }}
|
||||
|
||||
@@ -9,6 +9,8 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||
const loadingService = useLoadingService();
|
||||
|
||||
const newNodeInsertPosition = ref<XYPosition | null>(null);
|
||||
const suppressInteraction = ref(false);
|
||||
|
||||
const nodes = computed<INodeUi[]>(() => workflowStore.allNodes);
|
||||
const aiNodes = computed<INodeUi[]>(() =>
|
||||
nodes.value.filter(
|
||||
@@ -23,14 +25,20 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||
hasRangeSelection.value = value;
|
||||
}
|
||||
|
||||
function setSuppressInteraction(value: boolean) {
|
||||
suppressInteraction.value = value;
|
||||
}
|
||||
|
||||
return {
|
||||
newNodeInsertPosition,
|
||||
isLoading: loadingService.isLoading,
|
||||
aiNodes,
|
||||
hasRangeSelection: computed(() => hasRangeSelection.value),
|
||||
suppressInteraction: computed(() => suppressInteraction.value),
|
||||
startLoading: loadingService.startLoading,
|
||||
setLoadingText: loadingService.setLoadingText,
|
||||
stopLoading: loadingService.stopLoading,
|
||||
setHasRangeSelection,
|
||||
setSuppressInteraction,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -35,10 +35,15 @@
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
&:has(+ .cm-completionInfo-left) {
|
||||
/* Info box should not cast shadow on the list */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-right-radius: var(--border-radius-base);
|
||||
border-bottom-right-radius: var(--border-radius-base);
|
||||
background-color: var(--color-background-xlight);
|
||||
}
|
||||
|
||||
&:has(+ .cm-completionInfo) {
|
||||
@@ -335,6 +340,8 @@
|
||||
border-bottom-left-radius: var(--border-radius-base);
|
||||
border-left: var(--border-base);
|
||||
border-right: none;
|
||||
background-color: var(--color-infobox-background);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
&.cm-completionInfo-right {
|
||||
|
||||
@@ -2069,6 +2069,7 @@ onBeforeUnmount(() => {
|
||||
:read-only="isCanvasReadOnly"
|
||||
:executing="isWorkflowRunning"
|
||||
:key-bindings="keyBindingsEnabled"
|
||||
:suppress-interaction="canvasStore.suppressInteraction"
|
||||
@update:nodes:position="onUpdateNodesPosition"
|
||||
@update:node:position="onUpdateNodePosition"
|
||||
@update:node:activated="onSetNodeActivated"
|
||||
|
||||
Reference in New Issue
Block a user