mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-19 07:36:52 +00:00
refactor(editor): Scope execution-state reads by the injected workflow document (no-changelog) (#32219)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -127,6 +127,64 @@ export default defineConfig(
|
||||
message:
|
||||
'Do not call workflowsStore.setWorkflowId() — the current workflow id is derived from the route (useWorkflowId())',
|
||||
},
|
||||
// Guard: the legacy execution bridge on workflowsStore resolves by the
|
||||
// global workflow id, which silently reads the wrong instance inside
|
||||
// scoped hosts (execution preview, embedded editors). Read through
|
||||
// injectWorkflowExecutionStateStore() (or the documentId-keyed
|
||||
// useWorkflowExecutionStateStore) instead.
|
||||
{
|
||||
selector:
|
||||
"MemberExpression[property.name='getWorkflowExecution'][object.name='workflowsStore']",
|
||||
message:
|
||||
'Use injectWorkflowExecutionStateStore().value.activeExecution instead of workflowsStore.getWorkflowExecution — the bridge resolves by global workflow id and reads the wrong instance inside scoped hosts',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
"MemberExpression[property.name='workflowExecutionData'][object.name='workflowsStore']",
|
||||
message:
|
||||
'Use injectWorkflowExecutionStateStore().value.activeExecution instead of workflowsStore.workflowExecutionData — the bridge resolves by global workflow id and reads the wrong instance inside scoped hosts',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
"MemberExpression[property.name='getWorkflowRunData'][object.name='workflowsStore']",
|
||||
message:
|
||||
'Use injectWorkflowExecutionStateStore().value.activeExecutionRunData instead of workflowsStore.getWorkflowRunData',
|
||||
},
|
||||
{
|
||||
selector: "MemberExpression[property.name='executedNode'][object.name='workflowsStore']",
|
||||
message:
|
||||
'Use injectWorkflowExecutionStateStore().value.activeExecutionExecutedNode instead of workflowsStore.executedNode',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
"MemberExpression[property.name='workflowExecutionStartedData'][object.name='workflowsStore']",
|
||||
message:
|
||||
'Use injectWorkflowExecutionStateStore().value.activeExecutionStartedData instead of workflowsStore.workflowExecutionStartedData',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
"MemberExpression[property.name='workflowExecutionResultDataLastUpdate'][object.name='workflowsStore']",
|
||||
message:
|
||||
'Use injectWorkflowExecutionStateStore().value.activeExecutionResultDataLastUpdate instead of workflowsStore.workflowExecutionResultDataLastUpdate',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
"MemberExpression[property.name='workflowExecutionPairedItemMappings'][object.name='workflowsStore']",
|
||||
message:
|
||||
'Use injectWorkflowExecutionStateStore().value.activeExecutionPairedItemMappings instead of workflowsStore.workflowExecutionPairedItemMappings',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
"MemberExpression[property.name='lastSuccessfulExecution'][object.name='workflowsStore']",
|
||||
message:
|
||||
'Use injectWorkflowExecutionStateStore().value.lastSuccessfulExecution instead of workflowsStore.lastSuccessfulExecution',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
"MemberExpression[property.name='getWorkflowResultDataByNodeName'][object.name='workflowsStore']",
|
||||
message:
|
||||
'Use injectWorkflowExecutionStateStore().value.getActiveExecutionRunDataByNodeName() instead of workflowsStore.getWorkflowResultDataByNodeName()',
|
||||
},
|
||||
],
|
||||
// TODO: Remove these
|
||||
'n8n-local-rules/no-internal-package-import': 'warn',
|
||||
|
||||
@@ -33,15 +33,13 @@ import { type PinDataSource, usePinnedData } from '@/app/composables/usePinnedDa
|
||||
import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
import { useToast } from '@/app/composables/useToast';
|
||||
import { useWorkflowHelpers } from '@/app/composables/useWorkflowHelpers';
|
||||
import { useWorkflowNormalization } from '@/app/composables/useWorkflowNormalization';
|
||||
import { getExecutionErrorToastConfiguration } from '@/features/execution/executions/executions.utils';
|
||||
import {
|
||||
EnterpriseEditionFeature,
|
||||
FORM_TRIGGER_NODE_TYPE,
|
||||
MCP_TRIGGER_NODE_TYPE,
|
||||
STICKY_NODE_TYPE,
|
||||
UPDATE_WEBHOOK_ID_NODE_TYPES,
|
||||
VIEWS,
|
||||
WEBHOOK_NODE_TYPE,
|
||||
} from '@/app/constants';
|
||||
import {
|
||||
AddConnectionCommand,
|
||||
@@ -126,7 +124,6 @@ import {
|
||||
TelemetryHelpers,
|
||||
isCommunityPackageName,
|
||||
isHitlToolType,
|
||||
resolveNodeWebhookId,
|
||||
} from 'n8n-workflow';
|
||||
import { computed, nextTick, ref, type DeepReadonly } from 'vue';
|
||||
import { useUniqueNodeName } from '@/app/composables/useUniqueNodeName';
|
||||
@@ -212,6 +209,12 @@ export function useCanvasOperations() {
|
||||
const toast = useToast();
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const {
|
||||
requireNodeTypeDescription,
|
||||
resolveNodeParameters,
|
||||
resolveNodeWebhook,
|
||||
normalizeWorkflowData,
|
||||
} = useWorkflowNormalization();
|
||||
const telemetry = useTelemetry();
|
||||
const externalHooks = useExternalHooks();
|
||||
const clipboard = useClipboard();
|
||||
@@ -866,26 +869,6 @@ export function useCanvasOperations() {
|
||||
}
|
||||
}
|
||||
|
||||
function requireNodeTypeDescription(
|
||||
type: INodeUi['type'],
|
||||
version?: INodeUi['typeVersion'],
|
||||
): INodeTypeDescription {
|
||||
return (
|
||||
nodeTypesStore.getNodeType(type, version) ??
|
||||
nodeTypesStore.communityNodeType(type)?.nodeDescription ?? {
|
||||
properties: [],
|
||||
displayName: type,
|
||||
name: type,
|
||||
group: [],
|
||||
description: '',
|
||||
version: version ?? 1,
|
||||
defaults: {},
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function addNodes(
|
||||
nodes: AddedNodesAndConnections['nodes'],
|
||||
{ viewport, ...options }: AddNodesOptions = {},
|
||||
@@ -1315,18 +1298,6 @@ export function useCanvasOperations() {
|
||||
return nodeVersion;
|
||||
}
|
||||
|
||||
function resolveNodeParameters(node: INodeUi, nodeTypeDescription: INodeTypeDescription) {
|
||||
const nodeParameters = NodeHelpers.getNodeParameters(
|
||||
nodeTypeDescription?.properties ?? [],
|
||||
node.parameters,
|
||||
true,
|
||||
false,
|
||||
node,
|
||||
nodeTypeDescription,
|
||||
);
|
||||
node.parameters = nodeParameters ?? {};
|
||||
}
|
||||
|
||||
function resolveNodePosition(
|
||||
node: Omit<INodeUi, 'position'> & { position?: INodeUi['position'] },
|
||||
nodeTypeDescription: INodeTypeDescription,
|
||||
@@ -1617,18 +1588,6 @@ export function useCanvasOperations() {
|
||||
node.name = uniqueNodeName(localizedName);
|
||||
}
|
||||
|
||||
function resolveNodeWebhook(node: INodeUi, nodeTypeDescription: INodeTypeDescription) {
|
||||
resolveNodeWebhookId(node, nodeTypeDescription);
|
||||
|
||||
// if it's a webhook and the path is empty set the UUID as the default path
|
||||
if (
|
||||
[WEBHOOK_NODE_TYPE, FORM_TRIGGER_NODE_TYPE, MCP_TRIGGER_NODE_TYPE].includes(node.type) &&
|
||||
node.parameters.path === ''
|
||||
) {
|
||||
node.parameters.path = node.webhookId as string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the bounding rectangle of a node including its size
|
||||
*/
|
||||
@@ -2492,25 +2451,10 @@ export function useCanvasOperations() {
|
||||
const { workflowDocumentStore: initializedDocumentStore } =
|
||||
await workflowHelpers.initState(data);
|
||||
|
||||
// Filter out nodes with missing type to prevent canvas rendering crashes
|
||||
const validNodes = data.nodes
|
||||
.filter((node) => !!node.type)
|
||||
.map((node) => ({ ...node, position: ensureNodePosition(node.position) }));
|
||||
const validNodeNames = validNodes.map((node) => node.name);
|
||||
const { nodes, connections } = normalizeWorkflowData(data);
|
||||
|
||||
validNodes.forEach((node) => {
|
||||
const nodeTypeDescription = requireNodeTypeDescription(node.type, node.typeVersion);
|
||||
const isInstalledNode = nodeTypesStore.getIsNodeInstalled(node.type);
|
||||
nodeHelpers.matchCredentials(node);
|
||||
// skip this step because nodeTypeDescription is missing for unknown nodes
|
||||
if (isInstalledNode) {
|
||||
resolveNodeParameters(node, nodeTypeDescription);
|
||||
resolveNodeWebhook(node, nodeTypeDescription);
|
||||
}
|
||||
});
|
||||
|
||||
initializedDocumentStore.setNodes(validNodes);
|
||||
initializedDocumentStore.setConnections(sanitizeConnections(data.connections, validNodeNames));
|
||||
initializedDocumentStore.setNodes(nodes);
|
||||
initializedDocumentStore.setConnections(connections);
|
||||
|
||||
return { workflowDocumentStore: initializedDocumentStore };
|
||||
}
|
||||
|
||||
@@ -29,6 +29,22 @@ vi.mock('@/app/stores/workflowDocument.store', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
let mockExecution: IExecutionResponse | null = null;
|
||||
|
||||
vi.mock('@/app/stores/workflowExecutionState.store', async (importOriginal) => {
|
||||
const actual = await importOriginal<Record<string, unknown>>();
|
||||
return {
|
||||
...actual,
|
||||
injectWorkflowExecutionStateStore: vi.fn(() => ({
|
||||
// Plain accessor (not `computed`) so per-test reassignment of the
|
||||
// non-reactive `mockExecution` is always picked up.
|
||||
get value() {
|
||||
return { activeExecution: mockExecution };
|
||||
},
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useDataSchema', () => {
|
||||
const getSchema = useDataSchema().getSchema;
|
||||
|
||||
@@ -850,11 +866,8 @@ describe('useDataSchema', () => {
|
||||
],
|
||||
])(
|
||||
'should return correct output %s',
|
||||
([node, runIndex, outputIndex, getWorkflowExecution], output) => {
|
||||
vi.mocked(useWorkflowsStore).mockReturnValue({
|
||||
...useWorkflowsStore(),
|
||||
getWorkflowExecution: getWorkflowExecution as IExecutionResponse,
|
||||
});
|
||||
([node, runIndex, outputIndex, workflowExecution], output) => {
|
||||
mockExecution = (workflowExecution ?? null) as IExecutionResponse | null;
|
||||
expect(getNodeInputData(node as INodeUi, runIndex, outputIndex)).toEqual(output);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
Schema,
|
||||
SchemaType,
|
||||
} from '@/Interface';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { generatePath, getNodeParentExpression } from '@/app/utils/mappingUtils';
|
||||
import { isObject } from '@/app/utils/objectUtils';
|
||||
@@ -30,6 +30,7 @@ import { DEFAULT_SETTINGS } from '@/app/constants/workflows';
|
||||
|
||||
export function useDataSchema() {
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
|
||||
function getSchema(
|
||||
input: Optional<Primitives | object>,
|
||||
@@ -180,15 +181,15 @@ export function useDataSchema() {
|
||||
runIndex = 0,
|
||||
outputIndex = 0,
|
||||
): INodeExecutionData[] {
|
||||
const { getWorkflowExecution } = useWorkflowsStore();
|
||||
if (node === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (getWorkflowExecution === null) {
|
||||
const workflowExecution = workflowExecutionStateStore.value.activeExecution;
|
||||
if (workflowExecution === null) {
|
||||
return [];
|
||||
}
|
||||
const executionData = getWorkflowExecution.data;
|
||||
const executionData = workflowExecution.data;
|
||||
if (!executionData?.resultData) {
|
||||
// unknown status
|
||||
return [];
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
type Undoable,
|
||||
} from '@/app/models/history';
|
||||
import { useHistoryStore } from '@/app/stores/history.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import {
|
||||
useWorkflowDocumentStore,
|
||||
type WorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import {
|
||||
CanvasNodeDirtiness,
|
||||
type CanvasNodeDirtinessType,
|
||||
@@ -137,11 +137,13 @@ function findLoop(
|
||||
*/
|
||||
export function useNodeDirtiness(workflowDocumentId: MaybeRefOrGetter<WorkflowDocumentId>) {
|
||||
const historyStore = useHistoryStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
const workflowDocumentStore = computed(() =>
|
||||
useWorkflowDocumentStore(toValue(workflowDocumentId)),
|
||||
);
|
||||
const executionStateStore = computed(() =>
|
||||
useWorkflowExecutionStateStore(toValue(workflowDocumentId)),
|
||||
);
|
||||
|
||||
function getIncomingConnections(nodeName: string): INodeConnections {
|
||||
return workflowDocumentStore.value.incomingConnectionsByNodeName(nodeName);
|
||||
@@ -255,7 +257,7 @@ export function useNodeDirtiness(workflowDocumentId: MaybeRefOrGetter<WorkflowDo
|
||||
|
||||
const dirtinessByName = computed(() => {
|
||||
const dirtiness: Record<string, CanvasNodeDirtinessType | undefined> = {};
|
||||
const runDataByNode = workflowsStore.getWorkflowRunData ?? {};
|
||||
const runDataByNode = executionStateStore.value.activeExecutionRunData ?? {};
|
||||
|
||||
function setDirtiness(nodeName: string, value: CanvasNodeDirtinessType) {
|
||||
dirtiness[nodeName] = dirtiness[nodeName] ?? value;
|
||||
|
||||
@@ -102,6 +102,18 @@ vi.mock('@/app/stores/workflows.store', () => ({
|
||||
|
||||
vi.mock('@/app/stores/workflowExecutionState.store', () => ({
|
||||
useWorkflowExecutionStateStore: vi.fn().mockReturnValue(mockWorkflowExecutionStateStore),
|
||||
injectWorkflowExecutionStateStore: vi.fn(() => ({
|
||||
// Plain accessor so per-test reassignment of the mock fields is always
|
||||
// picked up.
|
||||
get value() {
|
||||
return {
|
||||
...mockWorkflowExecutionStateStore,
|
||||
get activeExecutionExecutedNode() {
|
||||
return mockWorkflowsStore.executedNode;
|
||||
},
|
||||
};
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/app/stores/nodeTypes.store', () => ({
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import type { INodeUi, IUpdateInformation } from '@/Interface';
|
||||
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { injectNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
@@ -102,9 +102,7 @@ export function useNodeExecution(
|
||||
const uiStore = useUIStore();
|
||||
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowExecutionStateStore = computed(() =>
|
||||
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
|
||||
);
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
|
||||
const { runWorkflow, stopCurrentExecution } = useRunWorkflow({ router });
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
@@ -146,7 +144,7 @@ export function useNodeExecution(
|
||||
const isNodeRunning = computed(() => {
|
||||
if (!workflowExecutionStateStore.value.isWorkflowRunning || codeGenerationInProgress.value)
|
||||
return false;
|
||||
const triggeredNode = workflowsStore.executedNode;
|
||||
const triggeredNode = workflowExecutionStateStore.value.activeExecutionExecutedNode;
|
||||
return (
|
||||
workflowExecutionStateStore.value.executingNode.isNodeExecuting(nodeRef.value?.name ?? '') ||
|
||||
triggeredNode === nodeRef.value?.name
|
||||
@@ -155,7 +153,7 @@ export function useNodeExecution(
|
||||
|
||||
const isListening = computed(() => {
|
||||
const waitingOnWebhook = workflowExecutionStateStore.value.executionWaitingForWebhook;
|
||||
const executedNode = workflowsStore.executedNode;
|
||||
const executedNode = workflowExecutionStateStore.value.activeExecutionExecutedNode;
|
||||
|
||||
return (
|
||||
!!nodeRef.value &&
|
||||
|
||||
@@ -58,6 +58,22 @@ vi.mock('@/app/stores/workflowDocument.store', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
const mockInjectedRunData = { value: null as IRunData | null };
|
||||
|
||||
vi.mock('@/app/stores/workflowExecutionState.store', async (importOriginal) => {
|
||||
const actual = await importOriginal<Record<string, unknown>>();
|
||||
return {
|
||||
...actual,
|
||||
injectWorkflowExecutionStateStore: vi.fn(() => ({
|
||||
// Plain accessor (not `computed`) so per-test reassignment of the
|
||||
// non-reactive holder is always picked up.
|
||||
get value() {
|
||||
return { activeExecutionRunData: mockInjectedRunData.value };
|
||||
},
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useNodeHelpers()', () => {
|
||||
beforeAll(() => {
|
||||
setActivePinia(createTestingPinia());
|
||||
@@ -72,6 +88,7 @@ describe('useNodeHelpers()', () => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockInjectedRunData.value = null;
|
||||
// Clear mock document store state
|
||||
for (const key of Object.keys(mockDocumentStoreUsedCredentials)) {
|
||||
delete mockDocumentStoreUsedCredentials[key];
|
||||
@@ -372,7 +389,7 @@ describe('useNodeHelpers()', () => {
|
||||
});
|
||||
|
||||
it('should return an empty array when runData is not available', () => {
|
||||
mockedStore(useWorkflowsStore).getWorkflowRunData = null;
|
||||
mockInjectedRunData.value = null;
|
||||
const { getNodeInputData } = useNodeHelpers();
|
||||
const node = createTestNode({
|
||||
name: 'test',
|
||||
@@ -385,7 +402,7 @@ describe('useNodeHelpers()', () => {
|
||||
|
||||
it('should return an empty array when taskData is unavailable', () => {
|
||||
const nodeName = 'Code';
|
||||
mockedStore(useWorkflowsStore).getWorkflowRunData = mock<IRunData>({
|
||||
mockInjectedRunData.value = mock<IRunData>({
|
||||
[nodeName]: [],
|
||||
});
|
||||
const { getNodeInputData } = useNodeHelpers();
|
||||
@@ -400,7 +417,7 @@ describe('useNodeHelpers()', () => {
|
||||
|
||||
it('should return an empty array when taskData.data is unavailable', () => {
|
||||
const nodeName = 'Code';
|
||||
mockedStore(useWorkflowsStore).getWorkflowRunData = mock<IRunData>({
|
||||
mockInjectedRunData.value = mock<IRunData>({
|
||||
[nodeName]: [{ data: undefined }],
|
||||
});
|
||||
const { getNodeInputData } = useNodeHelpers();
|
||||
@@ -416,7 +433,7 @@ describe('useNodeHelpers()', () => {
|
||||
it('should return input data from inputOverride', () => {
|
||||
const nodeName = 'Code';
|
||||
const data = [{ json: { hello: 'world' } }];
|
||||
mockedStore(useWorkflowsStore).getWorkflowRunData = mock<IRunData>({
|
||||
mockInjectedRunData.value = mock<IRunData>({
|
||||
[nodeName]: [
|
||||
{
|
||||
inputOverride: {
|
||||
@@ -439,7 +456,7 @@ describe('useNodeHelpers()', () => {
|
||||
'should return input data for "%s" node name, with given connection type and output index',
|
||||
(nodeName) => {
|
||||
const data = [{ json: { hello: 'world' } }];
|
||||
mockedStore(useWorkflowsStore).getWorkflowRunData = mock<IRunData>({
|
||||
mockInjectedRunData.value = mock<IRunData>({
|
||||
[nodeName]: [{ data: { main: [data] } }],
|
||||
});
|
||||
const { getNodeInputData } = useNodeHelpers();
|
||||
@@ -460,7 +477,7 @@ describe('useNodeHelpers()', () => {
|
||||
const nodeName = 'Test Node';
|
||||
const { getLastRunIndexWithData } = useNodeHelpers();
|
||||
|
||||
mockedStore(useWorkflowsStore).getWorkflowRunData = mock<IRunData>({
|
||||
mockInjectedRunData.value = mock<IRunData>({
|
||||
[nodeName]: [{ data: { main: [mockData] } }, { data: { main: [mockData] } }],
|
||||
});
|
||||
expect(getLastRunIndexWithData(nodeName)).toEqual(1);
|
||||
@@ -470,7 +487,7 @@ describe('useNodeHelpers()', () => {
|
||||
const nodeName = 'Test Node';
|
||||
const { getLastRunIndexWithData } = useNodeHelpers();
|
||||
|
||||
mockedStore(useWorkflowsStore).getWorkflowRunData = mock<IRunData>({
|
||||
mockInjectedRunData.value = mock<IRunData>({
|
||||
[nodeName]: [],
|
||||
});
|
||||
expect(getLastRunIndexWithData(nodeName)).toEqual(-1);
|
||||
@@ -480,7 +497,7 @@ describe('useNodeHelpers()', () => {
|
||||
const nodeName = 'Test Node';
|
||||
const { getLastRunIndexWithData } = useNodeHelpers();
|
||||
|
||||
mockedStore(useWorkflowsStore).getWorkflowRunData = null;
|
||||
mockInjectedRunData.value = null;
|
||||
expect(getLastRunIndexWithData(nodeName)).toEqual(-1);
|
||||
});
|
||||
|
||||
@@ -488,7 +505,7 @@ describe('useNodeHelpers()', () => {
|
||||
const nodeName = 'Test Node';
|
||||
const { getLastRunIndexWithData } = useNodeHelpers();
|
||||
|
||||
mockedStore(useWorkflowsStore).getWorkflowRunData = mock<IRunData>({
|
||||
mockInjectedRunData.value = mock<IRunData>({
|
||||
[nodeName]: [
|
||||
{ data: { main: [mockData, []] } },
|
||||
{ data: { main: [mockData, []] } },
|
||||
@@ -504,7 +521,7 @@ describe('useNodeHelpers()', () => {
|
||||
const nodeName = 'Test Node';
|
||||
const { getLastRunIndexWithData } = useNodeHelpers();
|
||||
|
||||
mockedStore(useWorkflowsStore).getWorkflowRunData = mock<IRunData>({
|
||||
mockInjectedRunData.value = mock<IRunData>({
|
||||
[nodeName]: [
|
||||
{ data: { main: [mockData], ai_tool: [mockData] } },
|
||||
{ data: { ai_tool: [mockData] } },
|
||||
@@ -518,7 +535,7 @@ describe('useNodeHelpers()', () => {
|
||||
describe('hasNodeExecuted()', () => {
|
||||
it('should return false when runData is not available', () => {
|
||||
const nodeName = 'Test Node';
|
||||
mockedStore(useWorkflowsStore).getWorkflowRunData = null;
|
||||
mockInjectedRunData.value = null;
|
||||
const { hasNodeExecuted } = useNodeHelpers();
|
||||
expect(hasNodeExecuted(nodeName)).toBe(false);
|
||||
});
|
||||
@@ -532,7 +549,7 @@ describe('useNodeHelpers()', () => {
|
||||
])('should return $expected when execution status is $status', ({ status, expected }) => {
|
||||
const nodeName = 'Test Node';
|
||||
|
||||
mockedStore(useWorkflowsStore).getWorkflowRunData = mock<IRunData>({
|
||||
mockInjectedRunData.value = mock<IRunData>({
|
||||
[nodeName]: [{ executionStatus: status }],
|
||||
});
|
||||
const { hasNodeExecuted } = useNodeHelpers();
|
||||
|
||||
@@ -50,6 +50,7 @@ import { hasPermission } from '@/app/utils/rbac/permissions';
|
||||
import { useCanvasStore } from '@/app/stores/canvas.store';
|
||||
import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
|
||||
declare namespace HttpRequestNode {
|
||||
@@ -71,6 +72,7 @@ export function useNodeHelpers() {
|
||||
const i18n = useI18n();
|
||||
const canvasStore = useCanvasStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials();
|
||||
|
||||
const isInsertingNodes = ref(false);
|
||||
@@ -242,7 +244,7 @@ export function useNodeHelpers() {
|
||||
// Set the status on all the nodes which produced an error so that it can be
|
||||
// displayed in the node-view
|
||||
function hasNodeExecutionIssues(node: INodeUi): boolean {
|
||||
const workflowResultData = workflowsStore.getWorkflowRunData;
|
||||
const workflowResultData = workflowExecutionStateStore.value.activeExecutionRunData;
|
||||
|
||||
if (!workflowResultData?.hasOwnProperty(node.name)) {
|
||||
return false;
|
||||
@@ -653,7 +655,8 @@ export function useNodeHelpers() {
|
||||
}
|
||||
|
||||
function getAllNodeTaskData(nodeName: string, execution?: IRunExecutionData) {
|
||||
const runData = execution?.resultData.runData ?? workflowsStore.getWorkflowRunData;
|
||||
const runData =
|
||||
execution?.resultData.runData ?? workflowExecutionStateStore.value.activeExecutionRunData;
|
||||
|
||||
return runData?.[nodeName] ?? null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { injectNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import {
|
||||
isExpression as isExpressionUtil,
|
||||
stringifyExpressionResult,
|
||||
@@ -36,7 +36,7 @@ export function useResolvedExpression({
|
||||
contextNodeName?: MaybeRefOrGetter<string>;
|
||||
}) {
|
||||
const ndvStore = injectNDVStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
|
||||
const { resolveExpression } = useWorkflowHelpers();
|
||||
@@ -53,7 +53,7 @@ export function useResolvedExpression({
|
||||
const activeNode = computed(() => ndvStore.value.activeNode);
|
||||
const hasRunData = computed(() =>
|
||||
Boolean(
|
||||
workflowsStore.workflowExecutionData?.data?.resultData?.runData[activeNode.value?.name ?? ''],
|
||||
workflowExecutionStateStore.value.activeExecutionRunData?.[activeNode.value?.name ?? ''],
|
||||
),
|
||||
);
|
||||
const isExpression = computed(() => isExpressionUtil(toValue(expression)));
|
||||
@@ -123,8 +123,8 @@ export function useResolvedExpression({
|
||||
expressionLocalResolveCtx,
|
||||
toRef(expression),
|
||||
toRef(additionalData),
|
||||
() => workflowsStore.getWorkflowExecution,
|
||||
() => workflowsStore.getWorkflowRunData,
|
||||
() => workflowExecutionStateStore.value.activeExecution,
|
||||
() => workflowExecutionStateStore.value.activeExecutionRunData,
|
||||
() => workflowDocumentStore.value.name,
|
||||
targetItem,
|
||||
],
|
||||
|
||||
@@ -653,7 +653,7 @@ describe('useRunWorkflow({ router })', () => {
|
||||
? { main: [[{ node: parentName, type: NodeConnectionTypes.Main, index: 0 }]] }
|
||||
: ({} as INodeConnections),
|
||||
);
|
||||
vi.mocked(workflowsStore).getWorkflowRunData = {
|
||||
const runData = {
|
||||
[parentName]: [
|
||||
{
|
||||
startTime: 1,
|
||||
@@ -675,6 +675,19 @@ describe('useRunWorkflow({ router })', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
vi.mocked(workflowsStore).getWorkflowRunData = runData;
|
||||
// Node dirtiness resolves run data through the execution-state store
|
||||
// keyed by the document id, so seed the real store as well.
|
||||
executionStateStore.setWorkflowExecutionData({
|
||||
id: 'previous-execution',
|
||||
workflowData: { id: '123', nodes: [], connections: {} },
|
||||
finished: true,
|
||||
mode: 'manual',
|
||||
status: 'success',
|
||||
startedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
data: { resultData: { runData } },
|
||||
} as unknown as IExecutionResponse);
|
||||
mockDocumentStore.serialize.mockReturnValue({
|
||||
nodes: [],
|
||||
} as unknown as WorkflowData);
|
||||
|
||||
@@ -5,6 +5,8 @@ import { ref, shallowRef, nextTick } from 'vue';
|
||||
import { waitFor } from '@testing-library/vue';
|
||||
import { useToolParameters } from './useToolParameters';
|
||||
import { useWorkflowsStore } from '../stores/workflows.store';
|
||||
import { useWorkflowExecutionStateStore } from '../stores/workflowExecutionState.store';
|
||||
import type { WorkflowDocumentId } from '../stores/workflowDocument.store';
|
||||
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
|
||||
import { useNodeTypesStore } from '../stores/nodeTypes.store';
|
||||
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
|
||||
@@ -15,6 +17,7 @@ import { AI_MCP_TOOL_NODE_TYPE } from '../constants';
|
||||
|
||||
const { mockWorkflowDocumentStore } = vi.hoisted(() => ({
|
||||
mockWorkflowDocumentStore: {
|
||||
documentId: 'test-workflow@latest',
|
||||
getNodeByName: vi.fn(),
|
||||
getParentNodes: vi.fn().mockReturnValue([]),
|
||||
allNodes: [],
|
||||
@@ -49,7 +52,12 @@ describe('useToolParameters', () => {
|
||||
mockWorkflowDocumentStore.getNodeByName.mockReset();
|
||||
projectsStore.currentProjectId = 'test-project';
|
||||
workflowsStore.setWorkflowId('test-workflow');
|
||||
workflowsStore.getWorkflowExecution = null;
|
||||
(
|
||||
mockedStore(
|
||||
useWorkflowExecutionStateStore,
|
||||
'test-workflow@latest' as WorkflowDocumentId,
|
||||
) as unknown as { activeExecution: unknown }
|
||||
).activeExecution = null;
|
||||
agentRequestStore.getQueryValue = vi.fn().mockReturnValue(null);
|
||||
});
|
||||
|
||||
@@ -158,7 +166,12 @@ describe('useToolParameters', () => {
|
||||
},
|
||||
};
|
||||
|
||||
workflowsStore.getWorkflowExecution = {
|
||||
(
|
||||
mockedStore(
|
||||
useWorkflowExecutionStateStore,
|
||||
'test-workflow@latest' as WorkflowDocumentId,
|
||||
) as unknown as { activeExecution: unknown }
|
||||
).activeExecution = {
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
traverseNodeParameters,
|
||||
} from 'n8n-workflow';
|
||||
import { computed, reactive, ref, watch, type Ref } from 'vue';
|
||||
import { useWorkflowsStore } from '../stores/workflows.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
|
||||
import { useNodeTypesStore } from '../stores/nodeTypes.store';
|
||||
@@ -27,7 +27,7 @@ interface GetToolParametersProps {
|
||||
|
||||
export function useToolParameters({ node }: GetToolParametersProps) {
|
||||
const parameters = ref<IFormInput[]>([]);
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const agentRequestStore = useAgentRequestStore();
|
||||
@@ -40,7 +40,7 @@ export function useToolParameters({ node }: GetToolParametersProps) {
|
||||
const nodeRunData = computed(() => {
|
||||
if (!node.value) return undefined;
|
||||
|
||||
const workflowExecutionData = workflowsStore.getWorkflowExecution;
|
||||
const workflowExecutionData = workflowExecutionStateStore.value.activeExecution;
|
||||
const lastRunData = workflowExecutionData?.data?.resultData.runData[node.value?.name];
|
||||
if (!lastRunData) return undefined;
|
||||
return lastRunData[0];
|
||||
|
||||
@@ -56,6 +56,8 @@ import {
|
||||
injectWorkflowDocumentStore,
|
||||
type WorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import type { IExecutionResponse } from '@/features/execution/executions/executions.types';
|
||||
|
||||
export type ResolveParameterOptions = {
|
||||
targetItem?: TargetItem;
|
||||
@@ -89,10 +91,10 @@ export async function resolveParameter<T = IDataObject>(
|
||||
}
|
||||
: opts_;
|
||||
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowObject = workflowDocumentStore.getWorkflowObjectAccessorSnapshot();
|
||||
const connections = workflowDocumentStore.connectionsBySourceNode;
|
||||
const executionData = workflowsStore.workflowExecutionData;
|
||||
const executionData = useWorkflowExecutionStateStore(workflowDocumentId)
|
||||
.activeExecution as IExecutionResponse | null;
|
||||
const pinData = workflowDocumentStore.getPinDataSnapshot();
|
||||
|
||||
let itemIndex = opts?.targetItem?.itemIndex || 0;
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { STORES } from '@n8n/stores';
|
||||
import {
|
||||
createTestNode,
|
||||
createTestNodeProperties,
|
||||
createTestWorkflow,
|
||||
mockNodeTypeDescription,
|
||||
} from '@/__tests__/mocks';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { SET_NODE_TYPE } from '@/app/constants';
|
||||
import type { INodeUi, IWorkflowDb } from '@/Interface';
|
||||
import { useWorkflowNormalization } from '@/app/composables/useWorkflowNormalization';
|
||||
|
||||
describe('useWorkflowNormalization', () => {
|
||||
const workflowId = 'test';
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(
|
||||
createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.NODE_TYPES]: {},
|
||||
[STORES.WORKFLOWS]: {
|
||||
workflowId,
|
||||
workflow: mock<IWorkflowDb>({
|
||||
id: workflowId,
|
||||
nodes: [],
|
||||
connections: {},
|
||||
tags: [],
|
||||
usedCredentials: [],
|
||||
}),
|
||||
},
|
||||
[STORES.SETTINGS]: {
|
||||
settings: {
|
||||
enterprise: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('normalizeWorkflowData', () => {
|
||||
it('should drop nodes with a missing type and sanitize their connections', () => {
|
||||
const validNode = createTestNode({ name: 'Start' });
|
||||
const invalidNode = createTestNode({ name: 'Missing', type: '' });
|
||||
const targetNode = createTestNode({ name: 'End' });
|
||||
const workflow = createTestWorkflow({
|
||||
nodes: [validNode, invalidNode, targetNode],
|
||||
connections: {
|
||||
Start: {
|
||||
main: [
|
||||
[
|
||||
{ node: 'End', type: 'main', index: 0 },
|
||||
{ node: 'Missing', type: 'main', index: 0 },
|
||||
],
|
||||
],
|
||||
},
|
||||
Missing: {
|
||||
main: [[{ node: 'End', type: 'main', index: 0 }]],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { normalizeWorkflowData } = useWorkflowNormalization();
|
||||
const { nodes, connections } = normalizeWorkflowData(workflow);
|
||||
|
||||
expect(nodes.map((node) => node.name)).toEqual(['Start', 'End']);
|
||||
expect(connections).toEqual({
|
||||
Start: {
|
||||
main: [[{ node: 'End', type: 'main', index: 0 }]],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should default position to [0, 0] for nodes with missing position', () => {
|
||||
const { position: _, ...nodeWithoutPosition } = createTestNode({ name: 'Start' });
|
||||
const workflow = createTestWorkflow({
|
||||
nodes: [nodeWithoutPosition as INodeUi],
|
||||
connections: {},
|
||||
});
|
||||
|
||||
const { normalizeWorkflowData } = useWorkflowNormalization();
|
||||
const { nodes } = normalizeWorkflowData(workflow);
|
||||
|
||||
expect(nodes).toEqual([expect.objectContaining({ name: 'Start', position: [0, 0] })]);
|
||||
});
|
||||
|
||||
it('should resolve node parameters from the node type description for installed nodes', () => {
|
||||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
const type = SET_NODE_TYPE;
|
||||
const version = 1;
|
||||
nodeTypesStore.nodeTypes = {
|
||||
[type]: {
|
||||
[version]: mockNodeTypeDescription({
|
||||
name: type,
|
||||
version,
|
||||
properties: [
|
||||
createTestNodeProperties({
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const workflow = createTestWorkflow({
|
||||
nodes: [createTestNode({ type, typeVersion: version })],
|
||||
connections: {},
|
||||
});
|
||||
|
||||
const { normalizeWorkflowData } = useWorkflowNormalization();
|
||||
const { nodes } = normalizeWorkflowData(workflow);
|
||||
|
||||
expect(nodes).toEqual([expect.objectContaining({ parameters: { value: true } })]);
|
||||
});
|
||||
|
||||
it('should leave parameters untouched for not-installed node types', () => {
|
||||
const workflow = createTestWorkflow({
|
||||
nodes: [
|
||||
createTestNode({
|
||||
type: 'n8n-nodes-community.unknown',
|
||||
parameters: { custom: 'value' },
|
||||
}),
|
||||
],
|
||||
connections: {},
|
||||
});
|
||||
|
||||
const { normalizeWorkflowData } = useWorkflowNormalization();
|
||||
const { nodes } = normalizeWorkflowData(workflow);
|
||||
|
||||
expect(nodes).toEqual([expect.objectContaining({ parameters: { custom: 'value' } })]);
|
||||
});
|
||||
|
||||
it('should not mutate the input nodes array', () => {
|
||||
const node = createTestNode({ name: 'Start' });
|
||||
const workflow = createTestWorkflow({ nodes: [node], connections: {} });
|
||||
const inputNodes = workflow.nodes;
|
||||
|
||||
const { normalizeWorkflowData } = useWorkflowNormalization();
|
||||
const { nodes } = normalizeWorkflowData(workflow);
|
||||
|
||||
expect(workflow.nodes).toBe(inputNodes);
|
||||
expect(nodes).not.toBe(inputNodes);
|
||||
expect(nodes[0]).not.toBe(node);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requireNodeTypeDescription', () => {
|
||||
it('should return a fallback description for unknown node types', () => {
|
||||
const { requireNodeTypeDescription } = useWorkflowNormalization();
|
||||
|
||||
const result = requireNodeTypeDescription('unknown-type', 2);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
displayName: 'unknown-type',
|
||||
name: 'unknown-type',
|
||||
version: 2,
|
||||
properties: [],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the registered description for known node types', () => {
|
||||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
const type = SET_NODE_TYPE;
|
||||
const version = 1;
|
||||
const description = mockNodeTypeDescription({ name: type, version });
|
||||
nodeTypesStore.nodeTypes = { [type]: { [version]: description } };
|
||||
|
||||
const { requireNodeTypeDescription } = useWorkflowNormalization();
|
||||
|
||||
expect(requireNodeTypeDescription(type, version)).toBe(description);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { IConnections, INodeTypeDescription } from 'n8n-workflow';
|
||||
import { NodeHelpers, resolveNodeWebhookId } from 'n8n-workflow';
|
||||
import type { INodeUi, IWorkflowDb } from '@/Interface';
|
||||
import { FORM_TRIGGER_NODE_TYPE, MCP_TRIGGER_NODE_TYPE, WEBHOOK_NODE_TYPE } from '@/app/constants';
|
||||
import { ensureNodePosition, sanitizeConnections } from '@/app/utils/workflowUtils';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
|
||||
|
||||
export interface NormalizedWorkflowData {
|
||||
nodes: INodeUi[];
|
||||
connections: IConnections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Node and connection normalization shared by every surface that hydrates a
|
||||
* workflow document store from raw workflow data (editor workspace
|
||||
* initialization, execution preview, ...).
|
||||
*
|
||||
* Reads from the node types and credentials stores but never writes global
|
||||
* workflow state — safe to use against any document store instance.
|
||||
*/
|
||||
export function useWorkflowNormalization() {
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
|
||||
function requireNodeTypeDescription(
|
||||
type: INodeUi['type'],
|
||||
version?: INodeUi['typeVersion'],
|
||||
): INodeTypeDescription {
|
||||
return (
|
||||
nodeTypesStore.getNodeType(type, version) ??
|
||||
nodeTypesStore.communityNodeType(type)?.nodeDescription ?? {
|
||||
properties: [],
|
||||
displayName: type,
|
||||
name: type,
|
||||
group: [],
|
||||
description: '',
|
||||
version: version ?? 1,
|
||||
defaults: {},
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function resolveNodeParameters(node: INodeUi, nodeTypeDescription: INodeTypeDescription) {
|
||||
const nodeParameters = NodeHelpers.getNodeParameters(
|
||||
nodeTypeDescription?.properties ?? [],
|
||||
node.parameters,
|
||||
true,
|
||||
false,
|
||||
node,
|
||||
nodeTypeDescription,
|
||||
);
|
||||
node.parameters = nodeParameters ?? {};
|
||||
}
|
||||
|
||||
function resolveNodeWebhook(node: INodeUi, nodeTypeDescription: INodeTypeDescription) {
|
||||
resolveNodeWebhookId(node, nodeTypeDescription);
|
||||
|
||||
// if it's a webhook and the path is empty set the UUID as the default path
|
||||
if (
|
||||
[WEBHOOK_NODE_TYPE, FORM_TRIGGER_NODE_TYPE, MCP_TRIGGER_NODE_TYPE].includes(node.type) &&
|
||||
node.parameters.path === ''
|
||||
) {
|
||||
node.parameters.path = node.webhookId as string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes raw workflow data for rendering: drops nodes with a missing
|
||||
* type, coerces node positions, matches credentials, resolves parameters
|
||||
* and webhook ids for installed node types, and sanitizes connections
|
||||
* against the surviving node names.
|
||||
*/
|
||||
function normalizeWorkflowData(
|
||||
data: Pick<IWorkflowDb, 'nodes' | 'connections'>,
|
||||
): NormalizedWorkflowData {
|
||||
// Filter out nodes with missing type to prevent canvas rendering crashes
|
||||
const validNodes = data.nodes
|
||||
.filter((node) => !!node.type)
|
||||
.map((node) => ({ ...node, position: ensureNodePosition(node.position) }));
|
||||
const validNodeNames = validNodes.map((node) => node.name);
|
||||
|
||||
validNodes.forEach((node) => {
|
||||
const nodeTypeDescription = requireNodeTypeDescription(node.type, node.typeVersion);
|
||||
const isInstalledNode = nodeTypesStore.getIsNodeInstalled(node.type);
|
||||
nodeHelpers.matchCredentials(node);
|
||||
// skip this step because nodeTypeDescription is missing for unknown nodes
|
||||
if (isInstalledNode) {
|
||||
resolveNodeParameters(node, nodeTypeDescription);
|
||||
resolveNodeWebhook(node, nodeTypeDescription);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
nodes: validNodes,
|
||||
connections: sanitizeConnections(data.connections, validNodeNames),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
requireNodeTypeDescription,
|
||||
resolveNodeParameters,
|
||||
resolveNodeWebhook,
|
||||
normalizeWorkflowData,
|
||||
};
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import type { TelemetryContext } from '@/app/types/telemetry';
|
||||
import type { useExecutionDataStore } from '@/app/stores/executionData.store';
|
||||
import type { WorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import type { CanvasRenderData } from '@/features/workflows/canvas/canvas.utils';
|
||||
import type { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
|
||||
export const WorkflowIdKey = 'workflowId' as unknown as InjectionKey<ComputedRef<string>>;
|
||||
export const CanvasKey = 'canvas' as unknown as InjectionKey<CanvasInjectionData>;
|
||||
@@ -26,9 +25,10 @@ export const WorkflowDocumentStoreKey: InjectionKey<ShallowRef<WorkflowDocumentS
|
||||
export const ExecutionDataStoreKey: InjectionKey<
|
||||
ShallowRef<ReturnType<typeof useExecutionDataStore> | null>
|
||||
> = Symbol('ExecutionDataStore');
|
||||
export const WorkflowExecutionStateStoreKey: InjectionKey<
|
||||
ShallowRef<ReturnType<typeof useWorkflowExecutionStateStore> | null>
|
||||
> = Symbol('WorkflowExecutionStateStore');
|
||||
// NOTE: there is intentionally no injection key for the workflow-execution-state
|
||||
// store — it shares its identity with the workflow document store and is always
|
||||
// derived from it via injectWorkflowExecutionStateStore(), so a subtree's
|
||||
// document scope and execution scope can never diverge.
|
||||
export const CanvasRenderDataKey: InjectionKey<Ref<CanvasRenderData>> = Symbol('CanvasRenderData');
|
||||
export const ChatHubToolContextKey: InjectionKey<boolean> = Symbol('ChatHubToolContext');
|
||||
export const AiBuilderScrollToBottomKey: InjectionKey<() => void> = Symbol('ChatScrollToBottom');
|
||||
|
||||
@@ -8,10 +8,15 @@
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { setActivePinia, createPinia, getActivePinia } from 'pinia';
|
||||
import { defineComponent, provide, shallowRef } from 'vue';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import {
|
||||
useWorkflowExecutionStateStore,
|
||||
getWorkflowExecutionStateStoreId,
|
||||
disposeWorkflowExecutionStateStore,
|
||||
injectWorkflowExecutionStateStore,
|
||||
} from '@/app/stores/workflowExecutionState.store';
|
||||
import {
|
||||
createWorkflowDocumentId,
|
||||
@@ -1396,4 +1401,50 @@ describe('workflowExecutionState.store', () => {
|
||||
expect(executionStateStore.executionWaitingForNextByNodeId.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('injectWorkflowExecutionStateStore', () => {
|
||||
function renderWithInjector({ providedWorkflowId }: { providedWorkflowId?: string }) {
|
||||
let injected!: ReturnType<typeof injectWorkflowExecutionStateStore>;
|
||||
|
||||
// inject() only resolves provides from ancestor components, so the
|
||||
// provide must live in a parent component.
|
||||
const ChildComponent = defineComponent({
|
||||
setup() {
|
||||
injected = injectWorkflowExecutionStateStore();
|
||||
},
|
||||
template: '<div />',
|
||||
});
|
||||
|
||||
const ParentComponent = defineComponent({
|
||||
components: { ChildComponent },
|
||||
setup() {
|
||||
if (providedWorkflowId) {
|
||||
const scopedDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(providedWorkflowId, 'scoped'),
|
||||
);
|
||||
provide(WorkflowDocumentStoreKey, shallowRef(scopedDocumentStore));
|
||||
}
|
||||
},
|
||||
template: '<ChildComponent />',
|
||||
});
|
||||
|
||||
createComponentRenderer(ParentComponent)();
|
||||
|
||||
return injected;
|
||||
}
|
||||
|
||||
it('resolves the execution-state store of the provided workflow document', () => {
|
||||
const injected = renderWithInjector({ providedWorkflowId: 'scoped-workflow' });
|
||||
|
||||
expect(injected.value.documentId).toBe(createWorkflowDocumentId('scoped-workflow', 'scoped'));
|
||||
});
|
||||
|
||||
it('falls back to the global workflow id when nothing is provided', () => {
|
||||
useWorkflowsStore().setWorkflowId('global-workflow');
|
||||
|
||||
const injected = renderWithInjector({});
|
||||
|
||||
expect(injected.value.documentId).toBe(createWorkflowDocumentId('global-workflow'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import { STORES } from '@n8n/stores';
|
||||
import {
|
||||
computed,
|
||||
effectScope,
|
||||
inject,
|
||||
onScopeDispose,
|
||||
readonly,
|
||||
ref,
|
||||
@@ -12,13 +11,18 @@ import {
|
||||
} from 'vue';
|
||||
import { createEventHook } from '@vueuse/core';
|
||||
import { structuralComputed } from '@n8n/composables/structuralComputed';
|
||||
import type { ExecutionStatus, ExecutionSummary, IRunExecutionData, ITaskData } from 'n8n-workflow';
|
||||
import type {
|
||||
ExecutionStatus,
|
||||
ExecutionSummary,
|
||||
IRunExecutionData,
|
||||
ITaskData,
|
||||
ITaskStartedData,
|
||||
} from 'n8n-workflow';
|
||||
import type { NodeExecuteBefore } from '@n8n/api-types/push/execution';
|
||||
import type {
|
||||
IExecutionResponse,
|
||||
IExecutionsStopData,
|
||||
} from '@/features/execution/executions/executions.types';
|
||||
import { WorkflowExecutionStateStoreKey } from '@/app/constants/injectionKeys';
|
||||
import { IN_PROGRESS_EXECUTION_ID } from '@/app/constants/placeholders';
|
||||
import { useExecutingNode } from '@/app/composables/useExecutingNode';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
@@ -27,7 +31,11 @@ import {
|
||||
disposeExecutionDataStore,
|
||||
useExecutionDataStore,
|
||||
} from './executionData.store';
|
||||
import { useWorkflowDocumentStore, type WorkflowDocumentId } from './workflowDocument.store';
|
||||
import {
|
||||
injectWorkflowDocumentStore,
|
||||
useWorkflowDocumentStore,
|
||||
type WorkflowDocumentId,
|
||||
} from './workflowDocument.store';
|
||||
import { useDocumentTitle } from '@/app/composables/useDocumentTitle';
|
||||
import { clearPopupWindowState } from '@/features/execution/executions/executions.utils';
|
||||
import { CHANGE_ACTION } from './workflowDocument/types';
|
||||
@@ -166,16 +174,26 @@ export function useWorkflowExecutionStateStore(id: WorkflowDocumentId) {
|
||||
* - `activeExecutionId === undefined` and `displayedExecutionId === string`
|
||||
* -> the displayed executionData store (preserved after active is cleared)
|
||||
* - otherwise null
|
||||
*
|
||||
* Typed as a mutable `IExecutionResponse` for consumers (the executionData
|
||||
* store exposes a readonly ref); treat it as read-only — all writes go
|
||||
* through the store actions.
|
||||
*/
|
||||
const activeExecution = computed(() => {
|
||||
const activeExecution = computed<IExecutionResponse | null>(() => {
|
||||
if (activeExecutionId.value === null) return pendingExecution.value;
|
||||
if (typeof activeExecutionId.value === 'string') {
|
||||
return useExecutionDataStore(createExecutionDataId(activeExecutionId.value)).execution;
|
||||
}
|
||||
if (typeof displayedExecutionId.value === 'string') {
|
||||
return useExecutionDataStore(createExecutionDataId(displayedExecutionId.value)).execution;
|
||||
}
|
||||
return null;
|
||||
const executionId =
|
||||
typeof activeExecutionId.value === 'string'
|
||||
? activeExecutionId.value
|
||||
: typeof displayedExecutionId.value === 'string'
|
||||
? displayedExecutionId.value
|
||||
: undefined;
|
||||
if (executionId === undefined) return null;
|
||||
const executionDataStore = useExecutionDataStore(createExecutionDataId(executionId));
|
||||
// Track the timestamp so in-place mutations that preserve the execution
|
||||
// object reference still propagate to consumers (same defensive pattern
|
||||
// as `activeExecutionRunData`).
|
||||
void executionDataStore.executionResultDataLastUpdate;
|
||||
return executionDataStore.execution as IExecutionResponse | null;
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -214,13 +232,18 @@ export function useWorkflowExecutionStateStore(id: WorkflowDocumentId) {
|
||||
const activeExecutionStartedData = computed(() => {
|
||||
const executionId = getResolvedActiveExecutionId();
|
||||
if (!executionId) return undefined;
|
||||
return useExecutionDataStore(createExecutionDataId(executionId)).executionStartedData;
|
||||
// Mutable-typed for consumers (the executionData store exposes a
|
||||
// readonly ref); treat it as read-only.
|
||||
return useExecutionDataStore(createExecutionDataId(executionId)).executionStartedData as
|
||||
| [executionId: string, data: { [nodeName: string]: ITaskStartedData[] }]
|
||||
| undefined;
|
||||
});
|
||||
|
||||
const activeExecutionPairedItemMappings = computed(() => {
|
||||
const executionId = getResolvedActiveExecutionId();
|
||||
if (!executionId) return {};
|
||||
return useExecutionDataStore(createExecutionDataId(executionId)).executionPairedItemMappings;
|
||||
return useExecutionDataStore(createExecutionDataId(executionId))
|
||||
.executionPairedItemMappings as Record<string, Set<string>>;
|
||||
});
|
||||
|
||||
const activeExecutionResultDataLastUpdate = computed(() => {
|
||||
@@ -289,10 +312,13 @@ export function useWorkflowExecutionStateStore(id: WorkflowDocumentId) {
|
||||
return useExecutionDataStore(createExecutionDataId(executionId)).executionWaitingByNodeId;
|
||||
});
|
||||
|
||||
const lastSuccessfulExecution = computed(() => {
|
||||
const lastSuccessfulExecution = computed<IExecutionResponse | null>(() => {
|
||||
const lid = lastSuccessfulExecutionId.value;
|
||||
if (!lid) return null;
|
||||
return useExecutionDataStore(createExecutionDataId(lid)).execution;
|
||||
// Mutable-typed for consumers (the executionData store exposes a
|
||||
// readonly ref); treat it as read-only.
|
||||
return useExecutionDataStore(createExecutionDataId(lid))
|
||||
.execution as IExecutionResponse | null;
|
||||
});
|
||||
|
||||
const isWorkflowRunning = computed(() => {
|
||||
@@ -897,9 +923,20 @@ export function disposeWorkflowExecutionStateStore(
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the active workflow-execution-state store from the component tree.
|
||||
* Returns null when not within a context that has provided the store.
|
||||
* Resolves the workflow-execution-state store for the current workflow
|
||||
* document scope.
|
||||
*
|
||||
* There is deliberately no separate provide for this store: the workflow
|
||||
* document store (`WorkflowDocumentStoreKey`) is the single provided source
|
||||
* of truth for a subtree's scope, and the execution-state store shares its
|
||||
* identity (same `WorkflowDocumentId`). Deriving from the injected document
|
||||
* store keeps the two from ever pointing at different scopes. Falls back to
|
||||
* the global workflow id outside any provide tree, exactly like
|
||||
* `injectWorkflowDocumentStore()`.
|
||||
*/
|
||||
export function injectWorkflowExecutionStateStore() {
|
||||
return inject(WorkflowExecutionStateStoreKey, null);
|
||||
export function injectWorkflowExecutionStateStore(): ComputedRef<
|
||||
ReturnType<typeof useWorkflowExecutionStateStore>
|
||||
> {
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
return computed(() => useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId));
|
||||
}
|
||||
|
||||
+5
-16
@@ -1,24 +1,13 @@
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import type { INode, IRunExecutionData } from 'n8n-workflow';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import type { INode } from 'n8n-workflow';
|
||||
import { computed, type ComputedRef } from 'vue';
|
||||
|
||||
export function useExecutionData({ node }: { node: ComputedRef<INode | undefined> }) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
|
||||
const workflowExecution = computed(() => {
|
||||
return workflowsStore.getWorkflowExecution;
|
||||
});
|
||||
const workflowExecution = computed(() => workflowExecutionStateStore.value.activeExecution);
|
||||
|
||||
const workflowRunData = computed(() => {
|
||||
if (workflowExecution.value === null) {
|
||||
return null;
|
||||
}
|
||||
const executionData: IRunExecutionData | undefined = workflowExecution.value.data;
|
||||
if (!executionData?.resultData?.runData) {
|
||||
return null;
|
||||
}
|
||||
return executionData.resultData.runData;
|
||||
});
|
||||
const workflowRunData = computed(() => workflowExecutionStateStore.value.activeExecutionRunData);
|
||||
|
||||
const hasNodeRun = computed(() => {
|
||||
return Boolean(
|
||||
|
||||
+34
-14
@@ -1,6 +1,10 @@
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store';
|
||||
import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store';
|
||||
import type { IExecutionResponse } from '@/features/execution/executions/executions.types';
|
||||
import { useExecutionRedaction } from './useExecutionRedaction';
|
||||
import { MODAL_CONFIRM } from '@/app/constants/modals';
|
||||
|
||||
@@ -24,6 +28,19 @@ vi.mock('vue-router', () => ({
|
||||
describe('useExecutionRedaction()', () => {
|
||||
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
|
||||
|
||||
// The composable reads the execution through the injected workflow document
|
||||
// scope; with nothing provided it falls back to the workflows store's
|
||||
// (empty) workflow id, so seed the execution-state store keyed by that id.
|
||||
// Testing pinia makes store getters writable at runtime; the cast makes
|
||||
// that writability visible to the type checker.
|
||||
function setActiveExecution(execution: IExecutionResponse | null) {
|
||||
const executionStateStore = mockedStore(
|
||||
useWorkflowExecutionStateStore,
|
||||
createWorkflowDocumentId(''),
|
||||
) as unknown as { activeExecution: IExecutionResponse | null };
|
||||
executionStateStore.activeExecution = execution;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
createTestingPinia({ stubActions: false });
|
||||
@@ -32,7 +49,7 @@ describe('useExecutionRedaction()', () => {
|
||||
|
||||
describe('computed properties', () => {
|
||||
it('should return isRedacted=false when no execution', () => {
|
||||
workflowsStore.getWorkflowExecution = null;
|
||||
setActiveExecution(null);
|
||||
const { isRedacted, canReveal, isDynamicCredentials } = useExecutionRedaction();
|
||||
|
||||
expect(isRedacted.value).toBe(false);
|
||||
@@ -41,11 +58,11 @@ describe('useExecutionRedaction()', () => {
|
||||
});
|
||||
|
||||
it('should return isRedacted=true when redactionInfo.isRedacted is true', () => {
|
||||
workflowsStore.getWorkflowExecution = {
|
||||
setActiveExecution({
|
||||
data: {
|
||||
redactionInfo: { isRedacted: true, reason: 'workflow_redaction_policy', canReveal: true },
|
||||
},
|
||||
} as never;
|
||||
} as never);
|
||||
|
||||
const { isRedacted, canReveal, isDynamicCredentials } = useExecutionRedaction();
|
||||
|
||||
@@ -55,7 +72,7 @@ describe('useExecutionRedaction()', () => {
|
||||
});
|
||||
|
||||
it('should detect dynamic credentials reason', () => {
|
||||
workflowsStore.getWorkflowExecution = {
|
||||
setActiveExecution({
|
||||
data: {
|
||||
redactionInfo: {
|
||||
isRedacted: true,
|
||||
@@ -63,7 +80,7 @@ describe('useExecutionRedaction()', () => {
|
||||
canReveal: false,
|
||||
},
|
||||
},
|
||||
} as never;
|
||||
} as never);
|
||||
|
||||
const { isDynamicCredentials, canReveal } = useExecutionRedaction();
|
||||
|
||||
@@ -74,12 +91,12 @@ describe('useExecutionRedaction()', () => {
|
||||
|
||||
describe('revealData', () => {
|
||||
it('should not fetch when user cancels confirmation', async () => {
|
||||
workflowsStore.getWorkflowExecution = {
|
||||
setActiveExecution({
|
||||
id: 'exec-123',
|
||||
data: {
|
||||
redactionInfo: { isRedacted: true, reason: 'workflow_redaction_policy', canReveal: true },
|
||||
},
|
||||
} as never;
|
||||
} as never);
|
||||
|
||||
confirm.mockResolvedValue('cancel');
|
||||
|
||||
@@ -91,34 +108,37 @@ describe('useExecutionRedaction()', () => {
|
||||
|
||||
it('should fetch with redactExecutionData=false on confirm', async () => {
|
||||
const revealedData = { resultData: { runData: { Node: [] } } };
|
||||
workflowsStore.getWorkflowExecution = {
|
||||
setActiveExecution({
|
||||
id: 'exec-123',
|
||||
data: {
|
||||
redactionInfo: { isRedacted: true, reason: 'workflow_redaction_policy', canReveal: true },
|
||||
},
|
||||
} as never;
|
||||
} as never);
|
||||
|
||||
confirm.mockResolvedValue(MODAL_CONFIRM);
|
||||
workflowsStore.fetchExecutionDataById = vi.fn().mockResolvedValue({
|
||||
data: revealedData,
|
||||
});
|
||||
|
||||
const executionDataStore = useExecutionDataStore(createExecutionDataId('exec-123'));
|
||||
const setExecutionRunDataSpy = vi.spyOn(executionDataStore, 'setExecutionRunData');
|
||||
|
||||
const { revealData } = useExecutionRedaction();
|
||||
await revealData();
|
||||
|
||||
expect(workflowsStore.fetchExecutionDataById).toHaveBeenCalledWith('exec-123', {
|
||||
redactExecutionData: false,
|
||||
});
|
||||
expect(workflowsStore.setWorkflowExecutionRunData).toHaveBeenCalledWith(revealedData);
|
||||
expect(setExecutionRunDataSpy).toHaveBeenCalledWith(revealedData);
|
||||
});
|
||||
|
||||
it('should show error toast when fetch fails', async () => {
|
||||
workflowsStore.getWorkflowExecution = {
|
||||
setActiveExecution({
|
||||
id: 'exec-123',
|
||||
data: {
|
||||
redactionInfo: { isRedacted: true, reason: 'workflow_redaction_policy', canReveal: true },
|
||||
},
|
||||
} as never;
|
||||
} as never);
|
||||
|
||||
const error = new Error('Forbidden');
|
||||
confirm.mockResolvedValue(MODAL_CONFIRM);
|
||||
@@ -131,11 +151,11 @@ describe('useExecutionRedaction()', () => {
|
||||
});
|
||||
|
||||
it('should not fetch when execution id is missing', async () => {
|
||||
workflowsStore.getWorkflowExecution = {
|
||||
setActiveExecution({
|
||||
data: {
|
||||
redactionInfo: { isRedacted: true, reason: 'workflow_redaction_policy', canReveal: true },
|
||||
},
|
||||
} as never;
|
||||
} as never);
|
||||
|
||||
confirm.mockResolvedValue(MODAL_CONFIRM);
|
||||
|
||||
|
||||
+17
-5
@@ -1,5 +1,8 @@
|
||||
import { computed, h } from 'vue';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { createExecutionDataId, useExecutionDataStore } from '@/app/stores/executionData.store';
|
||||
import { useMessage } from '@/app/composables/useMessage';
|
||||
import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
import { useToast } from '@/app/composables/useToast';
|
||||
@@ -9,12 +12,16 @@ import RevealDataWarning from '../components/RevealDataWarning.vue';
|
||||
|
||||
export function useExecutionRedaction() {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const message = useMessage();
|
||||
const telemetry = useTelemetry();
|
||||
const { showError } = useToast();
|
||||
const i18n = useI18n();
|
||||
|
||||
const redactionInfo = computed(() => workflowsStore.getWorkflowExecution?.data?.redactionInfo);
|
||||
const redactionInfo = computed(
|
||||
() => workflowExecutionStateStore.value.activeExecution?.data?.redactionInfo,
|
||||
);
|
||||
|
||||
const isRedacted = computed(() => redactionInfo.value?.isRedacted === true);
|
||||
|
||||
@@ -26,8 +33,8 @@ export function useExecutionRedaction() {
|
||||
|
||||
async function revealData() {
|
||||
telemetry.track('User clicked reveal data', {
|
||||
workflow_id: workflowsStore.workflowId,
|
||||
execution_id: workflowsStore.getWorkflowExecution?.id,
|
||||
workflow_id: workflowDocumentStore.value.workflowId,
|
||||
execution_id: workflowExecutionStateStore.value.activeExecution?.id,
|
||||
});
|
||||
|
||||
const warningContent = h(RevealDataWarning, {
|
||||
@@ -49,7 +56,7 @@ export function useExecutionRedaction() {
|
||||
|
||||
if (confirmed !== MODAL_CONFIRM) return;
|
||||
|
||||
const executionId = workflowsStore.getWorkflowExecution?.id;
|
||||
const executionId = workflowExecutionStateStore.value.activeExecution?.id;
|
||||
if (!executionId) return;
|
||||
|
||||
try {
|
||||
@@ -57,7 +64,12 @@ export function useExecutionRedaction() {
|
||||
redactExecutionData: false,
|
||||
});
|
||||
if (revealed?.data) {
|
||||
workflowsStore.setWorkflowExecutionRunData(revealed.data);
|
||||
// Write to the per-execution data store directly (keyed by the
|
||||
// revealed execution's id) so the update lands on the execution we
|
||||
// revealed regardless of which document scope we run under.
|
||||
useExecutionDataStore(createExecutionDataId(executionId)).setExecutionRunData(
|
||||
revealed.data,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error, i18n.baseText('ndv.redacted.revealError'));
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import LogsPanel from '@/features/execution/logs/components/LogsPanel.vue';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const hasExecutionData = computed(() => workflowsStore.workflowExecutionData);
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const hasExecutionData = computed(() => workflowExecutionStateStore.value.activeExecution);
|
||||
const canExecute = computed(() => route.query.canExecute === 'true');
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
|
||||
import { useRunWorkflow } from '@/app/composables/useRunWorkflow';
|
||||
import { VIEWS } from '@/app/constants';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import MessageWithButtons from '@n8n/chat/components/MessageWithButtons.vue';
|
||||
@@ -45,9 +45,7 @@ export function useChatState(
|
||||
const locale = useI18n();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowExecutionState = computed(() =>
|
||||
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
|
||||
);
|
||||
const workflowExecutionState = injectWorkflowExecutionStateStore();
|
||||
const rootStore = useRootStore();
|
||||
const logsStore = useLogsStore();
|
||||
const router = useRouter();
|
||||
@@ -322,7 +320,7 @@ export function useChatState(
|
||||
|
||||
const restoredChatMessages = computed(() =>
|
||||
restoreChatHistory(
|
||||
workflowsStore.workflowExecutionData,
|
||||
workflowExecutionState.value.activeExecution,
|
||||
locale.baseText('chat.window.chat.response.empty'),
|
||||
locale.baseText('chat.window.chat.response.redacted'),
|
||||
),
|
||||
|
||||
+3
-7
@@ -1,20 +1,16 @@
|
||||
import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
|
||||
export function useClearExecutionButtonVisible() {
|
||||
const route = useRoute();
|
||||
const sourceControlStore = useSourceControlStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowExecutionStateStore = computed(() =>
|
||||
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
|
||||
);
|
||||
const workflowExecutionData = computed(() => workflowsStore.workflowExecutionData);
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const workflowExecutionData = computed(() => workflowExecutionStateStore.value.activeExecution);
|
||||
const isWorkflowRunning = computed(() => workflowExecutionStateStore.value.isWorkflowRunning);
|
||||
const isReadOnlyRoute = computed(() => !!route?.meta?.readOnlyCanvas);
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
||||
+17
-22
@@ -2,11 +2,8 @@ import { watch, computed, ref, type ComputedRef } from 'vue';
|
||||
import type { IExecutionResponse } from '@/features/execution/executions/executions.types';
|
||||
import { Workflow, type IRunExecutionData, type ITaskStartedData } from 'n8n-workflow';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import {
|
||||
createWorkflowDocumentId,
|
||||
injectWorkflowDocumentStore,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
|
||||
import {
|
||||
copyExecutionData,
|
||||
@@ -36,6 +33,8 @@ export function useLogsExecutionData({ isEnabled, filter }: UseLogsExecutionData
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const currentExecution = computed(() => workflowExecutionStateStore.value.activeExecution);
|
||||
const toast = useToast();
|
||||
|
||||
const state = ref<
|
||||
@@ -43,8 +42,8 @@ export function useLogsExecutionData({ isEnabled, filter }: UseLogsExecutionData
|
||||
| undefined
|
||||
>();
|
||||
const updateInterval = computed(() =>
|
||||
workflowsStore.workflowExecutionData?.status === 'running' &&
|
||||
Object.keys(workflowsStore.workflowExecutionData.data?.resultData.runData ?? {}).length > 1
|
||||
currentExecution.value?.status === 'running' &&
|
||||
Object.keys(currentExecution.value.data?.resultData.runData ?? {}).length > 1
|
||||
? LOGS_EXECUTION_DATA_THROTTLE_DURATION
|
||||
: 0,
|
||||
);
|
||||
@@ -107,14 +106,10 @@ export function useLogsExecutionData({ isEnabled, filter }: UseLogsExecutionData
|
||||
|
||||
function resetExecutionData() {
|
||||
state.value = undefined;
|
||||
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId).setWorkflowExecutionData(
|
||||
null,
|
||||
);
|
||||
workflowExecutionStateStore.value.setWorkflowExecutionData(null);
|
||||
nodeHelpers.updateNodesExecutionIssues();
|
||||
// Clear partial execution destination to allow full workflow execution
|
||||
useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
).setChatPartialExecutionDestinationNode(null);
|
||||
workflowExecutionStateStore.value.setChatPartialExecutionDestinationNode(null);
|
||||
void workflowsStore.fetchLastSuccessfulExecution();
|
||||
}
|
||||
|
||||
@@ -146,20 +141,20 @@ export function useLogsExecutionData({ isEnabled, filter }: UseLogsExecutionData
|
||||
watch(
|
||||
// Fields that should trigger update
|
||||
[
|
||||
() => workflowsStore.workflowExecutionData?.id,
|
||||
() => workflowsStore.workflowExecutionData?.workflowData.id,
|
||||
() => workflowsStore.workflowExecutionData?.status,
|
||||
() => workflowsStore.workflowExecutionResultDataLastUpdate,
|
||||
() => workflowsStore.workflowExecutionStartedData,
|
||||
() => currentExecution.value?.id,
|
||||
() => currentExecution.value?.workflowData.id,
|
||||
() => currentExecution.value?.status,
|
||||
() => workflowExecutionStateStore.value.activeExecutionResultDataLastUpdate,
|
||||
() => workflowExecutionStateStore.value.activeExecutionStartedData,
|
||||
],
|
||||
useThrottleFn(
|
||||
([executionId], [previousExecutionId]) => {
|
||||
state.value =
|
||||
workflowsStore.workflowExecutionData === null
|
||||
currentExecution.value === null
|
||||
? undefined
|
||||
: {
|
||||
response: copyExecutionData(workflowsStore.workflowExecutionData),
|
||||
startData: workflowsStore.workflowExecutionStartedData?.[1] ?? {},
|
||||
response: copyExecutionData(currentExecution.value),
|
||||
startData: workflowExecutionStateStore.value.activeExecutionStartedData?.[1] ?? {},
|
||||
};
|
||||
|
||||
if (executionId !== previousExecutionId) {
|
||||
@@ -176,7 +171,7 @@ export function useLogsExecutionData({ isEnabled, filter }: UseLogsExecutionData
|
||||
);
|
||||
|
||||
watch(
|
||||
() => workflowsStore.workflowId,
|
||||
() => workflowDocumentStore.value.workflowId,
|
||||
() => {
|
||||
resetExecutionData();
|
||||
},
|
||||
|
||||
@@ -14,7 +14,6 @@ import { setActivePinia } from 'pinia';
|
||||
import { computed, shallowRef } from 'vue';
|
||||
import { WorkflowIdKey } from '@/app/constants/injectionKeys';
|
||||
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import {
|
||||
injectWorkflowDocumentStore,
|
||||
useWorkflowDocumentStore,
|
||||
@@ -67,7 +66,6 @@ const render = (props: Partial<Props> = {}, pinData?: INodeExecutionData[], runD
|
||||
setActivePinia(pinia);
|
||||
|
||||
const workflow = createTestWorkflow({ nodes, connections });
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id));
|
||||
workflowDocumentStore.hydrate(workflow);
|
||||
@@ -79,11 +77,10 @@ const render = (props: Partial<Props> = {}, pinData?: INodeExecutionData[], runD
|
||||
}
|
||||
|
||||
if (runData) {
|
||||
// The component reads run data via `workflowsStore.getWorkflowExecution`, which
|
||||
// resolves through the execution-state store keyed by `workflowsStore.workflowId`.
|
||||
useWorkflowExecutionStateStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
).setWorkflowExecutionData({
|
||||
// The component reads run data via `injectWorkflowExecutionStateStore()`,
|
||||
// which resolves through the execution-state store keyed by the injected
|
||||
// workflow document's id.
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId(workflow.id)).setWorkflowExecutionData({
|
||||
id: '',
|
||||
workflowData: {
|
||||
id: '',
|
||||
|
||||
@@ -10,9 +10,8 @@ import {
|
||||
} from '@/app/constants';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { waitingNodeTooltip } from '@/features/execution/executions/executions.utils';
|
||||
import { useExecutionRedaction } from '@/features/execution/executions/composables/useExecutionRedaction';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
@@ -106,11 +105,9 @@ const inputModes = [
|
||||
|
||||
const workflowId = useInjectWorkflowId();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowExecutionStateStore = computed(() =>
|
||||
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
|
||||
);
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const workflowExecution = computed(() => workflowExecutionStateStore.value.activeExecution);
|
||||
const router = useRouter();
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
const { canReveal, isDynamicCredentials, revealData } = useExecutionRedaction();
|
||||
@@ -135,15 +132,12 @@ const rootNode = computed(() => {
|
||||
});
|
||||
|
||||
const hasRootNodeRun = computed(() => {
|
||||
return !!(
|
||||
rootNode.value && workflowsStore.getWorkflowExecution?.data?.resultData.runData[rootNode.value]
|
||||
);
|
||||
return !!(rootNode.value && workflowExecution.value?.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]
|
||||
activeNode.value && workflowExecution.value?.data?.resultData.runData[activeNode.value.name]
|
||||
? 'debugging'
|
||||
: 'mapping',
|
||||
);
|
||||
@@ -199,7 +193,7 @@ const isExecutingPrevious = computed(() => {
|
||||
if (!workflowExecutionStateStore.value.isWorkflowRunning) {
|
||||
return false;
|
||||
}
|
||||
const triggeredNode = workflowsStore.executedNode;
|
||||
const triggeredNode = workflowExecutionStateStore.value.activeExecutionExecutedNode;
|
||||
const executingNode = workflowExecutionStateStore.value.executingNode.executingNode;
|
||||
|
||||
if (
|
||||
@@ -271,7 +265,7 @@ const waitingMessage = computed(() => {
|
||||
const parentNode = parentNodes.value[0];
|
||||
if (!parentNode) return '';
|
||||
|
||||
const runData = workflowsStore.getWorkflowExecution?.data?.resultData?.runData;
|
||||
const runData = workflowExecution.value?.data?.resultData?.runData;
|
||||
const parentRunData = runData?.[parentNode.name]?.[0];
|
||||
|
||||
return waitingNodeTooltip(
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { NodeConnectionTypes, type IRunData } from 'n8n-workflow';
|
||||
import RunData from '@/features/ndv/runData/components/RunData.vue';
|
||||
import RunInfo from '@/features/ndv/runData/components/RunInfo.vue';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { injectNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import RunDataAi from '@/features/ndv/runData/components/ai/RunDataAi.vue';
|
||||
@@ -27,7 +26,7 @@ import { N8nIcon, N8nRadioButtons, N8nSpinner, N8nText } from '@n8n/design-syste
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { WORKFLOW_SETTINGS_MODAL_KEY } from '@/app/constants';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
// Types
|
||||
|
||||
type RunDataRef = InstanceType<typeof RunData>;
|
||||
@@ -79,11 +78,8 @@ const emit = defineEmits<{
|
||||
const workflowId = useInjectWorkflowId();
|
||||
const ndvStore = injectNDVStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowExecutionStateStore = computed(() =>
|
||||
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
|
||||
);
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const telemetry = useTelemetry();
|
||||
const i18n = useI18n();
|
||||
const activeNode = computed(() => ndvStore.value.activeNode);
|
||||
@@ -136,7 +132,9 @@ const hasAiMetadata = computed(() => {
|
||||
node.value.name,
|
||||
'ALL_NON_MAIN',
|
||||
);
|
||||
const resultData = connectedSubNodes.map(workflowsStore.getWorkflowResultDataByNodeName);
|
||||
const resultData = connectedSubNodes.map(
|
||||
workflowExecutionStateStore.value.getActiveExecutionRunDataByNodeName,
|
||||
);
|
||||
|
||||
return resultData && Array.isArray(resultData) && resultData.length > 0;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,16 @@ describe('TriggerPanel.vue', () => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
function setExecutedNode(executedNode: string) {
|
||||
// Testing pinia makes store getters writable at runtime; the cast makes
|
||||
// that writability visible to the type checker.
|
||||
const executionStateStore = mockedStore(
|
||||
useWorkflowExecutionStateStore,
|
||||
createWorkflowDocumentId('1'),
|
||||
) as unknown as { activeExecutionExecutedNode: string | undefined };
|
||||
executionStateStore.activeExecutionExecutedNode = executedNode;
|
||||
}
|
||||
|
||||
it('renders default state', () => {
|
||||
const { getByTestId } = renderComponent(TriggerPanel, {
|
||||
props: { nodeName: 'Webhook' },
|
||||
@@ -69,7 +79,7 @@ describe('TriggerPanel.vue', () => {
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId('1')).setExecutionWaitingForWebhook(
|
||||
true,
|
||||
);
|
||||
workflowsStore.executedNode = 'Webhook';
|
||||
setExecutedNode('Webhook');
|
||||
const { getByTestId } = renderComponent(TriggerPanel, {
|
||||
props: { nodeName: 'Webhook' },
|
||||
global: {
|
||||
@@ -85,7 +95,7 @@ describe('TriggerPanel.vue', () => {
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId('1')).setExecutionWaitingForWebhook(
|
||||
true,
|
||||
);
|
||||
workflowsStore.executedNode = 'OtherNode';
|
||||
setExecutedNode('OtherNode');
|
||||
const { queryByTestId } = renderComponent(TriggerPanel, {
|
||||
props: { nodeName: 'Webhook' },
|
||||
global: {
|
||||
@@ -101,7 +111,7 @@ describe('TriggerPanel.vue', () => {
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId('1')).setExecutionWaitingForWebhook(
|
||||
true,
|
||||
);
|
||||
workflowsStore.executedNode = 'ChildNode';
|
||||
setExecutedNode('ChildNode');
|
||||
vi.spyOn(workflowDocStore, 'getParentNodes').mockReturnValue(['Webhook']);
|
||||
const { getByTestId } = renderComponent(TriggerPanel, {
|
||||
props: { nodeName: 'Webhook' },
|
||||
@@ -118,7 +128,7 @@ describe('TriggerPanel.vue', () => {
|
||||
useWorkflowExecutionStateStore(createWorkflowDocumentId('1')).setExecutionWaitingForWebhook(
|
||||
true,
|
||||
);
|
||||
workflowsStore.executedNode = 'UnrelatedNode';
|
||||
setExecutedNode('UnrelatedNode');
|
||||
const { queryByTestId } = renderComponent(TriggerPanel, {
|
||||
props: { nodeName: 'Webhook' },
|
||||
global: {
|
||||
|
||||
@@ -15,8 +15,7 @@ import NodeExecuteButton from '@/app/components/NodeExecuteButton.vue';
|
||||
import CopyInput from '@/app/components/CopyInput.vue';
|
||||
import NodeIcon from '@/app/components/NodeIcon.vue';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { injectNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
@@ -55,11 +54,8 @@ const emit = defineEmits<{
|
||||
const workflowId = useInjectWorkflowId();
|
||||
const nodesTypeStore = useNodeTypesStore();
|
||||
const uiStore = useUIStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowExecutionStateStore = computed(() =>
|
||||
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
|
||||
);
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const ndvStore = injectNDVStore();
|
||||
|
||||
const router = useRouter();
|
||||
@@ -179,7 +175,7 @@ const isListeningForEvents = computed(() => {
|
||||
return false;
|
||||
}
|
||||
|
||||
const executedNode = workflowsStore.executedNode;
|
||||
const executedNode = workflowExecutionStateStore.value.activeExecutionExecutedNode;
|
||||
const isCurrentNodeExecuted = executedNode === props.nodeName;
|
||||
const isChildNodeExecuted = executedNode
|
||||
? (workflowDocumentStore?.value?.getParentNodes(executedNode).includes(props.nodeName) ?? false)
|
||||
@@ -191,7 +187,7 @@ const isListeningForEvents = computed(() => {
|
||||
const workflowRunning = computed(() => workflowExecutionStateStore.value.isWorkflowRunning);
|
||||
|
||||
const isActivelyPolling = computed(() => {
|
||||
const triggeredNode = workflowsStore.executedNode;
|
||||
const triggeredNode = workflowExecutionStateStore.value.activeExecutionExecutedNode;
|
||||
|
||||
return workflowRunning.value && isPollingNode.value && props.nodeName === triggeredNode;
|
||||
});
|
||||
|
||||
+1
@@ -112,6 +112,7 @@ vi.mock('vue-router', async () => {
|
||||
let ndvStore: ReturnType<typeof mockedStore<typeof useNDVStore>>;
|
||||
|
||||
const workflowDocumentStoreMock = {
|
||||
documentId: createWorkflowDocumentId(''),
|
||||
getChildNodes: vi.fn().mockReturnValue([]),
|
||||
getParentNodes: vi.fn().mockReturnValue([]),
|
||||
getParentNodesByDepth: vi.fn().mockReturnValue([]),
|
||||
|
||||
+3
-3
@@ -27,7 +27,7 @@ import {
|
||||
import { isFullExecutionResponse, isResourceMapperValue } from '@/app/utils/typeGuards';
|
||||
import { i18n as locale } from '@n8n/i18n';
|
||||
import { injectNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { useDocumentVisibility } from '@/app/composables/useDocumentVisibility';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
|
||||
@@ -49,7 +49,7 @@ type Props = {
|
||||
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const ndvStore = injectNDVStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const expressionLocalResolveCtx = inject(ExpressionLocalResolveContextSymbol, undefined);
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
@@ -140,7 +140,7 @@ async function checkStaleFields(): Promise<void> {
|
||||
|
||||
// Reload fields to map when node is executed
|
||||
watch(
|
||||
() => workflowsStore.getWorkflowExecution,
|
||||
() => workflowExecutionStateStore.value.activeExecution,
|
||||
async (data) => {
|
||||
if (
|
||||
data &&
|
||||
|
||||
+5
-10
@@ -2,7 +2,7 @@
|
||||
import { computed } from 'vue';
|
||||
import type { IBinaryData, IRunData } from 'n8n-workflow';
|
||||
import BinaryDataDisplayEmbed from './BinaryDataDisplayEmbed.vue';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
|
||||
@@ -17,18 +17,13 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const workflowRunData = computed<IRunData | null>(() => {
|
||||
const workflowExecution = workflowsStore.getWorkflowExecution;
|
||||
if (workflowExecution === null) {
|
||||
return null;
|
||||
}
|
||||
const executionData = workflowExecution.data;
|
||||
return executionData ? executionData.resultData.runData : null;
|
||||
});
|
||||
const workflowRunData = computed<IRunData | null>(
|
||||
() => workflowExecutionStateStore.value.activeExecutionRunData,
|
||||
);
|
||||
|
||||
const binaryData = computed<IBinaryData | null>(() => {
|
||||
if (
|
||||
|
||||
@@ -63,7 +63,7 @@ import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store';
|
||||
import { useCollaborationStore } from '@/features/collaboration/collaboration/collaboration.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { executionDataToJson } from '@/app/utils/nodeTypesUtils';
|
||||
import { getGenericHints } from '@/app/utils/nodeViewUtils';
|
||||
import { searchInObject } from '@/app/utils/objectUtils';
|
||||
@@ -232,7 +232,11 @@ const dataContainerRef = ref<HTMLDivElement>();
|
||||
const workflowId = useInjectWorkflowId();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const ndvStore = injectNDVStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const currentExecution = computed(() => workflowExecutionStateStore.value.activeExecution);
|
||||
const lastSuccessfulExecution = computed(
|
||||
() => workflowExecutionStateStore.value.lastSuccessfulExecution,
|
||||
);
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const sourceControlStore = useSourceControlStore();
|
||||
const collaborationStore = useCollaborationStore();
|
||||
@@ -295,7 +299,7 @@ const hasAnyDataAvailable = computed(() => {
|
||||
node.value?.disabled ||
|
||||
hasPreviewSchema.value ||
|
||||
hasAnyUpstreamExecuted.value ||
|
||||
!!workflowsStore.lastSuccessfulExecution
|
||||
!!lastSuccessfulExecution.value
|
||||
);
|
||||
});
|
||||
const isSingleNodeView = computed(() => !displaysMultipleNodes.value);
|
||||
@@ -314,7 +318,7 @@ const shouldShowSchemaView = computed(() => {
|
||||
return (
|
||||
hasNodeRun.value ||
|
||||
hasPreviewSchema.value ||
|
||||
(!hasNodeRun.value && (hasAnyUpstreamExecuted.value || workflowsStore.lastSuccessfulExecution))
|
||||
(!hasNodeRun.value && (hasAnyUpstreamExecuted.value || lastSuccessfulExecution.value))
|
||||
);
|
||||
});
|
||||
|
||||
@@ -420,7 +424,7 @@ const executionHints = computed(() => {
|
||||
});
|
||||
|
||||
const workflowExecution = computed(
|
||||
() => props.workflowExecution ?? workflowsStore.getWorkflowExecution?.data ?? undefined,
|
||||
() => props.workflowExecution ?? currentExecution.value?.data ?? undefined,
|
||||
);
|
||||
const workflowRunData = computed(() => {
|
||||
if (workflowExecution.value === undefined) {
|
||||
@@ -441,7 +445,7 @@ const isTrimmedManualExecutionDataItem = computed(() =>
|
||||
);
|
||||
|
||||
const isExecutionInTerminalState = computed(() =>
|
||||
isTerminalExecutionStatus(workflowsStore.getWorkflowExecution?.status ?? undefined),
|
||||
isTerminalExecutionStatus(currentExecution.value?.status ?? undefined),
|
||||
);
|
||||
|
||||
const isExecutionRedacted = computed(
|
||||
@@ -967,7 +971,7 @@ function enterEditMode({ origin }: EnterEditModeArgs) {
|
||||
: Object.keys(inputData ?? {}).length;
|
||||
|
||||
const lastSuccessfulExecutionItems = getOutputtedNodeItems(
|
||||
workflowsStore.lastSuccessfulExecution,
|
||||
lastSuccessfulExecution.value,
|
||||
node.value,
|
||||
);
|
||||
previousExecutionDataUsedInEditMode.value =
|
||||
@@ -2182,7 +2186,7 @@ defineExpose({ enterEditMode });
|
||||
:class="$style.schema"
|
||||
:compact="props.compact"
|
||||
:truncate-limit="props.truncateLimit"
|
||||
:preview-execution="workflowsStore.lastSuccessfulExecution"
|
||||
:preview-execution="lastSuccessfulExecution"
|
||||
@clear:search="onSearchClear"
|
||||
@execute="executeNode"
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useExternalHooks } from '@/app/composables/useExternalHooks';
|
||||
import type { INodeUi, IRunDataDisplayMode, ITableData } from '@/Interface';
|
||||
import { injectNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { getMappedExpression } from '@/app/utils/mappingUtils';
|
||||
import { getPairedItemId } from '@/app/utils/pairedItemUtils';
|
||||
@@ -76,7 +76,7 @@ const draggableRef = ref<DraggableRef>();
|
||||
const fixedColumnWidths = ref<number[] | undefined>();
|
||||
|
||||
const ndvStore = injectNDVStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
|
||||
const i18n = useI18n();
|
||||
@@ -90,7 +90,9 @@ const highlight = computed(() => ndvStore.value.highlightDraggables);
|
||||
|
||||
const canDraggableDrop = computed(() => ndvStore.value.canDraggableDrop);
|
||||
const draggableStickyPosition = computed(() => ndvStore.value.draggableStickyPos);
|
||||
const pairedItemMappings = computed(() => workflowsStore.workflowExecutionPairedItemMappings);
|
||||
const pairedItemMappings = computed(
|
||||
() => workflowExecutionStateStore.value.activeExecutionPairedItemMappings,
|
||||
);
|
||||
const tableData = computed(() => convertToTable(props.inputData));
|
||||
const collapsingColumnIndex = computed(() => {
|
||||
if (!props.collapsingColumnName) {
|
||||
|
||||
+11
-2
@@ -11,6 +11,7 @@ import { useChatPanelStore } from '@/features/ai/assistant/chatPanel.store';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
|
||||
const mockRouterResolve = vi.fn(() => ({
|
||||
@@ -238,7 +239,11 @@ describe('NodeErrorView.vue', () => {
|
||||
|
||||
it('opens new window when error has different workflow and execution IDs', async () => {
|
||||
mockWorkflowsStore.workflowId = 'current-workflow-id';
|
||||
mockWorkflowsStore.getWorkflowExecution = {
|
||||
const mockExecutionStateStore = mockedStore(
|
||||
useWorkflowExecutionStateStore,
|
||||
createWorkflowDocumentId('current-workflow-id'),
|
||||
) as unknown as { activeExecution: IExecutionResponse | null };
|
||||
mockExecutionStateStore.activeExecution = {
|
||||
id: 'current-execution-id',
|
||||
} as IExecutionResponse;
|
||||
|
||||
@@ -273,7 +278,11 @@ describe('NodeErrorView.vue', () => {
|
||||
it('sets active node name when error is in current workflow/execution', async () => {
|
||||
mockWorkflowsStore.workflowId = 'current-workflow-id';
|
||||
mockNDVStore = mockedStore(useNDVStore, createWorkflowDocumentId('current-workflow-id'));
|
||||
mockWorkflowsStore.getWorkflowExecution = {
|
||||
const mockExecutionStateStore = mockedStore(
|
||||
useWorkflowExecutionStateStore,
|
||||
createWorkflowDocumentId('current-workflow-id'),
|
||||
) as unknown as { activeExecution: IExecutionResponse | null };
|
||||
mockExecutionStateStore.activeExecution = {
|
||||
id: 'current-execution-id',
|
||||
} as IExecutionResponse;
|
||||
|
||||
|
||||
+3
-3
@@ -8,7 +8,7 @@ import { useToast } from '@/app/composables/useToast';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { injectNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { useEditorContext } from '@/app/composables/useEditorContext';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import type {
|
||||
IDataObject,
|
||||
@@ -54,13 +54,13 @@ const assistantHelpers = useAIAssistantHelpers();
|
||||
const workflowId = useInjectWorkflowId();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const ndvStore = injectNDVStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const rootStore = useRootStore();
|
||||
const assistantStore = useAssistantStore();
|
||||
const chatPanelStore = useChatPanelStore();
|
||||
const uiStore = useUIStore();
|
||||
|
||||
const executionId = computed(() => workflowsStore.getWorkflowExecution?.id);
|
||||
const executionId = computed(() => workflowExecutionStateStore.value.activeExecution?.id);
|
||||
|
||||
const displayCause = computed(() => {
|
||||
return JSON.stringify(props.error.cause ?? '').length < MAX_DISPLAY_DATA_SIZE;
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
createWorkflowDocumentId,
|
||||
type WorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { computed, inject, ref, type ShallowRef } from 'vue';
|
||||
import type { TelemetryNdvSource } from '@/app/types/telemetry';
|
||||
import { WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys';
|
||||
@@ -104,14 +105,17 @@ function defineNDVStore(id: NDVStoreId) {
|
||||
const highlightDraggables = ref(false);
|
||||
const lastSetActiveNodeSource = ref<TelemetryNdvSource>();
|
||||
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(id);
|
||||
const executionStateStore = useWorkflowExecutionStateStore(id);
|
||||
const activeNode = computed(() => {
|
||||
return workflowDocumentStore.getNodeByName(activeNodeName.value || '') ?? null;
|
||||
});
|
||||
|
||||
const ndvInputData = computed(() => {
|
||||
const executionData = workflowsStore.getWorkflowExecution;
|
||||
// Touch the timestamp so in-place runData mutations (which keep the
|
||||
// execution object reference) still propagate.
|
||||
void executionStateStore.activeExecutionResultDataLastUpdate;
|
||||
const executionData = executionStateStore.activeExecution;
|
||||
const inputNodeName: string | undefined = input.value.nodeName;
|
||||
const inputRunIndex: number = input.value.run ?? 0;
|
||||
const inputBranchIndex: number = input.value.branch ?? 0;
|
||||
|
||||
@@ -22,8 +22,7 @@ import {
|
||||
import type { DataPinningDiscoveryEvent } from '@/app/event-bus';
|
||||
import { dataPinningEventBus } from '@/app/event-bus';
|
||||
import { ndvEventBus } from '@/features/ndv/shared/ndv.eventBus';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { injectNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
@@ -69,11 +68,8 @@ const nodeHelpers = useNodeHelpers();
|
||||
const activeNode = computed(() => ndvStore.value.activeNode);
|
||||
const pinnedData = usePinnedData(activeNode);
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowExecutionStateStore = computed(() =>
|
||||
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
|
||||
);
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const deviceSupport = useDeviceSupport();
|
||||
const workflowId = useInjectWorkflowId();
|
||||
const telemetry = useTelemetry();
|
||||
@@ -219,7 +215,7 @@ const isActiveStickyNode = computed(
|
||||
() => !!ndvStore.value.activeNode && ndvStore.value.activeNode.type === STICKY_NODE_TYPE,
|
||||
);
|
||||
|
||||
const workflowExecution = computed(() => workflowsStore.getWorkflowExecution);
|
||||
const workflowExecution = computed(() => workflowExecutionStateStore.value.activeExecution);
|
||||
|
||||
const maxOutputRun = computed(() => {
|
||||
if (activeNode.value === null) {
|
||||
|
||||
@@ -31,8 +31,7 @@ import { ndvEventBus } from '../ndv.eventBus';
|
||||
import { injectNDVStore } from '../ndv.store';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useDeviceSupport } from '@n8n/composables/useDeviceSupport';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
@@ -71,7 +70,6 @@ const activeNode = computed(() => ndvStore.value.activeNode);
|
||||
const pinnedData = usePinnedData(activeNode);
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const uiStore = useUIStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const deviceSupport = useDeviceSupport();
|
||||
const workflowId = useInjectWorkflowId();
|
||||
@@ -220,7 +218,8 @@ const isActiveStickyNode = computed(
|
||||
() => !!ndvStore.value.activeNode && ndvStore.value.activeNode.type === STICKY_NODE_TYPE,
|
||||
);
|
||||
|
||||
const workflowExecution = computed(() => workflowsStore.getWorkflowExecution);
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const workflowExecution = computed(() => workflowExecutionStateStore.value.activeExecution);
|
||||
|
||||
const maxOutputRun = computed(() => {
|
||||
if (activeNode.value === null) {
|
||||
@@ -304,9 +303,7 @@ const outputPanelEditMode = computed(() => ndvStore.value.outputPanelEditMode);
|
||||
const isWorkflowRunning = computed(() => uiStore.isActionActive.workflowRunning);
|
||||
|
||||
const isExecutionWaitingForWebhook = computed(
|
||||
() =>
|
||||
useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId)
|
||||
.executionWaitingForWebhook,
|
||||
() => workflowExecutionStateStore.value.executionWaitingForWebhook,
|
||||
);
|
||||
|
||||
const blockUi = computed(() => isWorkflowRunning.value || isExecutionWaitingForWebhook.value);
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import { escape } from '../utils';
|
||||
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { isAllowedInDotNotation } from '@/features/shared/editors/plugins/codemirror/completions/utils';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
@@ -10,7 +10,7 @@ import { computed } from 'vue';
|
||||
|
||||
function useJsonFieldCompletions() {
|
||||
const i18n = useI18n();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const ndvStore = computed(() => useNDVStore(workflowDocumentStore.value.documentId));
|
||||
|
||||
@@ -273,7 +273,7 @@ function useJsonFieldCompletions() {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const runData: IRunData | null = workflowsStore.getWorkflowRunData;
|
||||
const runData: IRunData | null = workflowExecutionStateStore.value.activeExecutionRunData;
|
||||
|
||||
const nodeRunData = runData?.[nodeName];
|
||||
|
||||
|
||||
+7
-4
@@ -45,7 +45,7 @@ import { EditorView, type ViewUpdate } from '@codemirror/view';
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { useAutocompleteTelemetry } from '@/app/composables/useAutocompleteTelemetry';
|
||||
import { ignoreUpdateAnnotation } from '@/app/utils/forceParse';
|
||||
import {
|
||||
@@ -83,7 +83,7 @@ export const useExpressionEditor = ({
|
||||
}) => {
|
||||
const ndvStore = injectNDVStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const { isMacOs } = useDeviceSupport();
|
||||
const i18n = useI18n();
|
||||
@@ -419,7 +419,7 @@ export const useExpressionEditor = ({
|
||||
}
|
||||
} catch (error) {
|
||||
const hasRunData =
|
||||
!!workflowsStore.workflowExecutionData?.data?.resultData?.runData[
|
||||
!!workflowExecutionStateStore.value.activeExecutionRunData?.[
|
||||
ndvStore.value.activeNode?.name ?? ''
|
||||
];
|
||||
result.resolved = `[${getExpressionErrorMessage(error, workflowDocumentStore.value.getPinDataSnapshot(), hasRunData)}]`;
|
||||
@@ -524,7 +524,10 @@ export const useExpressionEditor = ({
|
||||
});
|
||||
|
||||
watch(
|
||||
[() => workflowsStore.getWorkflowExecution, () => workflowsStore.getWorkflowRunData],
|
||||
[
|
||||
() => workflowExecutionStateStore.value.activeExecution,
|
||||
() => workflowExecutionStateStore.value.activeExecutionRunData,
|
||||
],
|
||||
debouncedUpdateSegments,
|
||||
);
|
||||
|
||||
|
||||
+7
-4
@@ -4,7 +4,7 @@ import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
|
||||
import { autocompletableNodeNames } from '@/features/shared/editors/plugins/codemirror/completions/utils';
|
||||
import useEnvironmentsStore from '@/features/settings/environments.ee/environments.store';
|
||||
import { injectNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { forceParse } from '@/app/utils/forceParse';
|
||||
import { executionDataToJson } from '@/app/utils/nodeTypesUtils';
|
||||
@@ -33,7 +33,7 @@ export function useTypescript(
|
||||
) {
|
||||
const { getInputDataWithPinned, getSchemaForExecutionData } = useDataSchema();
|
||||
const ndvStore = injectNDVStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowExecutionStateStore = injectWorkflowExecutionStateStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const { debounce } = useDebounce();
|
||||
const activeNodeName =
|
||||
@@ -71,7 +71,7 @@ export function useTypescript(
|
||||
if (node) {
|
||||
const inputData: INodeExecutionData[] = getInputDataWithPinned(node);
|
||||
const schema = getSchemaForExecutionData(executionDataToJson(inputData), true);
|
||||
const execution = workflowsStore.getWorkflowExecution;
|
||||
const execution = workflowExecutionStateStore.value.activeExecution;
|
||||
const binaryData = useNodeHelpers()
|
||||
.getBinaryData(
|
||||
execution?.data?.resultData?.runData ?? null,
|
||||
@@ -131,7 +131,10 @@ export function useTypescript(
|
||||
}
|
||||
|
||||
watch(
|
||||
[() => workflowsStore.getWorkflowExecution, () => workflowsStore.getWorkflowRunData],
|
||||
[
|
||||
() => workflowExecutionStateStore.value.activeExecution,
|
||||
() => workflowExecutionStateStore.value.activeExecutionRunData,
|
||||
],
|
||||
debounce(onWorkflowDataChange, { debounceTime: 200, trailing: true }),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user