chore: Refactor nodeValues back to NodeSettings (no-changelog) (#17300)

This commit is contained in:
Charlie Kolb
2025-07-15 11:14:07 +02:00
committed by GitHub
parent fc3129e378
commit ec69bcc3fd
6 changed files with 157 additions and 130 deletions

View File

@@ -2,7 +2,7 @@
import { useFocusPanelStore } from '@/stores/focusPanel.store'; import { useFocusPanelStore } from '@/stores/focusPanel.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { N8nText, N8nInput, N8nResizeWrapper } from '@n8n/design-system'; import { N8nText, N8nInput, N8nResizeWrapper } from '@n8n/design-system';
import { computed, nextTick, ref, watch } from 'vue'; import { computed, nextTick, ref, watch, toRef } from 'vue';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import { import {
formatAsExpression, formatAsExpression,
@@ -184,6 +184,7 @@ function valueChanged(value: string) {
} }
nodeSettingsParameters.updateNodeParameter( nodeSettingsParameters.updateNodeParameter(
toRef(resolvedParameter.value.node.parameters),
{ value, name: resolvedParameter.value.parameterPath as `parameters.${string}` }, { value, name: resolvedParameter.value.parameterPath as `parameters.${string}` },
value, value,
resolvedParameter.value.node, resolvedParameter.value.node,

View File

@@ -94,6 +94,19 @@ const emit = defineEmits<{
const slots = defineSlots<{ actions?: {} }>(); const slots = defineSlots<{ actions?: {} }>();
const nodeValues = ref<INodeParameters>({
color: '#ff0000',
alwaysOutputData: false,
executeOnce: false,
notesInFlow: false,
onError: 'stopWorkflow',
retryOnFail: false,
maxTries: 3,
waitBetweenTries: 1000,
notes: '',
parameters: {},
});
const nodeTypesStore = useNodeTypesStore(); const nodeTypesStore = useNodeTypesStore();
const ndvStore = useNDVStore(); const ndvStore = useNDVStore();
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
@@ -106,7 +119,6 @@ const nodeHelpers = useNodeHelpers();
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
const i18n = useI18n(); const i18n = useI18n();
const nodeSettingsParameters = useNodeSettingsParameters(); const nodeSettingsParameters = useNodeSettingsParameters();
const nodeValues = nodeSettingsParameters.nodeValues;
const nodeParameterWrapper = useTemplateRef('nodeParameterWrapper'); const nodeParameterWrapper = useTemplateRef('nodeParameterWrapper');
const shouldShowStaticScrollbar = ref(false); const shouldShowStaticScrollbar = ref(false);
@@ -355,7 +367,11 @@ const valueChanged = (parameterData: IUpdateInformation) => {
for (const key of Object.keys(nodeParameters as object)) { for (const key of Object.keys(nodeParameters as object)) {
if (nodeParameters?.[key] !== null && nodeParameters?.[key] !== undefined) { if (nodeParameters?.[key] !== null && nodeParameters?.[key] !== undefined) {
nodeSettingsParameters.setValue(`parameters.${key}`, nodeParameters[key] as string); nodeSettingsParameters.setValue(
nodeValues,
`parameters.${key}`,
nodeParameters[key] as string,
);
} }
} }
@@ -372,7 +388,13 @@ const valueChanged = (parameterData: IUpdateInformation) => {
} }
} else if (nameIsParameter(parameterData)) { } else if (nameIsParameter(parameterData)) {
// A node parameter changed // A node parameter changed
nodeSettingsParameters.updateNodeParameter(parameterData, newValue, _node, isToolNode.value); nodeSettingsParameters.updateNodeParameter(
nodeValues,
parameterData,
newValue,
_node,
isToolNode.value,
);
} else { } else {
// A property on the node itself changed // A property on the node itself changed

View File

@@ -18,43 +18,6 @@ describe('useNodeSettingsParameters', () => {
setActivePinia(createTestingPinia()); setActivePinia(createTestingPinia());
}); });
describe('setValue', () => {
afterEach(() => {
vi.clearAllMocks();
});
it('mutates nodeValues as expected', () => {
const nodeSettingsParameters = useNodeSettingsParameters();
expect(nodeSettingsParameters.nodeValues.value.color).toBe('#ff0000');
expect(nodeSettingsParameters.nodeValues.value.parameters).toEqual({});
nodeSettingsParameters.setValue('color', '#ffffff');
expect(nodeSettingsParameters.nodeValues.value.color).toBe('#ffffff');
expect(nodeSettingsParameters.nodeValues.value.parameters).toEqual({});
nodeSettingsParameters.setValue('parameters.key', 3);
expect(nodeSettingsParameters.nodeValues.value.parameters).toEqual({ key: 3 });
nodeSettingsParameters.nodeValues.value = { parameters: { some: { nested: {} } } };
nodeSettingsParameters.setValue('parameters.some.nested.key', true);
expect(nodeSettingsParameters.nodeValues.value.parameters).toEqual({
some: { nested: { key: true } },
});
nodeSettingsParameters.setValue('parameters', null);
expect(nodeSettingsParameters.nodeValues.value.parameters).toBe(undefined);
nodeSettingsParameters.setValue('newProperty', 'newValue');
expect(nodeSettingsParameters.nodeValues.value.newProperty).toBe('newValue');
});
});
describe('handleFocus', () => { describe('handleFocus', () => {
let ndvStore: MockedStore<typeof useNDVStore>; let ndvStore: MockedStore<typeof useNDVStore>;
let focusPanelStore: MockedStore<typeof useFocusPanelStore>; let focusPanelStore: MockedStore<typeof useFocusPanelStore>;

View File

@@ -1,6 +1,6 @@
import get from 'lodash/get'; import get from 'lodash/get';
import set from 'lodash/set'; import set from 'lodash/set';
import { ref } from 'vue'; import type { Ref } from 'vue';
import { import {
type INode, type INode,
type INodeParameters, type INodeParameters,
@@ -17,6 +17,7 @@ import { useExternalHooks } from './useExternalHooks';
import type { INodeUi, IUpdateInformation } from '@/Interface'; import type { INodeUi, IUpdateInformation } from '@/Interface';
import { import {
mustHideDuringCustomApiCall, mustHideDuringCustomApiCall,
setValue,
updateDynamicConnections, updateDynamicConnections,
updateParameterByPath, updateParameterByPath,
} from '@/utils/nodeSettingsUtils'; } from '@/utils/nodeSettingsUtils';
@@ -25,7 +26,6 @@ import { useFocusPanelStore } from '@/stores/focusPanel.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants'; import { KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
import { omitKey } from '@/utils/objectUtils';
import { import {
getMainAuthField, getMainAuthField,
getNodeAuthFields, getNodeAuthFields,
@@ -41,92 +41,8 @@ export function useNodeSettingsParameters() {
const canvasOperations = useCanvasOperations(); const canvasOperations = useCanvasOperations();
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
const nodeValues = ref<INodeParameters>({
color: '#ff0000',
alwaysOutputData: false,
executeOnce: false,
notesInFlow: false,
onError: 'stopWorkflow',
retryOnFail: false,
maxTries: 3,
waitBetweenTries: 1000,
notes: '',
parameters: {},
});
function setValue(name: string, value: NodeParameterValue) {
const nameParts = name.split('.');
let lastNamePart: string | undefined = nameParts.pop();
let isArray = false;
if (lastNamePart?.includes('[')) {
// It includes an index so we have to extract it
const lastNameParts = lastNamePart.match(/(.*)\[(\d+)\]$/);
if (lastNameParts) {
nameParts.push(lastNameParts[1]);
lastNamePart = lastNameParts[2];
isArray = true;
}
}
// Set the value so that everything updates correctly in the UI
if (nameParts.length === 0) {
// Data is on top level
if (value === null) {
// Property should be deleted
if (lastNamePart) {
nodeValues.value = omitKey(nodeValues.value, lastNamePart);
}
} else {
// Value should be set
nodeValues.value = {
...nodeValues.value,
[lastNamePart as string]: value,
};
}
} else {
// Data is on lower level
if (value === null) {
// Property should be deleted
let tempValue = get(nodeValues.value, nameParts.join('.')) as
| INodeParameters
| INodeParameters[];
if (lastNamePart && !Array.isArray(tempValue)) {
tempValue = omitKey(tempValue, lastNamePart);
}
if (isArray && Array.isArray(tempValue) && tempValue.length === 0) {
// If a value from an array got delete and no values are left
// delete also the parent
lastNamePart = nameParts.pop();
tempValue = get(nodeValues.value, nameParts.join('.')) as INodeParameters;
if (lastNamePart) {
tempValue = omitKey(tempValue, lastNamePart);
}
}
} else {
// Value should be set
if (typeof value === 'object') {
set(
get(nodeValues.value, nameParts.join('.')) as Record<string, unknown>,
lastNamePart as string,
deepCopy(value),
);
} else {
set(
get(nodeValues.value, nameParts.join('.')) as Record<string, unknown>,
lastNamePart as string,
value,
);
}
}
}
nodeValues.value = { ...nodeValues.value };
}
function updateNodeParameter( function updateNodeParameter(
nodeValues: Ref<INodeParameters>,
parameterData: IUpdateInformation & { name: `parameters.${string}` }, parameterData: IUpdateInformation & { name: `parameters.${string}` },
newValue: NodeParameterValue, newValue: NodeParameterValue,
node: INode, node: INode,
@@ -195,7 +111,7 @@ export function useNodeSettingsParameters() {
for (const [key, value] of Object.entries(nodeParameters as object)) { for (const [key, value] of Object.entries(nodeParameters as object)) {
if (value !== null && value !== undefined) { if (value !== null && value !== undefined) {
setValue(`parameters.${key}`, value as string); setValue(nodeValues, `parameters.${key}`, value as string);
} }
} }
@@ -353,7 +269,6 @@ export function useNodeSettingsParameters() {
} }
return { return {
nodeValues,
setValue, setValue,
shouldDisplayNodeParameter, shouldDisplayNodeParameter,
updateParameterByPath, updateParameterByPath,

View File

@@ -6,6 +6,7 @@ import type {
IDataObject, IDataObject,
INodeTypeDescription, INodeTypeDescription,
INodePropertyOptions, INodePropertyOptions,
INodeParameters,
INodeProperties, INodeProperties,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@@ -14,10 +15,12 @@ import {
nameIsParameter, nameIsParameter,
formatAsExpression, formatAsExpression,
parseFromExpression, parseFromExpression,
setValue,
shouldSkipParamValidation, shouldSkipParamValidation,
} from './nodeSettingsUtils'; } from './nodeSettingsUtils';
import { CUSTOM_API_CALL_KEY, SWITCH_NODE_TYPE } from '@/constants'; import { CUSTOM_API_CALL_KEY, SWITCH_NODE_TYPE } from '@/constants';
import type { INodeUi, IUpdateInformation } from '@/Interface'; import type { INodeUi, IUpdateInformation } from '@/Interface';
import { type Ref, ref } from 'vue';
describe('updateDynamicConnections', () => { describe('updateDynamicConnections', () => {
afterAll(() => { afterAll(() => {
@@ -553,3 +556,47 @@ describe('shouldSkipParamValidation', () => {
}); });
}); });
}); });
describe('setValue', () => {
let nodeValues: Ref<INodeParameters>;
beforeEach(() => {
nodeValues = ref({
color: '#ff0000',
alwaysOutputData: false,
executeOnce: false,
notesInFlow: false,
onError: 'stopWorkflow',
retryOnFail: false,
maxTries: 3,
waitBetweenTries: 1000,
notes: '',
parameters: {},
});
});
it('mutates nodeValues as expected', () => {
setValue(nodeValues, 'color', '#ffffff');
expect(nodeValues.value.color).toBe('#ffffff');
expect(nodeValues.value.parameters).toEqual({});
setValue(nodeValues, 'parameters.key', 3);
expect(nodeValues.value.parameters).toEqual({ key: 3 });
nodeValues.value = { parameters: { some: { nested: {} } } };
setValue(nodeValues, 'parameters.some.nested.key', true);
expect(nodeValues.value.parameters).toEqual({
some: { nested: { key: true } },
});
setValue(nodeValues, 'parameters', null);
expect(nodeValues.value.parameters).toBe(undefined);
setValue(nodeValues, 'newProperty', 'newValue');
expect(nodeValues.value.newProperty).toBe('newValue');
});
});

View File

@@ -17,6 +17,7 @@ import {
isINodePropertyOptionsList, isINodePropertyOptionsList,
displayParameter, displayParameter,
isResourceLocatorValue, isResourceLocatorValue,
deepCopy,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { INodeUi, IUpdateInformation } from '@/Interface'; import type { INodeUi, IUpdateInformation } from '@/Interface';
import { CUSTOM_API_CALL_KEY, SWITCH_NODE_TYPE } from '@/constants'; import { CUSTOM_API_CALL_KEY, SWITCH_NODE_TYPE } from '@/constants';
@@ -27,6 +28,84 @@ import unset from 'lodash/unset';
import { captureException } from '@sentry/vue'; import { captureException } from '@sentry/vue';
import { isPresent } from './typesUtils'; import { isPresent } from './typesUtils';
import type { Ref } from 'vue';
import { omitKey } from './objectUtils';
export function setValue(
nodeValues: Ref<INodeParameters>,
name: string,
value: NodeParameterValue,
) {
const nameParts = name.split('.');
let lastNamePart: string | undefined = nameParts.pop();
let isArray = false;
if (lastNamePart?.includes('[')) {
// It includes an index so we have to extract it
const lastNameParts = lastNamePart.match(/(.*)\[(\d+)\]$/);
if (lastNameParts) {
nameParts.push(lastNameParts[1]);
lastNamePart = lastNameParts[2];
isArray = true;
}
}
// Set the value so that everything updates correctly in the UI
if (nameParts.length === 0) {
// Data is on top level
if (value === null) {
// Property should be deleted
if (lastNamePart) {
nodeValues.value = omitKey(nodeValues.value, lastNamePart);
}
} else {
// Value should be set
nodeValues.value = {
...nodeValues.value,
[lastNamePart as string]: value,
};
}
} else {
// Data is on lower level
if (value === null) {
// Property should be deleted
let tempValue = get(nodeValues.value, nameParts.join('.')) as
| INodeParameters
| INodeParameters[];
if (lastNamePart && !Array.isArray(tempValue)) {
tempValue = omitKey(tempValue, lastNamePart);
}
if (isArray && Array.isArray(tempValue) && tempValue.length === 0) {
// If a value from an array got delete and no values are left
// delete also the parent
lastNamePart = nameParts.pop();
tempValue = get(nodeValues.value, nameParts.join('.')) as INodeParameters;
if (lastNamePart) {
tempValue = omitKey(tempValue, lastNamePart);
}
}
} else {
// Value should be set
if (typeof value === 'object') {
set(
get(nodeValues.value, nameParts.join('.')) as Record<string, unknown>,
lastNamePart as string,
deepCopy(value),
);
} else {
set(
get(nodeValues.value, nameParts.join('.')) as Record<string, unknown>,
lastNamePart as string,
value,
);
}
}
}
nodeValues.value = { ...nodeValues.value };
}
export function updateDynamicConnections( export function updateDynamicConnections(
node: INodeUi, node: INodeUi,