mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(editor): "Trigger node not found" error when chat message is entered (#14954)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watchEffect, useTemplateRef } from 'vue';
|
||||
import { computed, ref, watchEffect, useTemplateRef, watch } from 'vue';
|
||||
|
||||
// Components
|
||||
import ChatMessagesPanel from './components/ChatMessagesPanel.vue';
|
||||
@@ -68,7 +68,7 @@ const {
|
||||
sendMessage,
|
||||
refreshSession,
|
||||
displayExecution,
|
||||
} = useChatState(false, onWindowResize);
|
||||
} = useChatState(false);
|
||||
|
||||
// Expose internal state for testing
|
||||
defineExpose({
|
||||
@@ -91,6 +91,18 @@ function onPopOut() {
|
||||
watchEffect(() => {
|
||||
canvasStore.setPanelHeight(chatPanelState.value === LOGS_PANEL_STATE.ATTACHED ? height.value : 0);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => workflowsStore.logsPanelState,
|
||||
(state) => {
|
||||
if (state !== LOGS_PANEL_STATE.CLOSED) {
|
||||
setTimeout(() => {
|
||||
onWindowResize?.();
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -7,7 +7,7 @@ import MessageOptionAction from './MessageOptionAction.vue';
|
||||
import { chatEventBus } from '@n8n/chat/event-buses';
|
||||
import type { ArrowKeyDownPayload } from '@n8n/chat/components/Input.vue';
|
||||
import ChatInput from '@n8n/chat/components/Input.vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { watch, computed, ref } from 'vue';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import PanelHeader from '@/components/CanvasChat/future/components/PanelHeader.vue';
|
||||
@@ -136,6 +136,18 @@ async function copySessionId() {
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.isOpen,
|
||||
(isOpen) => {
|
||||
if (isOpen) {
|
||||
setTimeout(() => {
|
||||
chatEventBus.emit('focusInput');
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { type INodeUi } from '@/Interface';
|
||||
import { useCanvasStore } from '@/stores/canvas.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { ChatOptionsSymbol, ChatSymbol } from '@n8n/chat/constants';
|
||||
@@ -31,11 +30,10 @@ interface ChatState {
|
||||
displayExecution: (executionId: string) => void;
|
||||
}
|
||||
|
||||
export function useChatState(isReadOnly: boolean, onWindowResize?: () => void): ChatState {
|
||||
export function useChatState(isReadOnly: boolean): ChatState {
|
||||
const locale = useI18n();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const canvasStore = useCanvasStore();
|
||||
const router = useRouter();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
@@ -44,25 +42,16 @@ export function useChatState(isReadOnly: boolean, onWindowResize?: () => void):
|
||||
const currentSessionId = ref<string>(uuid().replace(/-/g, ''));
|
||||
|
||||
const previousChatMessages = computed(() => workflowsStore.getPastChatMessages);
|
||||
const canvasNodes = computed(() => workflowsStore.allNodes);
|
||||
const allConnections = computed(() => workflowsStore.allConnections);
|
||||
const logsPanelState = computed(() => workflowsStore.logsPanelState);
|
||||
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
|
||||
|
||||
// Initialize features with injected dependencies
|
||||
const {
|
||||
chatTriggerNode,
|
||||
connectedNode,
|
||||
allowFileUploads,
|
||||
allowedFilesMimeTypes,
|
||||
setChatTriggerNode,
|
||||
setConnectedNode,
|
||||
} = useChatTrigger({
|
||||
workflow,
|
||||
canvasNodes,
|
||||
getNodeByName: workflowsStore.getNodeByName,
|
||||
getNodeType: nodeTypesStore.getNodeType,
|
||||
});
|
||||
const { chatTriggerNode, connectedNode, allowFileUploads, allowedFilesMimeTypes } =
|
||||
useChatTrigger({
|
||||
workflow,
|
||||
getNodeByName: workflowsStore.getNodeByName,
|
||||
getNodeType: nodeTypesStore.getNodeType,
|
||||
});
|
||||
|
||||
const { sendMessage, isLoading } = useChatMessaging({
|
||||
chatTrigger: chatTriggerNode,
|
||||
@@ -134,37 +123,6 @@ export function useChatState(isReadOnly: boolean, onWindowResize?: () => void):
|
||||
provide(ChatSymbol, chatConfig);
|
||||
provide(ChatOptionsSymbol, chatOptions);
|
||||
|
||||
// Watchers
|
||||
watch(
|
||||
() => logsPanelState.value,
|
||||
(state) => {
|
||||
if (state !== LOGS_PANEL_STATE.CLOSED) {
|
||||
setChatTriggerNode();
|
||||
setConnectedNode();
|
||||
|
||||
setTimeout(() => {
|
||||
onWindowResize?.();
|
||||
chatEventBus.emit('focusInput');
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => allConnections.value,
|
||||
() => {
|
||||
if (canvasStore.isLoading) return;
|
||||
setTimeout(() => {
|
||||
if (!chatTriggerNode.value) {
|
||||
setChatTriggerNode();
|
||||
}
|
||||
setConnectedNode();
|
||||
}, 0);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// This function creates a promise that resolves when the workflow execution completes
|
||||
// It's used to handle the loading state while waiting for the workflow to finish
|
||||
async function createExecutionPromise() {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { ComputedRef, MaybeRef } from 'vue';
|
||||
import { ref, computed, unref } from 'vue';
|
||||
import type { ComputedRef } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import {
|
||||
CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE,
|
||||
NodeConnectionTypes,
|
||||
NodeHelpers,
|
||||
} from 'n8n-workflow';
|
||||
import type { INodeTypeDescription, Workflow, INode, INodeParameters } from 'n8n-workflow';
|
||||
import type { INodeTypeDescription, Workflow, INodeParameters } from 'n8n-workflow';
|
||||
import {
|
||||
AI_CATEGORY_AGENTS,
|
||||
AI_CATEGORY_CHAINS,
|
||||
@@ -18,21 +18,12 @@ import { isChatNode } from '@/components/CanvasChat/utils';
|
||||
export interface ChatTriggerDependencies {
|
||||
getNodeByName: (name: string) => INodeUi | null;
|
||||
getNodeType: (type: string, version: number) => INodeTypeDescription | null;
|
||||
canvasNodes: MaybeRef<INodeUi[]>;
|
||||
workflow: ComputedRef<Workflow>;
|
||||
}
|
||||
|
||||
export function useChatTrigger({
|
||||
getNodeByName,
|
||||
getNodeType,
|
||||
canvasNodes,
|
||||
workflow,
|
||||
}: ChatTriggerDependencies) {
|
||||
const chatTriggerName = ref<string | null>(null);
|
||||
const connectedNode = ref<INode | null>(null);
|
||||
|
||||
const chatTriggerNode = computed(() =>
|
||||
chatTriggerName.value ? getNodeByName(chatTriggerName.value) : null,
|
||||
export function useChatTrigger({ getNodeByName, getNodeType, workflow }: ChatTriggerDependencies) {
|
||||
const chatTriggerNode = computed(
|
||||
() => Object.values(workflow.value.nodes).find(isChatNode) ?? null,
|
||||
);
|
||||
|
||||
const allowFileUploads = computed(() => {
|
||||
@@ -49,22 +40,12 @@ export function useChatTrigger({
|
||||
);
|
||||
});
|
||||
|
||||
/** Gets the chat trigger node from the workflow */
|
||||
function setChatTriggerNode() {
|
||||
const triggerNode = unref(canvasNodes).find(isChatNode);
|
||||
|
||||
if (!triggerNode) {
|
||||
return;
|
||||
}
|
||||
chatTriggerName.value = triggerNode.name;
|
||||
}
|
||||
|
||||
/** Sets the connected node after finding the trigger */
|
||||
function setConnectedNode() {
|
||||
const connectedNode = computed(() => {
|
||||
const triggerNode = chatTriggerNode.value;
|
||||
|
||||
if (!triggerNode) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const chatChildren = workflow.value.getChildNodes(triggerNode.name);
|
||||
@@ -121,15 +102,14 @@ export function useChatTrigger({
|
||||
const result = Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent));
|
||||
return result;
|
||||
});
|
||||
connectedNode.value = chatRootNode ?? null;
|
||||
}
|
||||
|
||||
return chatRootNode ?? null;
|
||||
});
|
||||
|
||||
return {
|
||||
allowFileUploads,
|
||||
allowedFilesMimeTypes,
|
||||
chatTriggerNode,
|
||||
connectedNode: computed(() => connectedNode.value),
|
||||
setChatTriggerNode,
|
||||
setConnectedNode,
|
||||
connectedNode,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -146,6 +146,17 @@ function isLastChild(level: number) {
|
||||
icon="exclamation-triangle"
|
||||
:class="$style.compactErrorIcon"
|
||||
/>
|
||||
<N8nIconButton
|
||||
v-if="!isCompact || !props.latestInfo?.deleted"
|
||||
type="secondary"
|
||||
size="medium"
|
||||
icon="edit"
|
||||
style="color: var(--color-text-base)"
|
||||
:disabled="props.latestInfo?.deleted"
|
||||
:class="$style.openNdvButton"
|
||||
:aria-label="locale.baseText('logs.overview.body.open')"
|
||||
@click.stop="emit('openNdv', props.data)"
|
||||
/>
|
||||
<N8nIconButton
|
||||
v-if="
|
||||
!isCompact ||
|
||||
@@ -160,17 +171,6 @@ function isLastChild(level: number) {
|
||||
:disabled="props.latestInfo?.deleted || props.latestInfo?.disabled"
|
||||
@click.stop="emit('triggerPartialExecution', props.data)"
|
||||
/>
|
||||
<N8nIconButton
|
||||
v-if="!isCompact || !props.latestInfo?.deleted"
|
||||
type="secondary"
|
||||
size="small"
|
||||
icon="external-link-alt"
|
||||
style="color: var(--color-text-base)"
|
||||
:disabled="props.latestInfo?.deleted"
|
||||
:class="$style.openNdvButton"
|
||||
:aria-label="locale.baseText('logs.overview.body.open')"
|
||||
@click.stop="emit('openNdv', props.data)"
|
||||
/>
|
||||
<N8nButton
|
||||
v-if="!isCompact || props.data.children.length > 0"
|
||||
type="secondary"
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import {
|
||||
createAiData,
|
||||
createLogEntries,
|
||||
deepToRaw,
|
||||
findSelectedLogEntry,
|
||||
getTreeNodeData,
|
||||
getTreeNodeDataV2,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
} from 'n8n-workflow';
|
||||
import { type LogEntrySelection } from '../CanvasChat/types/logs';
|
||||
import { type IExecutionResponse } from '@/Interface';
|
||||
import { isReactive, reactive } from 'vue';
|
||||
|
||||
describe(getTreeNodeData, () => {
|
||||
it('should generate one node per execution', () => {
|
||||
@@ -608,3 +610,28 @@ describe(createLogEntries, () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe(deepToRaw, () => {
|
||||
it('should convert reactive fields to raw in data with circular structure', () => {
|
||||
const data = reactive({
|
||||
foo: reactive({ bar: {} }),
|
||||
bazz: {},
|
||||
});
|
||||
|
||||
data.foo.bar = data;
|
||||
data.bazz = data;
|
||||
|
||||
const raw = deepToRaw(data);
|
||||
|
||||
expect(isReactive(data)).toBe(true);
|
||||
expect(isReactive(data.foo)).toBe(true);
|
||||
expect(isReactive(data.foo.bar)).toBe(true);
|
||||
expect(isReactive(data.bazz)).toBe(true);
|
||||
expect(isReactive(raw)).toBe(false);
|
||||
expect(isReactive(raw.foo)).toBe(false);
|
||||
expect(isReactive(raw.foo.bar)).toBe(false);
|
||||
expect(isReactive(raw.bazz)).toBe(false);
|
||||
|
||||
console.log(raw.foo.bar);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -431,8 +431,18 @@ export function findSelectedLogEntry(
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function deepToRaw<T>(sourceObj: T): T {
|
||||
const seen = new WeakMap();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const objectIterator = (input: any): any => {
|
||||
if (seen.has(input)) {
|
||||
return input;
|
||||
}
|
||||
|
||||
if (input !== null && typeof input === 'object') {
|
||||
seen.set(input, true);
|
||||
}
|
||||
|
||||
if (Array.isArray(input)) {
|
||||
return input.map((item) => objectIterator(item));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user