mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(editor): Open failed node in failed execution from sub-workflow node (#17076)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1523,6 +1523,7 @@
|
||||
"nodeView.showError.mounted2.message": "There was a problem initializing the workflow",
|
||||
"nodeView.showError.mounted2.title": "Init Problem",
|
||||
"nodeView.showError.openExecution.title": "Problem loading execution",
|
||||
"nodeView.showError.openExecution.node": "Problem opening node in execution",
|
||||
"nodeView.showError.openWorkflow.title": "Problem opening workflow",
|
||||
"nodeView.showError.stopExecution.title": "Problem stopping execution",
|
||||
"nodeView.showError.stopWaitingForWebhook.title": "Problem deleting test webhook",
|
||||
|
||||
@@ -1,30 +1,51 @@
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
|
||||
import NodeErrorView from '@/components/Error/NodeErrorView.vue';
|
||||
|
||||
import { reactive } from 'vue';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { NodeError } from 'n8n-workflow';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import type { IExecutionResponse } from '@/Interface';
|
||||
import NodeErrorView from '@/components/Error/NodeErrorView.vue';
|
||||
import { useAssistantStore } from '@/stores/assistant.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
|
||||
const renderComponent = createComponentRenderer(NodeErrorView);
|
||||
const mockRouterResolve = vi.fn(() => ({
|
||||
href: '',
|
||||
}));
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({
|
||||
resolve: mockRouterResolve,
|
||||
}),
|
||||
useRoute: () => reactive({ meta: {} }),
|
||||
RouterLink: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock window.open
|
||||
Object.defineProperty(window, 'open', {
|
||||
value: vi.fn(),
|
||||
writable: true,
|
||||
});
|
||||
|
||||
let mockAiAssistantStore: ReturnType<typeof mockedStore<typeof useAssistantStore>>;
|
||||
let mockNodeTypeStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
|
||||
let mockNdvStore: ReturnType<typeof mockedStore<typeof useNDVStore>>;
|
||||
let mockNDVStore: ReturnType<typeof mockedStore<typeof useNDVStore>>;
|
||||
let mockWorkflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
|
||||
|
||||
const renderComponent = createComponentRenderer(NodeErrorView);
|
||||
|
||||
describe('NodeErrorView.vue', () => {
|
||||
let error: NodeError;
|
||||
|
||||
beforeEach(() => {
|
||||
createTestingPinia();
|
||||
|
||||
mockAiAssistantStore = mockedStore(useAssistantStore);
|
||||
mockNodeTypeStore = mockedStore(useNodeTypesStore);
|
||||
mockNdvStore = mockedStore(useNDVStore);
|
||||
mockNDVStore = mockedStore(useNDVStore);
|
||||
mockWorkflowsStore = mockedStore(useWorkflowsStore);
|
||||
|
||||
//@ts-expect-error
|
||||
error = {
|
||||
name: 'NodeOperationError',
|
||||
@@ -59,7 +80,7 @@ describe('NodeErrorView.vue', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders an Error with a messages array', async () => {
|
||||
it('renders an Error with a messages array', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
error: {
|
||||
@@ -74,7 +95,7 @@ describe('NodeErrorView.vue', () => {
|
||||
expect(errorMessage).toHaveTextContent('Unexpected identifier [line 1]');
|
||||
});
|
||||
|
||||
it('renders an Error with a message string', async () => {
|
||||
it('renders an Error with a message string', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
error: {
|
||||
@@ -89,8 +110,8 @@ describe('NodeErrorView.vue', () => {
|
||||
expect(errorMessage).toHaveTextContent('Unexpected identifier [line 1]');
|
||||
});
|
||||
|
||||
it('should not render AI assistant button when error happens in deprecated function node', async () => {
|
||||
//@ts-expect-error
|
||||
it('should not render AI assistant button when error happens in deprecated function node', () => {
|
||||
// @ts-expect-error - Mock node type store method
|
||||
mockNodeTypeStore.getNodeType = vi.fn(() => ({
|
||||
type: 'n8n-nodes-base.function',
|
||||
typeVersion: 1,
|
||||
@@ -168,20 +189,111 @@ describe('NodeErrorView.vue', () => {
|
||||
expect(getByTestId('ask-assistant-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('open error node details when open error node is clicked', async () => {
|
||||
const { getByTestId, emitted } = renderComponent({
|
||||
props: {
|
||||
error: {
|
||||
...error,
|
||||
name: 'NodeOperationError',
|
||||
functionality: 'configuration-node',
|
||||
describe('onOpenErrorNodeDetailClick', () => {
|
||||
it('does nothing when error has no node', async () => {
|
||||
const errorWithoutNode = {
|
||||
name: 'NodeOperationError',
|
||||
functionality: 'configuration-node',
|
||||
message: 'Error without node',
|
||||
node: undefined,
|
||||
};
|
||||
|
||||
const { queryByTestId } = renderComponent({
|
||||
props: {
|
||||
error: errorWithoutNode,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const button = queryByTestId('node-error-view-open-node-button');
|
||||
|
||||
// If there's no node, button should not render or if it does, clicking it should do nothing
|
||||
if (button) {
|
||||
await userEvent.click(button);
|
||||
}
|
||||
|
||||
expect(window.open).not.toHaveBeenCalled();
|
||||
expect(mockNDVStore.activeNodeName).toBeNull();
|
||||
});
|
||||
|
||||
await userEvent.click(getByTestId('node-error-view-open-node-button'));
|
||||
it('opens new window when error has different workflow and execution IDs', async () => {
|
||||
mockWorkflowsStore.workflowId = 'current-workflow-id';
|
||||
mockWorkflowsStore.getWorkflowExecution = {
|
||||
id: 'current-execution-id',
|
||||
} as IExecutionResponse;
|
||||
|
||||
expect(emitted().click).toHaveLength(1);
|
||||
expect(mockNdvStore.activeNodeName).toBe(error.node.name);
|
||||
const testError = {
|
||||
...error,
|
||||
name: 'NodeOperationError',
|
||||
functionality: 'configuration-node',
|
||||
workflowId: 'different-workflow-id',
|
||||
executionId: 'different-execution-id',
|
||||
};
|
||||
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
error: testError,
|
||||
},
|
||||
});
|
||||
|
||||
const button = getByTestId('node-error-view-open-node-button');
|
||||
await userEvent.click(button);
|
||||
|
||||
expect(mockRouterResolve).toHaveBeenCalledWith({
|
||||
name: 'ExecutionPreview',
|
||||
params: {
|
||||
name: 'different-workflow-id',
|
||||
executionId: 'different-execution-id',
|
||||
nodeId: 'd1ce5dc9-f9ae-4ac6-84e5-0696ba175dd9',
|
||||
},
|
||||
});
|
||||
expect(window.open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets active node name when error is in current workflow/execution', async () => {
|
||||
mockWorkflowsStore.workflowId = 'current-workflow-id';
|
||||
mockWorkflowsStore.getWorkflowExecution = {
|
||||
id: 'current-execution-id',
|
||||
} as IExecutionResponse;
|
||||
|
||||
const testError = {
|
||||
...error,
|
||||
name: 'NodeOperationError',
|
||||
functionality: 'configuration-node',
|
||||
workflowId: 'current-workflow-id',
|
||||
executionId: 'current-execution-id',
|
||||
};
|
||||
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
error: testError,
|
||||
},
|
||||
});
|
||||
|
||||
const button = getByTestId('node-error-view-open-node-button');
|
||||
await userEvent.click(button);
|
||||
|
||||
expect(window.open).not.toHaveBeenCalled();
|
||||
expect(mockNDVStore.activeNodeName).toBe('ErrorCode');
|
||||
});
|
||||
|
||||
it('sets active node name when error has no workflow/execution IDs', async () => {
|
||||
const testError = {
|
||||
...error,
|
||||
name: 'NodeOperationError',
|
||||
functionality: 'configuration-node',
|
||||
};
|
||||
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
error: testError,
|
||||
},
|
||||
});
|
||||
|
||||
const button = getByTestId('node-error-view-open-node-button');
|
||||
await userEvent.click(button);
|
||||
|
||||
expect(window.open).not.toHaveBeenCalled();
|
||||
expect(mockNDVStore.activeNodeName).toBe('ErrorCode');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import type {
|
||||
IDataObject,
|
||||
@@ -16,7 +18,7 @@ import type {
|
||||
NodeOperationError,
|
||||
} from 'n8n-workflow';
|
||||
import { sanitizeHtml } from '@/utils/htmlUtils';
|
||||
import { MAX_DISPLAY_DATA_SIZE, NEW_ASSISTANT_SESSION_MODAL } from '@/constants';
|
||||
import { MAX_DISPLAY_DATA_SIZE, NEW_ASSISTANT_SESSION_MODAL, VIEWS } from '@/constants';
|
||||
import type { BaseTextKey } from '@n8n/i18n';
|
||||
import { useAssistantStore } from '@/stores/assistant.store';
|
||||
import type { ChatRequest } from '@/types/assistant.types';
|
||||
@@ -34,6 +36,8 @@ type Props = {
|
||||
};
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const router = useRouter();
|
||||
const clipboard = useClipboard();
|
||||
const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
@@ -41,10 +45,14 @@ const assistantHelpers = useAIAssistantHelpers();
|
||||
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const ndvStore = useNDVStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const rootStore = useRootStore();
|
||||
const assistantStore = useAssistantStore();
|
||||
const uiStore = useUIStore();
|
||||
|
||||
const workflowId = computed(() => workflowsStore.workflowId);
|
||||
const executionId = computed(() => workflowsStore.getWorkflowExecution?.id);
|
||||
|
||||
const displayCause = computed(() => {
|
||||
return JSON.stringify(props.error.cause ?? '').length < MAX_DISPLAY_DATA_SIZE;
|
||||
});
|
||||
@@ -206,7 +214,7 @@ function getErrorMessage(): string {
|
||||
|
||||
if (isSubNodeError.value) {
|
||||
message = i18n.baseText('nodeErrorView.errorSubNode', {
|
||||
interpolate: { node: props.error.node.name },
|
||||
interpolate: { node: props.error.node?.name ?? '' },
|
||||
});
|
||||
} else if (
|
||||
isNonEmptyString(props.error.message) &&
|
||||
@@ -384,7 +392,32 @@ function nodeIsHidden() {
|
||||
}
|
||||
|
||||
const onOpenErrorNodeDetailClick = () => {
|
||||
ndvStore.activeNodeName = props.error.node.name;
|
||||
if (!props.error.node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
'workflowId' in props.error &&
|
||||
workflowId.value &&
|
||||
typeof props.error.workflowId === 'string' &&
|
||||
workflowId.value !== props.error.workflowId &&
|
||||
'executionId' in props.error &&
|
||||
executionId.value &&
|
||||
typeof props.error.executionId === 'string' &&
|
||||
executionId.value !== props.error.executionId
|
||||
) {
|
||||
const link = router.resolve({
|
||||
name: VIEWS.EXECUTION_PREVIEW,
|
||||
params: {
|
||||
name: props.error.workflowId,
|
||||
executionId: props.error.executionId,
|
||||
nodeId: props.error.node.id,
|
||||
},
|
||||
});
|
||||
window.open(link.href, '_blank');
|
||||
} else {
|
||||
ndvStore.activeNodeName = props.error.node.name;
|
||||
}
|
||||
};
|
||||
|
||||
async function onAskAssistantClick() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { reactive } from 'vue';
|
||||
import { createTestWorkflowObject, defaultNodeDescriptions } from '@/__tests__/mocks';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
||||
import { type MockedStore, mockedStore, SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
||||
import RunData from '@/components/RunData.vue';
|
||||
import { STORES } from '@n8n/stores';
|
||||
import { SET_NODE_TYPE } from '@/constants';
|
||||
@@ -11,7 +12,8 @@ import userEvent from '@testing-library/user-event';
|
||||
import { waitFor } from '@testing-library/vue';
|
||||
import type { INodeExecutionData, ITaskData, ITaskMetadata } from 'n8n-workflow';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { useNodeTypesStore } from '../stores/nodeTypes.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useSchemaPreviewStore } from '@/stores/schemaPreview.store';
|
||||
|
||||
const MOCK_EXECUTION_URL = 'execution.url/123';
|
||||
|
||||
@@ -22,8 +24,12 @@ const { trackOpeningRelatedExecution, resolveRelatedExecutionUrl } = vi.hoisted(
|
||||
|
||||
vi.mock('vue-router', () => {
|
||||
return {
|
||||
useRouter: () => ({}),
|
||||
useRoute: () => ({ meta: {} }),
|
||||
useRouter: () => ({
|
||||
resolve: vi.fn(() => ({
|
||||
href: '',
|
||||
})),
|
||||
}),
|
||||
useRoute: () => reactive({ meta: {} }),
|
||||
RouterLink: vi.fn(),
|
||||
};
|
||||
});
|
||||
@@ -41,6 +47,10 @@ vi.mock('@/composables/useWorkflowHelpers', async (importOriginal) => {
|
||||
});
|
||||
|
||||
describe('RunData', () => {
|
||||
let workflowsStore: MockedStore<typeof useWorkflowsStore>;
|
||||
let nodeTypesStore: MockedStore<typeof useNodeTypesStore>;
|
||||
let schemaPreviewStore: MockedStore<typeof useSchemaPreviewStore>;
|
||||
|
||||
beforeAll(() => {
|
||||
resolveRelatedExecutionUrl.mockReturnValue('execution.url/123');
|
||||
});
|
||||
@@ -611,14 +621,16 @@ describe('RunData', () => {
|
||||
|
||||
const render = ({
|
||||
defaultRunItems,
|
||||
workflowId,
|
||||
workflowNodes = nodes,
|
||||
displayMode,
|
||||
displayMode = 'html',
|
||||
pinnedData,
|
||||
paneType = 'output',
|
||||
metadata,
|
||||
runs,
|
||||
}: {
|
||||
defaultRunItems?: INodeExecutionData[];
|
||||
workflowId?: string;
|
||||
workflowNodes?: INodeUi[];
|
||||
displayMode: IRunDataDisplayMode;
|
||||
pinnedData?: INodeExecutionData[];
|
||||
@@ -677,24 +689,27 @@ describe('RunData', () => {
|
||||
|
||||
setActivePinia(pinia);
|
||||
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
workflowsStore = mockedStore(useWorkflowsStore);
|
||||
schemaPreviewStore = mockedStore(useSchemaPreviewStore);
|
||||
|
||||
nodeTypesStore.setNodeTypes(defaultNodeDescriptions);
|
||||
vi.mocked(workflowsStore).getNodeByName.mockReturnValue(workflowNodes[0]);
|
||||
workflowsStore.getNodeByName.mockReturnValue(workflowNodes[0]);
|
||||
|
||||
if (pinnedData) {
|
||||
vi.mocked(workflowsStore).pinDataByNodeName.mockReturnValue(pinnedData);
|
||||
workflowsStore.pinDataByNodeName.mockReturnValue(pinnedData);
|
||||
}
|
||||
|
||||
schemaPreviewStore.getSchemaPreview = vi.fn().mockResolvedValue({});
|
||||
|
||||
return createComponentRenderer(RunData, {
|
||||
props: {
|
||||
node: {
|
||||
name: 'Test Node',
|
||||
},
|
||||
workflow: createTestWorkflowObject({
|
||||
// @ts-expect-error allow missing properties in test
|
||||
workflowNodes,
|
||||
id: workflowId,
|
||||
nodes: workflowNodes,
|
||||
}),
|
||||
displayMode,
|
||||
},
|
||||
@@ -710,6 +725,7 @@ describe('RunData', () => {
|
||||
name: 'Test Node',
|
||||
type: SET_NODE_TYPE,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
nodes: [{ name: 'Test Node', indicies: [], depth: 1 }],
|
||||
runIndex: 0,
|
||||
@@ -717,6 +733,9 @@ describe('RunData', () => {
|
||||
isExecuting: false,
|
||||
mappingEnabled: true,
|
||||
distanceFromActive: 0,
|
||||
tooMuchDataTitle: '',
|
||||
executingMessage: '',
|
||||
noDataInBranchMessage: '',
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
|
||||
@@ -311,9 +311,7 @@ const subworkflowExecutionError = computed(() => {
|
||||
} as NodeError;
|
||||
});
|
||||
|
||||
const hasSubworkflowExecutionError = computed(() =>
|
||||
Boolean(workflowsStore.subWorkflowExecutionError),
|
||||
);
|
||||
const hasSubworkflowExecutionError = computed(() => !!workflowsStore.subWorkflowExecutionError);
|
||||
|
||||
// Sub-nodes may wish to display the parent node error as it can contain additional metadata
|
||||
const parentNodeError = computed(() => {
|
||||
@@ -1775,7 +1773,7 @@ defineExpose({ enterEditMode });
|
||||
v-else-if="hasNodeRun && !inputData.length && !displaysMultipleNodes && !search"
|
||||
:class="$style.center"
|
||||
>
|
||||
<slot name="no-output-data">xxx</slot>
|
||||
<slot name="no-output-data"></slot>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -13,6 +13,7 @@ const props = withDefaults(
|
||||
workflow?: IWorkflowDb | IWorkflowTemplate['workflow'];
|
||||
executionId?: string;
|
||||
executionMode?: string;
|
||||
nodeId?: string;
|
||||
loaderType?: 'image' | 'spinner';
|
||||
canOpenNDV?: boolean;
|
||||
hideNodeIssues?: boolean;
|
||||
@@ -24,6 +25,7 @@ const props = withDefaults(
|
||||
workflow: undefined,
|
||||
executionId: undefined,
|
||||
executionMode: undefined,
|
||||
nodeId: undefined,
|
||||
loaderType: 'image',
|
||||
canOpenNDV: true,
|
||||
hideNodeIssues: false,
|
||||
@@ -95,6 +97,7 @@ const loadExecution = () => {
|
||||
command: 'openExecution',
|
||||
executionId: props.executionId,
|
||||
executionMode: props.executionMode ?? '',
|
||||
nodeId: props.nodeId,
|
||||
canOpenNDV: props.canOpenNDV,
|
||||
}),
|
||||
'*',
|
||||
|
||||
@@ -45,6 +45,7 @@ const workflowPermissions = computed(
|
||||
() => getResourcePermissions(workflowsStore.getWorkflowById(workflowId.value)?.scopes).workflow,
|
||||
);
|
||||
const executionId = computed(() => route.params.executionId as string);
|
||||
const nodeId = computed(() => route.params.nodeId as string);
|
||||
const executionUIDetails = computed<IExecutionUIData | null>(() =>
|
||||
props.execution ? executionHelpers.getUIDetails(props.execution) : null,
|
||||
);
|
||||
@@ -327,6 +328,7 @@ const onVoteClick = async (voteValue: AnnotationVote) => {
|
||||
loader-type="spinner"
|
||||
:execution-id="executionId"
|
||||
:execution-mode="execution?.mode || ''"
|
||||
:node-id="nodeId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3081,6 +3081,61 @@ describe('useCanvasOperations', () => {
|
||||
type: 'error',
|
||||
});
|
||||
});
|
||||
it('should set active node when nodeId is provided and node exists', async () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const ndvStore = mockedStore(useNDVStore);
|
||||
const { openExecution } = useCanvasOperations();
|
||||
|
||||
const executionId = '123';
|
||||
const nodeId = 'node-123';
|
||||
const mockNode = { id: nodeId, name: 'Test Node', type: 'test' } as INodeUi;
|
||||
const executionData: IExecutionResponse = {
|
||||
id: executionId,
|
||||
finished: true,
|
||||
status: 'success',
|
||||
startedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
workflowData: createTestWorkflow(),
|
||||
mode: 'manual' as WorkflowExecuteMode,
|
||||
};
|
||||
|
||||
workflowsStore.getExecution.mockResolvedValue(executionData);
|
||||
workflowsStore.getNodeById.mockReturnValue(mockNode);
|
||||
|
||||
await openExecution(executionId, nodeId);
|
||||
|
||||
expect(workflowsStore.getNodeById).toHaveBeenCalledWith(nodeId);
|
||||
expect(ndvStore.activeNodeName).toBe(mockNode.name);
|
||||
});
|
||||
|
||||
it('should show error when nodeId is provided but node does not exist', async () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const { openExecution } = useCanvasOperations();
|
||||
const toast = useToast();
|
||||
|
||||
const executionId = '123';
|
||||
const nodeId = 'non-existent-node';
|
||||
const executionData: IExecutionResponse = {
|
||||
id: executionId,
|
||||
finished: true,
|
||||
status: 'success',
|
||||
startedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
workflowData: createTestWorkflow(),
|
||||
mode: 'manual' as WorkflowExecuteMode,
|
||||
};
|
||||
|
||||
workflowsStore.getExecution.mockResolvedValue(executionData);
|
||||
workflowsStore.getNodeById.mockReturnValue(undefined);
|
||||
|
||||
await openExecution(executionId, nodeId);
|
||||
|
||||
expect(workflowsStore.getNodeById).toHaveBeenCalledWith(nodeId);
|
||||
expect(toast.showError).toHaveBeenCalledWith(
|
||||
new Error(`Node with id "${nodeId}" could not be found!`),
|
||||
'Problem opening node in execution',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('connectAdjacentNodes', () => {
|
||||
|
||||
@@ -2130,7 +2130,7 @@ export function useCanvasOperations() {
|
||||
deleteNodes(ids);
|
||||
}
|
||||
|
||||
async function openExecution(executionId: string) {
|
||||
async function openExecution(executionId: string, nodeId?: string) {
|
||||
let data: IExecutionResponse | undefined;
|
||||
try {
|
||||
data = await workflowsStore.getExecution(executionId);
|
||||
@@ -2159,6 +2159,18 @@ export function useCanvasOperations() {
|
||||
workflowsStore.setWorkflowPinData({});
|
||||
}
|
||||
|
||||
if (nodeId) {
|
||||
const node = workflowsStore.getNodeById(nodeId);
|
||||
if (node) {
|
||||
ndvStore.activeNodeName = node.name;
|
||||
} else {
|
||||
toast.showError(
|
||||
new Error(`Node with id "${nodeId}" could not be found!`),
|
||||
i18n.baseText('nodeView.showError.openExecution.node'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
uiStore.stateIsDirty = false;
|
||||
|
||||
return data;
|
||||
|
||||
@@ -247,7 +247,7 @@ export const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':executionId',
|
||||
path: ':executionId/:nodeId?',
|
||||
name: VIEWS.EXECUTION_PREVIEW,
|
||||
components: {
|
||||
executionPreview: WorkflowExecutionsPreview,
|
||||
|
||||
@@ -1322,13 +1322,13 @@ function trackRunWorkflowToNode(node: INodeUi) {
|
||||
void externalHooks.run('nodeView.onRunNode', telemetryPayload);
|
||||
}
|
||||
|
||||
async function onOpenExecution(executionId: string) {
|
||||
async function onOpenExecution(executionId: string, nodeId?: string) {
|
||||
canvasStore.startLoading();
|
||||
|
||||
resetWorkspace();
|
||||
await initializeData();
|
||||
|
||||
const data = await openExecution(executionId);
|
||||
const data = await openExecution(executionId, nodeId);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
@@ -1581,7 +1581,7 @@ async function onPostMessageReceived(messageEvent: MessageEvent) {
|
||||
isProductionExecutionPreview.value =
|
||||
json.executionMode !== 'manual' && json.executionMode !== 'evaluation';
|
||||
|
||||
await onOpenExecution(json.executionId);
|
||||
await onOpenExecution(json.executionId, json.nodeId);
|
||||
canOpenNDV.value = json.canOpenNDV ?? true;
|
||||
hideNodeIssues.value = json.hideNodeIssues ?? false;
|
||||
isExecutionPreview.value = true;
|
||||
|
||||
Reference in New Issue
Block a user