mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(editor): Show mappings by default in sub-node NDVs when the root node isn't executed (#12642)
This commit is contained in:
parent
114ed88368
commit
fb662dd95c
|
@ -4,10 +4,15 @@ import InputPanel, { type Props } from '@/components/InputPanel.vue';
|
||||||
import { STORES } from '@/constants';
|
import { STORES } from '@/constants';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { NodeConnectionType, type IConnections, type INodeExecutionData } from 'n8n-workflow';
|
import { waitFor } from '@testing-library/vue';
|
||||||
|
import {
|
||||||
|
NodeConnectionType,
|
||||||
|
type IConnections,
|
||||||
|
type INodeExecutionData,
|
||||||
|
type IRunData,
|
||||||
|
} from 'n8n-workflow';
|
||||||
import { setActivePinia } from 'pinia';
|
import { setActivePinia } from 'pinia';
|
||||||
import { mockedStore } from '../__tests__/utils';
|
import { mockedStore } from '../__tests__/utils';
|
||||||
import { waitFor } from '@testing-library/vue';
|
|
||||||
|
|
||||||
vi.mock('vue-router', () => {
|
vi.mock('vue-router', () => {
|
||||||
return {
|
return {
|
||||||
|
@ -24,7 +29,7 @@ const nodes = [
|
||||||
createTestNode({ name: 'Tool' }),
|
createTestNode({ name: 'Tool' }),
|
||||||
];
|
];
|
||||||
|
|
||||||
const render = (props: Partial<Props> = {}, pinData?: INodeExecutionData[]) => {
|
const render = (props: Partial<Props> = {}, pinData?: INodeExecutionData[], runData?: IRunData) => {
|
||||||
const connections: IConnections = {
|
const connections: IConnections = {
|
||||||
[nodes[0].name]: {
|
[nodes[0].name]: {
|
||||||
[NodeConnectionType.Main]: [
|
[NodeConnectionType.Main]: [
|
||||||
|
@ -50,12 +55,38 @@ const render = (props: Partial<Props> = {}, pinData?: INodeExecutionData[]) => {
|
||||||
setActivePinia(pinia);
|
setActivePinia(pinia);
|
||||||
|
|
||||||
const workflow = createTestWorkflow({ nodes, connections });
|
const workflow = createTestWorkflow({ nodes, connections });
|
||||||
useWorkflowsStore().setWorkflow(workflow);
|
const workflowStore = useWorkflowsStore();
|
||||||
|
|
||||||
|
workflowStore.setWorkflow(workflow);
|
||||||
|
|
||||||
if (pinData) {
|
if (pinData) {
|
||||||
mockedStore(useWorkflowsStore).pinDataByNodeName.mockReturnValue(pinData);
|
mockedStore(useWorkflowsStore).pinDataByNodeName.mockReturnValue(pinData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (runData) {
|
||||||
|
workflowStore.setWorkflowExecutionData({
|
||||||
|
id: '',
|
||||||
|
workflowData: {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
active: false,
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
|
versionId: '',
|
||||||
|
},
|
||||||
|
finished: false,
|
||||||
|
mode: 'trigger',
|
||||||
|
status: 'success',
|
||||||
|
startedAt: new Date(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
data: {
|
||||||
|
resultData: { runData },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const workflowObject = createTestWorkflowObject({
|
const workflowObject = createTestWorkflowObject({
|
||||||
nodes,
|
nodes,
|
||||||
connections,
|
connections,
|
||||||
|
@ -83,4 +114,27 @@ describe('InputPanel', () => {
|
||||||
await waitFor(() => expect(queryByTestId('ndv-data-size-warning')).not.toBeInTheDocument());
|
await waitFor(() => expect(queryByTestId('ndv-data-size-warning')).not.toBeInTheDocument());
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("opens mapping tab by default if the node hasn't run yet", async () => {
|
||||||
|
const { findByTestId } = render({ currentNodeName: 'Tool' });
|
||||||
|
|
||||||
|
expect((await findByTestId('radio-button-mapping')).parentNode).toBeChecked();
|
||||||
|
expect((await findByTestId('radio-button-debugging')).parentNode).not.toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens debugging tab by default if the node has already run', async () => {
|
||||||
|
const { findByTestId } = render({ currentNodeName: 'Tool' }, undefined, {
|
||||||
|
Tool: [
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
source: [],
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect((await findByTestId('radio-button-mapping')).parentNode).not.toBeChecked();
|
||||||
|
expect((await findByTestId('radio-button-debugging')).parentNode).toBeChecked();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import {
|
import {
|
||||||
CRON_NODE_TYPE,
|
CRON_NODE_TYPE,
|
||||||
INTERVAL_NODE_TYPE,
|
INTERVAL_NODE_TYPE,
|
||||||
MANUAL_TRIGGER_NODE_TYPE,
|
MANUAL_TRIGGER_NODE_TYPE,
|
||||||
START_NODE_TYPE,
|
START_NODE_TYPE,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { waitingNodeTooltip } from '@/utils/executionUtils';
|
import { waitingNodeTooltip } from '@/utils/executionUtils';
|
||||||
import { uniqBy } from 'lodash-es';
|
import { uniqBy } from 'lodash-es';
|
||||||
|
import { N8nRadioButtons, N8nText, N8nTooltip } from 'n8n-design-system';
|
||||||
import type { INodeInputConfiguration, INodeOutputConfiguration, Workflow } from 'n8n-workflow';
|
import type { INodeInputConfiguration, INodeOutputConfiguration, Workflow } from 'n8n-workflow';
|
||||||
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
|
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { useNDVStore } from '../stores/ndv.store';
|
||||||
import InputNodeSelect from './InputNodeSelect.vue';
|
import InputNodeSelect from './InputNodeSelect.vue';
|
||||||
import NodeExecuteButton from './NodeExecuteButton.vue';
|
import NodeExecuteButton from './NodeExecuteButton.vue';
|
||||||
import RunData from './RunData.vue';
|
import RunData from './RunData.vue';
|
||||||
import WireMeUp from './WireMeUp.vue';
|
import WireMeUp from './WireMeUp.vue';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
|
||||||
import { N8nRadioButtons, N8nTooltip, N8nText } from 'n8n-design-system';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
|
|
||||||
type MappingMode = 'debugging' | 'mapping';
|
type MappingMode = 'debugging' | 'mapping';
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ const telemetry = useTelemetry();
|
||||||
|
|
||||||
const showDraggableHintWithDelay = ref(false);
|
const showDraggableHintWithDelay = ref(false);
|
||||||
const draggableHintShown = ref(false);
|
const draggableHintShown = ref(false);
|
||||||
const inputMode = ref<MappingMode>('debugging');
|
|
||||||
const mappedNode = ref<string | null>(null);
|
const mappedNode = ref<string | null>(null);
|
||||||
const inputModes = [
|
const inputModes = [
|
||||||
{ value: 'mapping', label: i18n.baseText('ndv.input.mapping') },
|
{ value: 'mapping', label: i18n.baseText('ndv.input.mapping') },
|
||||||
|
@ -88,6 +88,27 @@ const {
|
||||||
focusedMappableInput,
|
focusedMappableInput,
|
||||||
isMappingOnboarded: isUserOnboarded,
|
isMappingOnboarded: isUserOnboarded,
|
||||||
} = storeToRefs(ndvStore);
|
} = storeToRefs(ndvStore);
|
||||||
|
|
||||||
|
const rootNode = computed(() => {
|
||||||
|
if (!activeNode.value) return null;
|
||||||
|
|
||||||
|
return props.workflow.getChildNodes(activeNode.value.name, 'ALL').at(0) ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasRootNodeRun = computed(() => {
|
||||||
|
return !!(
|
||||||
|
rootNode.value && workflowsStore.getWorkflowExecution?.data?.resultData.runData[rootNode.value]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputMode = ref<MappingMode>(
|
||||||
|
// Show debugging mode by default only when the node has already run
|
||||||
|
activeNode.value &&
|
||||||
|
workflowsStore.getWorkflowExecution?.data?.resultData.runData[activeNode.value.name]
|
||||||
|
? 'debugging'
|
||||||
|
: 'mapping',
|
||||||
|
);
|
||||||
|
|
||||||
const isMappingMode = computed(() => isActiveNodeConfig.value && inputMode.value === 'mapping');
|
const isMappingMode = computed(() => isActiveNodeConfig.value && inputMode.value === 'mapping');
|
||||||
const showDraggableHint = computed(() => {
|
const showDraggableHint = computed(() => {
|
||||||
const toIgnore = [START_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE, CRON_NODE_TYPE, INTERVAL_NODE_TYPE];
|
const toIgnore = [START_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE, CRON_NODE_TYPE, INTERVAL_NODE_TYPE];
|
||||||
|
@ -159,12 +180,6 @@ const isExecutingPrevious = computed(() => {
|
||||||
});
|
});
|
||||||
const workflowRunning = computed(() => uiStore.isActionActive.workflowRunning);
|
const workflowRunning = computed(() => uiStore.isActionActive.workflowRunning);
|
||||||
|
|
||||||
const rootNode = computed(() => {
|
|
||||||
if (!activeNode.value) return null;
|
|
||||||
|
|
||||||
return props.workflow.getChildNodes(activeNode.value.name, 'ALL').at(0) ?? null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const rootNodesParents = computed(() => {
|
const rootNodesParents = computed(() => {
|
||||||
if (!rootNode.value) return [];
|
if (!rootNode.value) return [];
|
||||||
return props.workflow.getParentNodesByDepth(rootNode.value);
|
return props.workflow.getParentNodesByDepth(rootNode.value);
|
||||||
|
@ -404,9 +419,28 @@ function activatePane() {
|
||||||
v-if="(isActiveNodeConfig && rootNode) || parentNodes.length"
|
v-if="(isActiveNodeConfig && rootNode) || parentNodes.length"
|
||||||
:class="$style.noOutputData"
|
:class="$style.noOutputData"
|
||||||
>
|
>
|
||||||
<N8nText tag="div" :bold="true" color="text-dark" size="large">{{
|
<template v-if="isMappingEnabled || hasRootNodeRun">
|
||||||
i18n.baseText('ndv.input.noOutputData.title')
|
<N8nText tag="div" :bold="true" color="text-dark" size="large">{{
|
||||||
}}</N8nText>
|
i18n.baseText('ndv.input.noOutputData.title')
|
||||||
|
}}</N8nText>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<N8nText tag="div" :bold="true" color="text-dark" size="large">{{
|
||||||
|
i18n.baseText('ndv.input.rootNodeHasNotRun.title')
|
||||||
|
}}</N8nText>
|
||||||
|
<N8nText tag="div" color="text-dark" size="medium">
|
||||||
|
<i18n-t tag="span" keypath="ndv.input.rootNodeHasNotRun.description">
|
||||||
|
<template #link>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
data-test-id="switch-to-mapping-mode-link"
|
||||||
|
@click.prevent="onInputModeChange('mapping')"
|
||||||
|
>{{ i18n.baseText('ndv.input.rootNodeHasNotRun.description.link') }}</a
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</N8nText>
|
||||||
|
</template>
|
||||||
<N8nTooltip v-if="!readOnly" :visible="showDraggableHint && showDraggableHintWithDelay">
|
<N8nTooltip v-if="!readOnly" :visible="showDraggableHint && showDraggableHintWithDelay">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div
|
<div
|
||||||
|
@ -423,6 +457,7 @@ function activatePane() {
|
||||||
:transparent="true"
|
:transparent="true"
|
||||||
:node-name="(isActiveNodeConfig ? rootNode : currentNodeName) ?? ''"
|
:node-name="(isActiveNodeConfig ? rootNode : currentNodeName) ?? ''"
|
||||||
:label="i18n.baseText('ndv.input.noOutputData.executePrevious')"
|
:label="i18n.baseText('ndv.input.noOutputData.executePrevious')"
|
||||||
|
class="mt-m"
|
||||||
telemetry-source="inputs"
|
telemetry-source="inputs"
|
||||||
data-test-id="execute-previous-node"
|
data-test-id="execute-previous-node"
|
||||||
@execute="onNodeExecute"
|
@execute="onNodeExecute"
|
||||||
|
@ -494,11 +529,7 @@ function activatePane() {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
.noOutputData {
|
.noOutputData {
|
||||||
max-width: 180px;
|
max-width: 250px;
|
||||||
|
|
||||||
> *:first-child {
|
|
||||||
margin-bottom: var(--spacing-m);
|
|
||||||
}
|
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
margin-bottom: var(--spacing-2xs);
|
margin-bottom: var(--spacing-2xs);
|
||||||
|
|
|
@ -28,19 +28,6 @@ exports[`InputPanel > should render 1`] = `
|
||||||
role="radiogroup"
|
role="radiogroup"
|
||||||
>
|
>
|
||||||
|
|
||||||
<label
|
|
||||||
aria-checked="false"
|
|
||||||
class="n8n-radio-button container hoverable"
|
|
||||||
role="radio"
|
|
||||||
tabindex="-1"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="button medium"
|
|
||||||
data-test-id="radio-button-mapping"
|
|
||||||
>
|
|
||||||
Mapping
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<label
|
<label
|
||||||
aria-checked="true"
|
aria-checked="true"
|
||||||
class="n8n-radio-button container hoverable"
|
class="n8n-radio-button container hoverable"
|
||||||
|
@ -49,6 +36,19 @@ exports[`InputPanel > should render 1`] = `
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="button active medium"
|
class="button active medium"
|
||||||
|
data-test-id="radio-button-mapping"
|
||||||
|
>
|
||||||
|
Mapping
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
aria-checked="false"
|
||||||
|
class="n8n-radio-button container hoverable"
|
||||||
|
role="radio"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="button medium"
|
||||||
data-test-id="radio-button-debugging"
|
data-test-id="radio-button-debugging"
|
||||||
>
|
>
|
||||||
Debugging
|
Debugging
|
||||||
|
|
|
@ -984,6 +984,9 @@
|
||||||
"ndv.input.notConnected.learnMore": "Learn more",
|
"ndv.input.notConnected.learnMore": "Learn more",
|
||||||
"ndv.input.disabled": "The '{nodeName}' node is disabled and won’t execute.",
|
"ndv.input.disabled": "The '{nodeName}' node is disabled and won’t execute.",
|
||||||
"ndv.input.disabled.cta": "Enable it",
|
"ndv.input.disabled.cta": "Enable it",
|
||||||
|
"ndv.input.rootNodeHasNotRun.title": "Parent node hasn’t run yet",
|
||||||
|
"ndv.input.rootNodeHasNotRun.description": "Inputs that the parent node sends to this one will appear here. To map data in from previous nodes, use the {link} view.",
|
||||||
|
"ndv.input.rootNodeHasNotRun.description.link": "mapping",
|
||||||
"ndv.output": "Output",
|
"ndv.output": "Output",
|
||||||
"ndv.output.ai.empty": "👈 Use these logs to see information on how the {node} node completed processing. You can click on a node to see the input it received and data it output.",
|
"ndv.output.ai.empty": "👈 Use these logs to see information on how the {node} node completed processing. You can click on a node to see the input it received and data it output.",
|
||||||
"ndv.output.ai.waiting": "Waiting for message",
|
"ndv.output.ai.waiting": "Waiting for message",
|
||||||
|
|
Loading…
Reference in a new issue