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