mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-01 15:47:41 +08:00
fix(editor): Handle chat trigger waiting state in setup cards (#27682)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -194,7 +194,7 @@ describe('NodeExecuteButton', () => {
|
||||
});
|
||||
|
||||
const { getByRole } = renderComponent();
|
||||
expect(getByRole('button').textContent).toBe('Test chat');
|
||||
expect(getByRole('button').textContent).toBe('Open chat');
|
||||
});
|
||||
|
||||
it('displays correct button label for polling node', () => {
|
||||
|
||||
@@ -469,7 +469,7 @@ describe('useNodeExecution', () => {
|
||||
|
||||
const { buttonLabel } = useNodeExecution(node);
|
||||
|
||||
expect(buttonLabel.value).toBe('ndv.execute.testChat');
|
||||
expect(buttonLabel.value).toBe('chat.open');
|
||||
});
|
||||
|
||||
it('should return listenForTestEvent for webhook node', () => {
|
||||
|
||||
@@ -221,7 +221,7 @@ export function useNodeExecution(
|
||||
}
|
||||
|
||||
if (isChatNode.value) {
|
||||
return i18n.baseText('ndv.execute.testChat');
|
||||
return i18n.baseText('chat.open');
|
||||
}
|
||||
|
||||
if (isWebhookNode.value) {
|
||||
|
||||
+6
-1
@@ -2,6 +2,7 @@
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n, type BaseTextKey } from '@n8n/i18n';
|
||||
import { N8nButton, N8nCallout, N8nIcon, N8nText } from '@n8n/design-system';
|
||||
import { CHAT_TRIGGER_NODE_TYPE } from '@/app/constants/nodeTypes';
|
||||
|
||||
import NodeIcon from '@/app/components/NodeIcon.vue';
|
||||
import CredentialIcon from '@/features/credentials/components/CredentialIcon.vue';
|
||||
@@ -101,6 +102,10 @@ const useCredentialIcon = computed(
|
||||
|
||||
const isComplete = computed(() => props.state.isComplete);
|
||||
const isExecutable = computed(() => executableNode.value !== null);
|
||||
const showExecuteButton = computed(() => {
|
||||
if (nodeType.value?.name === CHAT_TRIGGER_NODE_TYPE && isInListeningState.value) return false;
|
||||
return isExecutable.value;
|
||||
});
|
||||
const isLastCard = computed(() => props.stepIndex === props.totalCards - 1);
|
||||
const showArrows = computed(() => props.totalCards > 1);
|
||||
const isPrevDisabled = computed(() => props.stepIndex === 0);
|
||||
@@ -229,7 +234,7 @@ watch(isActive, (active, wasActive) => {
|
||||
/>
|
||||
|
||||
<TriggerExecuteButton
|
||||
v-if="isExecutable"
|
||||
v-if="showExecuteButton"
|
||||
:label="executeLabel"
|
||||
:icon="executeButtonIcon"
|
||||
:disabled="isButtonDisabled || isTestingCredential"
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useI18n } from '@n8n/i18n';
|
||||
import { N8nCallout, N8nIcon, N8nText } from '@n8n/design-system';
|
||||
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import { CHAT_TRIGGER_NODE_TYPE } from '@/app/constants/nodeTypes';
|
||||
import TriggerExecuteButton from '@/features/setupPanel/components/TriggerExecuteButton.vue';
|
||||
import WebhookUrlPreview from '@/features/setupPanel/components/WebhookUrlPreview.vue';
|
||||
import { useTriggerExecution } from '@/features/setupPanel/composables/useTriggerExecution';
|
||||
@@ -80,6 +81,12 @@ const {
|
||||
|
||||
const { webhookUrls } = useWebhookUrls(executableNodeRef);
|
||||
|
||||
const showExecuteButton = computed(() => {
|
||||
if (props.executableNode?.type === CHAT_TRIGGER_NODE_TYPE && isInListeningState.value)
|
||||
return false;
|
||||
return !!props.executableNode;
|
||||
});
|
||||
|
||||
const showTriggerCallout = computed(() => props.isTrigger && isInListeningState.value);
|
||||
|
||||
const onExecuteClick = async () => {
|
||||
@@ -183,7 +190,7 @@ defineExpose({ markInteracted });
|
||||
</div>
|
||||
<slot name="footer-actions" />
|
||||
<TriggerExecuteButton
|
||||
v-if="executableNode"
|
||||
v-if="showExecuteButton"
|
||||
:label="executeLabel"
|
||||
:icon="executeButtonIcon"
|
||||
:disabled="isButtonDisabled || isTestingCredential"
|
||||
|
||||
+67
@@ -4,6 +4,9 @@ import { createTestingPinia } from '@pinia/testing';
|
||||
import { createTestNode, mockNodeTypeDescription } from '@/__tests__/mocks';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useLogsStore } from '@/app/stores/logs.store';
|
||||
import { CHAT_TRIGGER_NODE_TYPE } from '@/app/constants/nodeTypes';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
|
||||
import { useTriggerExecution } from '@/features/setupPanel/composables/useTriggerExecution';
|
||||
@@ -43,10 +46,14 @@ const createNode = (overrides: Partial<INodeUi> = {}): INodeUi =>
|
||||
|
||||
describe('useTriggerExecution', () => {
|
||||
let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
|
||||
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
|
||||
let logsStore: ReturnType<typeof mockedStore<typeof useLogsStore>>;
|
||||
|
||||
beforeEach(() => {
|
||||
createTestingPinia();
|
||||
nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
workflowsStore = mockedStore(useWorkflowsStore);
|
||||
logsStore = mockedStore(useLogsStore);
|
||||
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(null);
|
||||
|
||||
mockExecutionState.isExecuting = false;
|
||||
@@ -107,6 +114,66 @@ describe('useTriggerExecution', () => {
|
||||
|
||||
expect(isInListeningState.value).toBe(true);
|
||||
});
|
||||
|
||||
it('should be true for chat trigger when logs panel is open and destination node matches', () => {
|
||||
const chatNode = createNode({
|
||||
name: 'When chat message received',
|
||||
type: CHAT_TRIGGER_NODE_TYPE,
|
||||
});
|
||||
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(
|
||||
mockNodeTypeDescription({
|
||||
name: CHAT_TRIGGER_NODE_TYPE,
|
||||
displayName: 'When chat message received',
|
||||
}),
|
||||
);
|
||||
logsStore.isOpen = true;
|
||||
workflowsStore.chatPartialExecutionDestinationNode = 'When chat message received';
|
||||
|
||||
const node = ref<INodeUi | null>(chatNode);
|
||||
const { isInListeningState } = useTriggerExecution(node);
|
||||
|
||||
expect(isInListeningState.value).toBe(true);
|
||||
});
|
||||
|
||||
it('should be false for chat trigger when logs panel is closed', () => {
|
||||
const chatNode = createNode({
|
||||
name: 'When chat message received',
|
||||
type: CHAT_TRIGGER_NODE_TYPE,
|
||||
});
|
||||
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(
|
||||
mockNodeTypeDescription({
|
||||
name: CHAT_TRIGGER_NODE_TYPE,
|
||||
displayName: 'When chat message received',
|
||||
}),
|
||||
);
|
||||
logsStore.isOpen = false;
|
||||
workflowsStore.chatPartialExecutionDestinationNode = 'When chat message received';
|
||||
|
||||
const node = ref<INodeUi | null>(chatNode);
|
||||
const { isInListeningState } = useTriggerExecution(node);
|
||||
|
||||
expect(isInListeningState.value).toBe(false);
|
||||
});
|
||||
|
||||
it('should be false for chat trigger when destination node does not match', () => {
|
||||
const chatNode = createNode({
|
||||
name: 'When chat message received',
|
||||
type: CHAT_TRIGGER_NODE_TYPE,
|
||||
});
|
||||
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(
|
||||
mockNodeTypeDescription({
|
||||
name: CHAT_TRIGGER_NODE_TYPE,
|
||||
displayName: 'When chat message received',
|
||||
}),
|
||||
);
|
||||
logsStore.isOpen = true;
|
||||
workflowsStore.chatPartialExecutionDestinationNode = 'Some Other Node';
|
||||
|
||||
const node = ref<INodeUi | null>(chatNode);
|
||||
const { isInListeningState } = useTriggerExecution(node);
|
||||
|
||||
expect(isInListeningState.value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isButtonDisabled', () => {
|
||||
|
||||
+12
-3
@@ -6,6 +6,8 @@ import { useNodeExecution, type UseNodeExecutionOptions } from '@/app/composable
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { getTriggerNodeServiceName } from '@/app/utils/nodeTypesUtils';
|
||||
import { CHAT_TRIGGER_NODE_TYPE } from '@/app/constants/nodeTypes';
|
||||
import { useLogsStore } from '@/app/stores/logs.store';
|
||||
|
||||
/**
|
||||
* Wraps `useNodeExecution` with listening-hint logic for setup-panel cards.
|
||||
@@ -19,6 +21,7 @@ export function useTriggerExecution(
|
||||
const i18n = useI18n();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const logsStore = useLogsStore();
|
||||
|
||||
const {
|
||||
isExecuting,
|
||||
@@ -39,9 +42,15 @@ export function useTriggerExecution(
|
||||
: null,
|
||||
);
|
||||
|
||||
const isInListeningState = computed(
|
||||
() => isListening.value || isListeningForWorkflowEvents.value,
|
||||
);
|
||||
const isInListeningState = computed(() => {
|
||||
if (isListening.value || isListeningForWorkflowEvents.value) return true;
|
||||
|
||||
return (
|
||||
nodeType.value?.name === CHAT_TRIGGER_NODE_TYPE &&
|
||||
logsStore.isOpen &&
|
||||
workflowsStore.chatPartialExecutionDestinationNode === nodeValue.value?.name
|
||||
);
|
||||
});
|
||||
|
||||
const listeningHint = computed(() => {
|
||||
if (!isInListeningState.value || !nodeType.value) return '';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { computed, toValue, type MaybeRef } from 'vue';
|
||||
import { computedAsync } from '@vueuse/core';
|
||||
import type { IWebhookDescription } from 'n8n-workflow';
|
||||
import { CHAT_TRIGGER_NODE_TYPE } from '@/app/constants/nodeTypes';
|
||||
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
@@ -82,6 +83,7 @@ export function useWebhookUrls(node: MaybeRef<INodeUi | null>) {
|
||||
const webhookUrls = computedAsync(async () => {
|
||||
const currentNode = nodeValue.value;
|
||||
if (!currentNode || webhooks.value.length === 0) return [];
|
||||
if (nodeType.value?.name === CHAT_TRIGGER_NODE_TYPE) return [];
|
||||
|
||||
// Access parameters synchronously so Vue tracks it as a dependency.
|
||||
// Without this, changing a node parameter (e.g. webhook path) won't
|
||||
|
||||
Reference in New Issue
Block a user