mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-19 11:01:15 +00:00
refactor(editor): Migrate agentRequest store to @n8n/stores package (no-changelog) (#15462)
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>> & {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user