fix(editor): Show mappings by default in sub-node NDVs when the root node isn't executed (#12642)

This commit is contained in:
autologie 2025-01-23 08:47:41 +01:00 committed by GitHub
parent 114ed88368
commit fb662dd95c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 124 additions and 36 deletions

View file

@ -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();
});
}); });

View file

@ -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);

View file

@ -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

View file

@ -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 wont execute.", "ndv.input.disabled": "The '{nodeName}' node is disabled and wont execute.",
"ndv.input.disabled.cta": "Enable it", "ndv.input.disabled.cta": "Enable it",
"ndv.input.rootNodeHasNotRun.title": "Parent node hasnt 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",