fix(editor): Don't mark node as dirty when NDV is opened (#15222)

This commit is contained in:
Suguru Inoue
2025-05-20 15:23:38 +02:00
committed by GitHub
parent fbf7083062
commit 8d1170e3dd
4 changed files with 53 additions and 14 deletions

View File

@@ -55,13 +55,16 @@ describe('Sub-workflow creation and typed usage', () => {
openNode('Execute Workflow');
let openedUrl = '';
// Prevent sub-workflow from opening in new window
cy.window().then((win) => {
cy.stub(win, 'open').callsFake((url) => {
cy.visit(url);
openedUrl = url;
});
});
selectResourceLocatorItem('workflowId', 0, 'Create a');
cy.then(() => cy.visit(openedUrl));
// **************************
// NAVIGATE TO CHILD WORKFLOW
// **************************

View File

@@ -13,7 +13,7 @@ import type {
ResourceMapperFields,
ResourceMapperValue,
} from 'n8n-workflow';
import { NodeHelpers } from 'n8n-workflow';
import { deepCopy, NodeHelpers } from 'n8n-workflow';
import { computed, onMounted, reactive, watch } from 'vue';
import MappingModeSelect from './MappingModeSelect.vue';
import MatchingColumnsSelect from './MatchingColumnsSelect.vue';
@@ -537,7 +537,9 @@ function emitValueChanged(): void {
pruneParamValues();
emit('valueChanged', {
name: `${props.path}`,
value: state.paramValue,
// deepCopy ensures that mutations to state.paramValue that occur in
// this component are never visible to the store without explicit event emits
value: deepCopy(state.paramValue),
node: props.node?.name,
});
updateNodeIssues();

View File

@@ -31,6 +31,7 @@ import * as apiUtils from '@/utils/apiUtils';
import { useSettingsStore } from '@/stores/settings.store';
import { useLocalStorage } from '@vueuse/core';
import { ref } from 'vue';
import { createTestNode } from '@/__tests__/mocks';
vi.mock('@/stores/ndv.store', () => ({
useNDVStore: vi.fn(() => ({
@@ -1041,6 +1042,36 @@ describe('useWorkflowsStore', () => {
});
});
describe('setNodeParameters', () => {
beforeEach(() => {
workflowsStore.setNodes([createTestNode({ name: 'a', parameters: { p: 1, q: true } })]);
});
it('should set node parameters', () => {
expect(workflowsStore.nodesByName.a.parameters).toEqual({ p: 1, q: true });
workflowsStore.setNodeParameters({ name: 'a', value: { q: false, r: 's' } });
expect(workflowsStore.nodesByName.a.parameters).toEqual({ q: false, r: 's' });
});
it('should set node parameters preserving existing ones if append=true', () => {
expect(workflowsStore.nodesByName.a.parameters).toEqual({ p: 1, q: true });
workflowsStore.setNodeParameters({ name: 'a', value: { q: false, r: 's' } }, true);
expect(workflowsStore.nodesByName.a.parameters).toEqual({ p: 1, q: false, r: 's' });
});
it('should not update last parameter update time if parameters are set to the same value', () => {
expect(workflowsStore.getParametersLastUpdate('a')).toEqual(undefined);
workflowsStore.setNodeParameters({ name: 'a', value: { p: 1, q: true } });
expect(workflowsStore.getParametersLastUpdate('a')).toEqual(undefined);
});
});
describe('renameNodeSelectedAndExecution', () => {
it('should rename node and update execution data', () => {
const nodeName = 'Rename me';

View File

@@ -1290,12 +1290,12 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
return false;
}
function setNodeIssue(nodeIssueData: INodeIssueData): boolean {
function setNodeIssue(nodeIssueData: INodeIssueData): void {
const nodeIndex = workflow.value.nodes.findIndex((node) => {
return node.name === nodeIssueData.node;
});
if (nodeIndex === -1) {
return false;
return;
}
const node = workflow.value.nodes[nodeIndex];
@@ -1304,7 +1304,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
// Remove the value if one exists
if (node.issues?.[nodeIssueData.type] === undefined) {
// No values for type exist so nothing has to get removed
return true;
return;
}
const { [nodeIssueData.type]: removedNodeIssue, ...remainingNodeIssues } = node.issues;
@@ -1319,7 +1319,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
},
});
}
return true;
}
function addNode(nodeData: INodeUi): void {
@@ -1390,12 +1389,14 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
if (nodeIndex !== -1) {
for (const key of Object.keys(updateInformation.properties)) {
uiStore.stateIsDirty = true;
const typedKey = key as keyof INodeUpdatePropertiesInformation['properties'];
const property = updateInformation.properties[typedKey];
updateNodeAtIndex(nodeIndex, { [key]: property });
const changed = updateNodeAtIndex(nodeIndex, { [key]: property });
if (changed) {
uiStore.stateIsDirty = true;
}
}
}
}
@@ -1420,7 +1421,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const excludeKeys = ['position', 'notes', 'notesInFlow'];
if (!excludeKeys.includes(updateInformation.key)) {
if (changed && !excludeKeys.includes(updateInformation.key)) {
nodeMetadata.value[workflow.value.nodes[nodeIndex].name].parametersLastUpdatedAt = Date.now();
}
}
@@ -1439,17 +1440,19 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const node = workflow.value.nodes[nodeIndex];
uiStore.stateIsDirty = true;
const newParameters =
!!append && isObject(updateInformation.value)
? { ...node.parameters, ...updateInformation.value }
: updateInformation.value;
updateNodeAtIndex(nodeIndex, {
const changed = updateNodeAtIndex(nodeIndex, {
parameters: newParameters as INodeParameters,
});
nodeMetadata.value[node.name].parametersLastUpdatedAt = Date.now();
if (changed) {
uiStore.stateIsDirty = true;
nodeMetadata.value[node.name].parametersLastUpdatedAt = Date.now();
}
}
function setLastNodeParameters(updateInformation: IUpdateInformation): void {