refactor(editor): Migrate agentRequest store to @n8n/stores package (no-changelog) (#15462)

This commit is contained in:
Alex Grozav
2025-05-16 16:46:09 +03:00
committed by GitHub
parent 0244f1d98b
commit 4f82040083
11 changed files with 18 additions and 9 deletions

View File

@@ -5,7 +5,7 @@ import { FROM_AI_PARAMETERS_MODAL_KEY, AI_MCP_TOOL_NODE_TYPE } from '@/constants
import { STORES } from '@n8n/stores';
import userEvent from '@testing-library/user-event';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useAgentRequestStore } from '@/stores/agentRequest.store';
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
import { useRouter } from 'vue-router';
import { NodeConnectionTypes } from 'n8n-workflow';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';

View File

@@ -2,7 +2,7 @@
import { useI18n } from '@/composables/useI18n';
import { useRunWorkflow } from '@/composables/useRunWorkflow';
import { FROM_AI_PARAMETERS_MODAL_KEY, AI_MCP_TOOL_NODE_TYPE } from '@/constants';
import { useAgentRequestStore } from '@/stores/agentRequest.store';
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { createEventBus } from '@n8n/utils/event-bus';
import {

View File

@@ -25,7 +25,7 @@ import { useSettingsStore } from '@/stores/settings.store';
import { usePushConnectionStore } from '@/stores/pushConnection.store';
import { createTestNode, createTestWorkflow } from '@/__tests__/mocks';
import { waitFor } from '@testing-library/vue';
import { useAgentRequestStore } from '@/stores/agentRequest.store';
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
vi.mock('@/stores/workflows.store', () => {
const storeState: Partial<ReturnType<typeof useWorkflowsStore>> & {

View File

@@ -43,7 +43,7 @@ import { useSettingsStore } from '@/stores/settings.store';
import { usePushConnectionStore } from '@/stores/pushConnection.store';
import { useNodeDirtiness } from '@/composables/useNodeDirtiness';
import { useCanvasOperations } from './useCanvasOperations';
import { useAgentRequestStore } from '@/stores/agentRequest.store';
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof useRouter> }) {
const nodeHelpers = useNodeHelpers();

View File

@@ -1,246 +0,0 @@
import { setActivePinia, createPinia } from 'pinia';
import { useAgentRequestStore } from './agentRequest.store';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { nextTick } from 'vue';
// Mock localStorage
const localStorageMock = {
getItem: vi.fn(),
setItem: vi.fn(),
clear: vi.fn(),
};
Object.defineProperty(window, 'localStorage', {
value: localStorageMock,
writable: true,
});
describe('parameterOverrides.store', () => {
beforeEach(() => {
setActivePinia(createPinia());
localStorageMock.getItem.mockReset();
localStorageMock.setItem.mockReset();
localStorageMock.clear.mockReset();
});
describe('Initialization', () => {
it('initializes with empty state when localStorage is empty', () => {
localStorageMock.getItem.mockReturnValue(null);
const store = useAgentRequestStore();
expect(store.agentRequests).toEqual({});
});
it('initializes with data from localStorage', () => {
const mockData = {
'workflow-1': {
'node-1': { param1: 'value1' },
},
};
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
const store = useAgentRequestStore();
expect(store.agentRequests).toEqual(mockData);
});
it('handles localStorage errors gracefully', () => {
localStorageMock.getItem.mockImplementation(() => {
throw new Error('Storage error');
});
const store = useAgentRequestStore();
expect(store.agentRequests).toEqual({});
});
});
describe('Getters', () => {
it('gets parameter overrides for a node', () => {
const mockData = {
'workflow-1': {
'node-1': { param1: 'value1', param2: 'value2' },
},
};
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
const store = useAgentRequestStore();
const overrides = store.getAgentRequests('workflow-1', 'node-1');
expect(overrides).toEqual({ param1: 'value1', param2: 'value2' });
});
it('returns empty object for non-existent workflow/node', () => {
const store = useAgentRequestStore();
const overrides = store.getAgentRequests('non-existent', 'node-1');
expect(overrides).toEqual({});
});
it('gets a specific parameter override', () => {
const mockData = {
'workflow-1': {
'node-1': { param1: 'value1', param2: 'value2' },
},
};
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
const store = useAgentRequestStore();
const override = store.getAgentRequest('workflow-1', 'node-1', 'param1');
expect(override).toBe('value1');
});
it('returns undefined for non-existent parameter', () => {
const mockData = {
'workflow-1': {
'node-1': { param1: 'value1' },
},
};
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
const store = useAgentRequestStore();
const override = store.getAgentRequest('workflow-1', 'node-1', 'non-existent');
expect(override).toBeUndefined();
});
});
describe('Actions', () => {
it('adds a parameter override', () => {
const store = useAgentRequestStore();
store.addAgentRequest('workflow-1', 'node-1', 'param1', 'value1');
expect(store.agentRequests['workflow-1']['node-1']['param1']).toBe('value1');
});
it('adds multiple parameter overrides', () => {
const store = useAgentRequestStore();
store.addAgentRequests('workflow-1', 'node-1', {
param1: 'value1',
param2: 'value2',
});
expect(store.agentRequests['workflow-1']['node-1']).toEqual({
param1: 'value1',
param2: 'value2',
});
});
it('clears parameter overrides for a node', () => {
const mockData = {
'workflow-1': {
'node-1': { param1: 'value1', param2: 'value2' },
'node-2': { param3: 'value3' },
},
};
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
const store = useAgentRequestStore();
store.clearAgentRequests('workflow-1', 'node-1');
expect(store.agentRequests['workflow-1']['node-1']).toEqual({});
expect(store.agentRequests['workflow-1']['node-2']).toEqual({ param3: 'value3' });
});
it('clears all parameter overrides for a workflow', () => {
const mockData = {
'workflow-1': {
'node-1': { param1: 'value1' },
'node-2': { param2: 'value2' },
},
'workflow-2': {
'node-3': { param3: 'value3' },
},
};
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
const store = useAgentRequestStore();
store.clearAllAgentRequests('workflow-1');
expect(store.agentRequests['workflow-1']).toEqual({});
expect(store.agentRequests['workflow-2']).toEqual({
'node-3': { param3: 'value3' },
});
});
it('clears all parameter overrides when no workflowId is provided', () => {
const mockData = {
'workflow-1': {
'node-1': { param1: 'value1' },
},
'workflow-2': {
'node-2': { param2: 'value2' },
},
};
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
const store = useAgentRequestStore();
store.clearAllAgentRequests();
expect(store.agentRequests).toEqual({});
});
});
describe('generateAgentRequest', () => {
it('generateAgentRequest', () => {
const store = useAgentRequestStore();
store.addAgentRequests('workflow-1', 'id1', {
param1: 'override1',
'parent.child': 'override2',
'parent.array[0].value': 'overrideArray1',
'parent.array[1].value': 'overrideArray2',
});
const result = store.generateAgentRequest('workflow-1', 'id1');
expect(result).toEqual({
param1: 'override1',
parent: {
child: 'override2',
array: [
{
value: 'overrideArray1',
},
{
value: 'overrideArray2',
},
],
},
});
});
});
describe('Persistence', () => {
it('saves to localStorage when state changes', async () => {
const store = useAgentRequestStore();
localStorageMock.setItem.mockReset();
store.addAgentRequest('workflow-1', 'node-1', 'param1', 'value1');
// Wait for the next tick to allow the watch to execute
await nextTick();
expect(localStorageMock.setItem).toHaveBeenCalledWith(
'n8n-agent-requests',
JSON.stringify({
'workflow-1': {
'node-1': { param1: 'value1' },
},
}),
);
});
it('should handle localStorage errors when saving', async () => {
const store = useAgentRequestStore();
localStorageMock.setItem.mockReset();
localStorageMock.setItem.mockImplementation(() => {
throw new Error('Storage error');
});
store.addAgentRequest('workflow-1', 'node-1', 'param1', 'value1');
await nextTick();
expect(store.agentRequests['workflow-1']['node-1'].param1).toBe('value1');
});
});
});

View File

@@ -1,210 +0,0 @@
import { type INodeParameters, type NodeParameterValueType } from 'n8n-workflow';
import { defineStore } from 'pinia';
import { ref, watch } from 'vue';
interface IAgentRequestStoreState {
[workflowId: string]: {
[nodeName: string]: INodeParameters;
};
}
const STORAGE_KEY = 'n8n-agent-requests';
export const useAgentRequestStore = defineStore('agentRequest', () => {
// State
const agentRequests = ref<IAgentRequestStoreState>(loadFromLocalStorage());
// Load initial state from localStorage
function loadFromLocalStorage(): IAgentRequestStoreState {
try {
const storedData = localStorage.getItem(STORAGE_KEY);
return storedData ? JSON.parse(storedData) : {};
} catch (error) {
return {};
}
}
// Save state to localStorage whenever it changes
watch(
agentRequests,
(newValue) => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(newValue));
} catch (error) {
console.error('Failed to save agent requests to localStorage:', error);
}
},
{ deep: true },
);
// Helper function to ensure workflow and node entries exist
const ensureWorkflowAndNodeExist = (workflowId: string, nodeId: string): void => {
if (!agentRequests.value[workflowId]) {
agentRequests.value[workflowId] = {};
}
if (!agentRequests.value[workflowId][nodeId]) {
agentRequests.value[workflowId][nodeId] = {};
}
};
// Getters
const getAgentRequests = (workflowId: string, nodeId: string): INodeParameters => {
return agentRequests.value[workflowId]?.[nodeId] || {};
};
const getAgentRequest = (
workflowId: string,
nodeId: string,
paramName: string,
): NodeParameterValueType | undefined => {
return agentRequests.value[workflowId]?.[nodeId]?.[paramName];
};
// Actions
const addAgentRequest = (
workflowId: string,
nodeId: string,
paramName: string,
paramValues: NodeParameterValueType,
): INodeParameters => {
ensureWorkflowAndNodeExist(workflowId, nodeId);
agentRequests.value[workflowId][nodeId] = {
...agentRequests.value[workflowId][nodeId],
[paramName]: paramValues,
};
return agentRequests.value[workflowId][nodeId];
};
const addAgentRequests = (workflowId: string, nodeId: string, params: INodeParameters): void => {
ensureWorkflowAndNodeExist(workflowId, nodeId);
agentRequests.value[workflowId][nodeId] = {
...agentRequests.value[workflowId][nodeId],
...params,
};
};
const clearAgentRequests = (workflowId: string, nodeId: string): void => {
if (agentRequests.value[workflowId]) {
agentRequests.value[workflowId][nodeId] = {};
}
};
const clearAllAgentRequests = (workflowId?: string): void => {
if (workflowId) {
// Clear requests for a specific workflow
agentRequests.value[workflowId] = {};
} else {
// Clear all requests
agentRequests.value = {};
}
};
function parsePath(path: string): string[] {
return path.split('.').reduce((acc: string[], part) => {
if (part.includes('[')) {
const [arrayName, index] = part.split('[');
if (arrayName) acc.push(arrayName);
if (index) acc.push(index.replace(']', ''));
} else {
acc.push(part);
}
return acc;
}, []);
}
function buildRequestObject(path: string[], value: NodeParameterValueType): INodeParameters {
const result: INodeParameters = {};
let current = result;
for (let i = 0; i < path.length - 1; i++) {
const part = path[i];
const nextPart = path[i + 1];
const isArrayIndex = nextPart && !isNaN(Number(nextPart));
if (isArrayIndex) {
if (!current[part]) {
current[part] = [];
}
while ((current[part] as NodeParameterValueType[]).length <= Number(nextPart)) {
(current[part] as NodeParameterValueType[]).push({});
}
} else if (!current[part]) {
current[part] = {};
}
current = current[part] as INodeParameters;
}
current[path[path.length - 1]] = value;
return result;
}
// Helper function to deep merge objects
function deepMerge(target: INodeParameters, source: INodeParameters): INodeParameters {
const result = { ...target };
for (const key in source) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
// Recursively merge nested objects
result[key] = deepMerge(
(result[key] as INodeParameters) || {},
source[key] as INodeParameters,
);
} else if (Array.isArray(source[key])) {
// For arrays, merge by index
if (Array.isArray(result[key])) {
const targetArray = result[key] as NodeParameterValueType[];
const sourceArray = source[key] as NodeParameterValueType[];
// Ensure target array has enough elements
while (targetArray.length < sourceArray.length) {
targetArray.push({});
}
// Merge each array item
sourceArray.forEach((item, index) => {
if (item && typeof item === 'object') {
targetArray[index] = deepMerge(
(targetArray[index] as INodeParameters) || {},
item as INodeParameters,
) as NodeParameterValueType;
} else {
targetArray[index] = item;
}
});
} else {
result[key] = source[key];
}
} else {
// For primitive values, use source value
result[key] = source[key];
}
}
return result;
}
const generateAgentRequest = (workflowId: string, nodeId: string): INodeParameters => {
const nodeRequests = agentRequests.value[workflowId]?.[nodeId] || {};
return Object.entries(nodeRequests).reduce(
(acc, [path, value]) => deepMerge(acc, buildRequestObject(parsePath(path), value)),
{} as INodeParameters,
);
};
return {
agentRequests,
getAgentRequests,
getAgentRequest,
addAgentRequest,
addAgentRequests,
clearAgentRequests,
clearAllAgentRequests,
generateAgentRequest,
};
});

View File

@@ -120,7 +120,7 @@ import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
import { useBuilderStore } from '@/stores/builder.store';
import { useFoldersStore } from '@/stores/folders.store';
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
import { useAgentRequestStore } from '@/stores/agentRequest.store';
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
import { needsAgentInput } from '@/utils/nodes/nodeTransforms';
import { useLogsStore } from '@/stores/logs.store';