mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat: Add Chat Trigger node (#7409)
Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com> Co-authored-by: Jesper Bylund <mail@jesperbylund.com> Co-authored-by: OlegIvaniv <me@olegivaniv.com> Co-authored-by: Deborah <deborah@starfallprojects.co.uk> Co-authored-by: Jan Oberhauser <janober@users.noreply.github.com> Co-authored-by: Jon <jonathan.bennetts@gmail.com> Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in> Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com> Co-authored-by: Giulio Andreini <andreini@netseven.it> Co-authored-by: Mason Geloso <Mason.geloso@gmail.com> Co-authored-by: Mason Geloso <hone@Masons-Mac-mini.local> Co-authored-by: Mutasem Aldmour <mutasem@n8n.io>
This commit is contained in:
@@ -42,6 +42,7 @@
|
||||
"@jsplumb/core": "^5.13.2",
|
||||
"@jsplumb/util": "^5.13.2",
|
||||
"@lezer/common": "^1.0.4",
|
||||
"@n8n/chat": "workspace:*",
|
||||
"@n8n/codemirror-lang-sql": "^1.0.2",
|
||||
"@n8n/permissions": "workspace:*",
|
||||
"@vueuse/components": "^10.5.0",
|
||||
@@ -89,7 +90,8 @@
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/luxon": "^3.2.0",
|
||||
"@types/uuid": "^8.3.2",
|
||||
"miragejs": "^0.1.47"
|
||||
"miragejs": "^0.1.47",
|
||||
"unplugin-icons": "^0.17.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "*",
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import type { INodeTypeData, INodeTypeDescription } from 'n8n-workflow';
|
||||
import {
|
||||
AGENT_NODE_TYPE,
|
||||
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||
MANUAL_TRIGGER_NODE_TYPE,
|
||||
} from '@/constants';
|
||||
import { AGENT_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE } from '@/constants';
|
||||
import nodeTypesJson from '../../../nodes-base/dist/types/nodes.json';
|
||||
import aiNodeTypesJson from '../../../@n8n/nodes-langchain/dist/types/nodes.json';
|
||||
|
||||
@@ -20,10 +16,10 @@ export const testingNodeTypes: INodeTypeData = {
|
||||
description: findNodeWithName(MANUAL_TRIGGER_NODE_TYPE),
|
||||
},
|
||||
},
|
||||
[MANUAL_CHAT_TRIGGER_NODE_TYPE]: {
|
||||
[CHAT_TRIGGER_NODE_TYPE]: {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: findNodeWithName(MANUAL_CHAT_TRIGGER_NODE_TYPE),
|
||||
description: findNodeWithName(CHAT_TRIGGER_NODE_TYPE),
|
||||
},
|
||||
},
|
||||
[AGENT_NODE_TYPE]: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { computed, ref } from 'vue';
|
||||
import type { EventBus } from 'n8n-design-system/utils';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
import Modal from './Modal.vue';
|
||||
import { CHAT_EMBED_MODAL_KEY, WEBHOOK_NODE_TYPE } from '../constants';
|
||||
import { CHAT_EMBED_MODAL_KEY, CHAT_TRIGGER_NODE_TYPE, WEBHOOK_NODE_TYPE } from '../constants';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import HtmlEditor from '@/components/HtmlEditor/HtmlEditor.vue';
|
||||
@@ -43,11 +43,30 @@ const tabs = ref([
|
||||
const currentTab = ref('cdn');
|
||||
|
||||
const webhookNode = computed(() => {
|
||||
return workflowsStore.workflow.nodes.find((node) => node.type === WEBHOOK_NODE_TYPE);
|
||||
for (const type of [CHAT_TRIGGER_NODE_TYPE, WEBHOOK_NODE_TYPE]) {
|
||||
const node = workflowsStore.workflow.nodes.find((node) => node.type === type);
|
||||
if (node) {
|
||||
// This has to be kept up-to-date with the mode in the Chat-Trigger node
|
||||
if (type === CHAT_TRIGGER_NODE_TYPE && !node.parameters.public) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
node,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const webhookUrl = computed(() => {
|
||||
return `${rootStore.getWebhookUrl}${webhookNode.value ? `/${webhookNode.value.webhookId}` : ''}`;
|
||||
const url = `${rootStore.getWebhookUrl}${
|
||||
webhookNode.value ? `/${webhookNode.value.node.webhookId}` : ''
|
||||
}`;
|
||||
|
||||
return webhookNode.value?.type === CHAT_TRIGGER_NODE_TYPE ? `${url}/chat` : url;
|
||||
});
|
||||
|
||||
function indentLines(code: string, indent: string = ' ') {
|
||||
@@ -57,7 +76,7 @@ function indentLines(code: string, indent: string = ' ') {
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
const importCode = 'import';
|
||||
const importCode = 'import'; // To avoid vite from parsing the import statement
|
||||
const commonCode = computed(() => ({
|
||||
import: `${importCode} '@n8n/chat/style.css';
|
||||
${importCode} { createChat } from '@n8n/chat';`,
|
||||
@@ -126,29 +145,38 @@ function closeDialog() {
|
||||
<n8n-tabs v-model="currentTab" :options="tabs" />
|
||||
|
||||
<div v-if="currentTab !== 'cdn'">
|
||||
<n8n-text>
|
||||
{{ i18n.baseText('chatEmbed.install') }}
|
||||
</n8n-text>
|
||||
<div class="mb-s">
|
||||
<n8n-text>
|
||||
{{ i18n.baseText('chatEmbed.install') }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
<CodeNodeEditor :model-value="commonCode.install" is-read-only />
|
||||
</div>
|
||||
|
||||
<n8n-text>
|
||||
<i18n-t :keypath="`chatEmbed.paste.${currentTab}`">
|
||||
<template #code>
|
||||
<code>{{ i18n.baseText(`chatEmbed.paste.${currentTab}.file`) }}</code>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</n8n-text>
|
||||
<div class="mb-s">
|
||||
<n8n-text>
|
||||
<i18n-t :keypath="`chatEmbed.paste.${currentTab}`">
|
||||
<template #code>
|
||||
<code>{{ i18n.baseText(`chatEmbed.paste.${currentTab}.file`) }}</code>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</n8n-text>
|
||||
</div>
|
||||
|
||||
<HtmlEditor v-if="currentTab === 'cdn'" :model-value="cdnCode" is-read-only />
|
||||
<HtmlEditor v-if="currentTab === 'vue'" :model-value="vueCode" is-read-only />
|
||||
<CodeNodeEditor v-if="currentTab === 'react'" :model-value="reactCode" is-read-only />
|
||||
<CodeNodeEditor v-if="currentTab === 'other'" :model-value="otherCode" is-read-only />
|
||||
|
||||
<n8n-info-tip>
|
||||
<n8n-text>
|
||||
{{ i18n.baseText('chatEmbed.packageInfo.description') }}
|
||||
<n8n-link :href="i18n.baseText('chatEmbed.url')" new-window size="small" bold>
|
||||
<n8n-link :href="i18n.baseText('chatEmbed.url')" new-window bold>
|
||||
{{ i18n.baseText('chatEmbed.packageInfo.link') }}
|
||||
</n8n-link>
|
||||
</n8n-text>
|
||||
|
||||
<n8n-info-tip class="mt-s">
|
||||
{{ i18n.baseText('chatEmbed.chatTriggerNode') }}
|
||||
</n8n-info-tip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
:style="nodeWrapperStyles"
|
||||
data-test-id="canvas-node"
|
||||
:data-name="data.name"
|
||||
:data-node-type="nodeType?.name"
|
||||
@contextmenu="(e: MouseEvent) => openContextMenu(e, 'node-right-click')"
|
||||
>
|
||||
<div v-show="isSelected" class="select-background"></div>
|
||||
@@ -1026,6 +1027,11 @@ export default defineComponent({
|
||||
left: -67px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-node-type='@n8n/n8n-nodes-langchain.chatTrigger'] {
|
||||
--configurable-node-min-input-count: 1;
|
||||
--configurable-node-input-width: 176px;
|
||||
}
|
||||
}
|
||||
|
||||
&--trigger .node-default .node-box {
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
import {
|
||||
AGENT_NODE_TYPE,
|
||||
BASIC_CHAIN_NODE_TYPE,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||
MANUAL_TRIGGER_NODE_TYPE,
|
||||
NODE_CREATOR_OPEN_SOURCES,
|
||||
@@ -190,7 +191,9 @@ export const useActions = () => {
|
||||
];
|
||||
|
||||
const isChatTriggerMissing =
|
||||
allNodes.find((node) => node.type === MANUAL_CHAT_TRIGGER_NODE_TYPE) === undefined;
|
||||
allNodes.find((node) =>
|
||||
[MANUAL_CHAT_TRIGGER_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE].includes(node.type),
|
||||
) === undefined;
|
||||
const isCompatibleNode = addedNodes.some((node) => COMPATIBLE_CHAT_NODES.includes(node.type));
|
||||
|
||||
return isCompatibleNode && isChatTriggerMissing;
|
||||
@@ -211,7 +214,7 @@ export const useActions = () => {
|
||||
}
|
||||
|
||||
if (shouldPrependChatTrigger(addedNodes)) {
|
||||
addedNodes.unshift({ type: MANUAL_CHAT_TRIGGER_NODE_TYPE, isAutoAdd: true });
|
||||
addedNodes.unshift({ type: CHAT_TRIGGER_NODE_TYPE, isAutoAdd: true });
|
||||
connections.push({
|
||||
from: { nodeIndex: 0 },
|
||||
to: { nodeIndex: 1 },
|
||||
|
||||
@@ -276,9 +276,14 @@ export default defineComponent({
|
||||
return null;
|
||||
},
|
||||
showTriggerPanel(): boolean {
|
||||
const override = !!this.activeNodeType?.triggerPanel;
|
||||
if (typeof this.activeNodeType?.triggerPanel === 'boolean') {
|
||||
return override;
|
||||
}
|
||||
|
||||
const isWebhookBasedNode = !!this.activeNodeType?.webhooks?.length;
|
||||
const isPollingNode = this.activeNodeType?.polling;
|
||||
const override = !!this.activeNodeType?.triggerPanel;
|
||||
|
||||
return (
|
||||
!this.readOnly && this.isTriggerNode && (isWebhookBasedNode || isPollingNode || override)
|
||||
);
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
MANUAL_TRIGGER_NODE_TYPE,
|
||||
MODAL_CONFIRM,
|
||||
FORM_TRIGGER_NODE_TYPE,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
} from '@/constants';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
@@ -40,6 +41,7 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useMessage } from '@/composables/useMessage';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||
import { nodeViewEventBus } from '@/event-bus';
|
||||
import { usePinnedData } from '@/composables/usePinnedData';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -116,6 +118,9 @@ export default defineComponent({
|
||||
isManualTriggerNode(): boolean {
|
||||
return Boolean(this.nodeType && this.nodeType.name === MANUAL_TRIGGER_NODE_TYPE);
|
||||
},
|
||||
isChatNode(): boolean {
|
||||
return Boolean(this.nodeType && this.nodeType.name === CHAT_TRIGGER_NODE_TYPE);
|
||||
},
|
||||
isFormTriggerNode(): boolean {
|
||||
return Boolean(this.nodeType && this.nodeType.name === FORM_TRIGGER_NODE_TYPE);
|
||||
},
|
||||
@@ -186,6 +191,10 @@ export default defineComponent({
|
||||
return this.label;
|
||||
}
|
||||
|
||||
if (this.isChatNode) {
|
||||
return this.$locale.baseText('ndv.execute.testChat');
|
||||
}
|
||||
|
||||
if (this.isWebhookNode) {
|
||||
return this.$locale.baseText('ndv.execute.listenForTestEvent');
|
||||
}
|
||||
@@ -212,7 +221,10 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
async onClick() {
|
||||
if (this.isListeningForEvents) {
|
||||
if (this.isChatNode) {
|
||||
this.ndvStore.setActiveNodeName(null);
|
||||
nodeViewEventBus.emit('openChat');
|
||||
} else if (this.isListeningForEvents) {
|
||||
await this.stopWaitingForWebhook();
|
||||
} else if (this.isListeningForWorkflowEvents) {
|
||||
this.$emit('stopExecution');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="webhooksNode.length" class="webhooks">
|
||||
<div v-if="webhooksNode.length && visibleWebhookUrls.length > 0" class="webhooks">
|
||||
<div
|
||||
class="clickable headline"
|
||||
:class="{ expanded: !isMinimized }"
|
||||
@@ -11,31 +11,22 @@
|
||||
</div>
|
||||
<el-collapse-transition>
|
||||
<div v-if="!isMinimized" class="node-webhooks">
|
||||
<div class="url-selection">
|
||||
<div v-if="!isProductionOnly" class="url-selection">
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<n8n-radio-buttons
|
||||
v-model="showUrlFor"
|
||||
:options="[
|
||||
{ label: baseText.testUrl, value: 'test' },
|
||||
{
|
||||
label: baseText.productionUrl,
|
||||
value: 'production',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<n8n-radio-buttons v-model="showUrlFor" :options="urlOptions" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<n8n-tooltip
|
||||
v-for="(webhook, index) in webhooksNode.filter((webhook) => !webhook.ndvHideUrl)"
|
||||
v-for="(webhook, index) in visibleWebhookUrls"
|
||||
:key="index"
|
||||
class="item"
|
||||
:content="baseText.clickToCopy"
|
||||
placement="left"
|
||||
>
|
||||
<div v-if="!webhook.ndvHideMethod" class="webhook-wrapper">
|
||||
<div v-if="isWebhookMethodVisible(webhook)" class="webhook-wrapper">
|
||||
<div class="http-field">
|
||||
<div class="http-method">
|
||||
{{ getWebhookExpressionValue(webhook, 'httpMethod') }}<br />
|
||||
@@ -65,7 +56,12 @@ import type { INodeTypeDescription, IWebhookDescription } from 'n8n-workflow';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { FORM_TRIGGER_NODE_TYPE, OPEN_URL_PANEL_TRIGGER_NODE_TYPES } from '@/constants';
|
||||
import {
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
FORM_TRIGGER_NODE_TYPE,
|
||||
OPEN_URL_PANEL_TRIGGER_NODE_TYPES,
|
||||
PRODUCTION_ONLY_TRIGGER_NODE_TYPES,
|
||||
} from '@/constants';
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
|
||||
@@ -91,6 +87,27 @@ export default defineComponent({
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isProductionOnly(): boolean {
|
||||
return this.nodeType && PRODUCTION_ONLY_TRIGGER_NODE_TYPES.includes(this.nodeType.name);
|
||||
},
|
||||
urlOptions(): Array<{ label: string; value: string }> {
|
||||
return [
|
||||
...(this.isProductionOnly ? [] : [{ label: this.baseText.testUrl, value: 'test' }]),
|
||||
{
|
||||
label: this.baseText.productionUrl,
|
||||
value: 'production',
|
||||
},
|
||||
];
|
||||
},
|
||||
visibleWebhookUrls(): IWebhookDescription[] {
|
||||
return this.webhooksNode.filter((webhook) => {
|
||||
if (typeof webhook.ndvHideUrl === 'string') {
|
||||
return !this.getWebhookExpressionValue(webhook, 'ndvHideUrl');
|
||||
}
|
||||
|
||||
return !webhook.ndvHideUrl;
|
||||
});
|
||||
},
|
||||
webhooksNode(): IWebhookDescription[] {
|
||||
if (this.nodeType === null || this.nodeType.webhooks === undefined) {
|
||||
return [];
|
||||
@@ -103,6 +120,20 @@ export default defineComponent({
|
||||
baseText() {
|
||||
const nodeType = this.nodeType.name;
|
||||
switch (nodeType) {
|
||||
case CHAT_TRIGGER_NODE_TYPE:
|
||||
return {
|
||||
toggleTitle: this.$locale.baseText('nodeWebhooks.webhookUrls.chatTrigger'),
|
||||
clickToDisplay: this.$locale.baseText(
|
||||
'nodeWebhooks.clickToDisplayWebhookUrls.formTrigger',
|
||||
),
|
||||
clickToHide: this.$locale.baseText('nodeWebhooks.clickToHideWebhookUrls.chatTrigger'),
|
||||
clickToCopy: this.$locale.baseText('nodeWebhooks.clickToCopyWebhookUrls.chatTrigger'),
|
||||
testUrl: this.$locale.baseText('nodeWebhooks.testUrl'),
|
||||
productionUrl: this.$locale.baseText('nodeWebhooks.productionUrl'),
|
||||
copyTitle: this.$locale.baseText('nodeWebhooks.showMessage.title.chatTrigger'),
|
||||
copyMessage: this.$locale.baseText('nodeWebhooks.showMessage.message.chatTrigger'),
|
||||
};
|
||||
|
||||
case FORM_TRIGGER_NODE_TYPE:
|
||||
return {
|
||||
toggleTitle: this.$locale.baseText('nodeWebhooks.webhookUrls.formTrigger'),
|
||||
@@ -153,10 +184,21 @@ export default defineComponent({
|
||||
},
|
||||
getWebhookUrlDisplay(webhookData: IWebhookDescription): string {
|
||||
if (this.node) {
|
||||
return this.getWebhookUrl(webhookData, this.node, this.showUrlFor);
|
||||
return this.getWebhookUrl(
|
||||
webhookData,
|
||||
this.node,
|
||||
this.isProductionOnly ? 'production' : this.showUrlFor,
|
||||
);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
isWebhookMethodVisible(webhook: IWebhookDescription): boolean {
|
||||
if (typeof webhook.ndvHideMethod === 'string') {
|
||||
return !this.getWebhookExpressionValue(webhook, 'ndvHideMethod');
|
||||
}
|
||||
|
||||
return !webhook.ndvHideMethod;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -168,7 +168,6 @@ import {
|
||||
isAuthRelatedParameter,
|
||||
} from '@/utils/nodeTypesUtils';
|
||||
import { get, set } from 'lodash-es';
|
||||
import { nodeViewEventBus } from '@/event-bus';
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
|
||||
const FixedCollectionParameter = defineAsyncComponent(
|
||||
@@ -476,14 +475,16 @@ export default defineComponent({
|
||||
this.$emit('activate');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Handles default node button parameter type actions
|
||||
* @param parameter
|
||||
*/
|
||||
onButtonAction(parameter: INodeProperties) {
|
||||
const action: string | undefined = parameter.typeOptions?.action;
|
||||
|
||||
switch (action) {
|
||||
case 'openChat':
|
||||
this.ndvStore.setActiveNodeName(null);
|
||||
nodeViewEventBus.emit('openChat');
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
isNodeAuthField(name: string): boolean {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<transition name="fade" mode="out-in">
|
||||
<div v-if="hasIssues" key="empty"></div>
|
||||
<div v-if="hasIssues || hideContent" key="empty"></div>
|
||||
<div v-else-if="isListeningForEvents" key="listening">
|
||||
<n8n-pulse>
|
||||
<NodeIcon :node-type="nodeType" :size="40"></NodeIcon>
|
||||
@@ -45,6 +45,12 @@
|
||||
{{ listeningHint }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
<div v-if="displayChatButton">
|
||||
<n8n-button @click="openWebhookUrl()" class="mb-xl">
|
||||
{{ $locale.baseText('ndv.trigger.chatTrigger.openChat') }}
|
||||
</n8n-button>
|
||||
</div>
|
||||
|
||||
<NodeExecuteButton
|
||||
data-test-id="trigger-execute-button"
|
||||
:node-name="nodeName"
|
||||
@@ -105,6 +111,7 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import {
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
VIEWS,
|
||||
WEBHOOK_NODE_TYPE,
|
||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||
@@ -156,6 +163,36 @@ export default defineComponent({
|
||||
|
||||
return null;
|
||||
},
|
||||
hideContent(): boolean {
|
||||
if (!this.nodeType?.triggerPanel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.nodeType?.triggerPanel &&
|
||||
this.nodeType?.triggerPanel.hasOwnProperty('hideContent')
|
||||
) {
|
||||
const hideContent = this.nodeType?.triggerPanel.hideContent;
|
||||
if (typeof hideContent === 'boolean') {
|
||||
return hideContent;
|
||||
}
|
||||
|
||||
if (this.node) {
|
||||
const hideContentValue = this.getCurrentWorkflow().expression.getSimpleParameterValue(
|
||||
this.node,
|
||||
hideContent,
|
||||
'internal',
|
||||
{},
|
||||
);
|
||||
|
||||
if (typeof hideContentValue === 'boolean') {
|
||||
return hideContentValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
hasIssues(): boolean {
|
||||
return Boolean(
|
||||
this.node?.issues && (this.node.issues.parameters || this.node.issues.credentials),
|
||||
@@ -168,6 +205,13 @@ export default defineComponent({
|
||||
|
||||
return '';
|
||||
},
|
||||
displayChatButton(): boolean {
|
||||
return Boolean(
|
||||
this.node &&
|
||||
this.node.type === CHAT_TRIGGER_NODE_TYPE &&
|
||||
this.node.parameters.mode !== 'webhook',
|
||||
);
|
||||
},
|
||||
isWebhookNode(): boolean {
|
||||
return Boolean(this.node && this.node.type === WEBHOOK_NODE_TYPE);
|
||||
},
|
||||
@@ -219,11 +263,16 @@ export default defineComponent({
|
||||
: this.$locale.baseText('ndv.trigger.webhookNode.listening');
|
||||
},
|
||||
listeningHint(): string {
|
||||
return this.nodeType?.name === FORM_TRIGGER_NODE_TYPE
|
||||
? this.$locale.baseText('ndv.trigger.webhookBasedNode.formTrigger.serviceHint')
|
||||
: this.$locale.baseText('ndv.trigger.webhookBasedNode.serviceHint', {
|
||||
switch (this.nodeType?.name) {
|
||||
case CHAT_TRIGGER_NODE_TYPE:
|
||||
return this.$locale.baseText('ndv.trigger.webhookBasedNode.chatTrigger.serviceHint');
|
||||
case FORM_TRIGGER_NODE_TYPE:
|
||||
return this.$locale.baseText('ndv.trigger.webhookBasedNode.formTrigger.serviceHint');
|
||||
default:
|
||||
return this.$locale.baseText('ndv.trigger.webhookBasedNode.serviceHint', {
|
||||
interpolate: { service: this.serviceName },
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
header(): string {
|
||||
const serviceName = this.nodeType ? getTriggerNodeServiceName(this.nodeType) : '';
|
||||
@@ -349,6 +398,15 @@ export default defineComponent({
|
||||
this.executionsHelpEventBus.emit('expand');
|
||||
}
|
||||
},
|
||||
openWebhookUrl() {
|
||||
this.$telemetry.track('User clicked ndv link', {
|
||||
workflow_id: this.workflowsStore.workflowId,
|
||||
session_id: this.sessionId,
|
||||
pane: 'input',
|
||||
type: 'open-chat',
|
||||
});
|
||||
window.open(this.webhookTestUrl, '_blank', 'noreferrer');
|
||||
},
|
||||
onLinkClick(e: MouseEvent) {
|
||||
if (!e.target) {
|
||||
return;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
@keydown.stop
|
||||
>
|
||||
<template #content>
|
||||
<div v-loading="isLoading" class="workflow-lm-chat" data-test-id="workflow-lm-chat-dialog">
|
||||
<div class="workflow-lm-chat" data-test-id="workflow-lm-chat-dialog">
|
||||
<div class="messages ignore-key-press">
|
||||
<div
|
||||
v-for="message in messages"
|
||||
@@ -64,6 +64,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MessageTyping ref="messageContainer" v-if="isLoading" />
|
||||
</div>
|
||||
<div v-if="node" class="logs-wrapper" data-test-id="lm-chat-logs">
|
||||
<n8n-text class="logs-title" tag="p" size="large">{{
|
||||
@@ -128,6 +129,7 @@ import {
|
||||
AI_CODE_NODE_TYPE,
|
||||
AI_SUBCATEGORY,
|
||||
CHAT_EMBED_MODAL_KEY,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||
VIEWS,
|
||||
WORKFLOW_LM_CHAT_MODAL_KEY,
|
||||
@@ -137,13 +139,17 @@ import { workflowRun } from '@/mixins/workflowRun';
|
||||
import { get, last } from 'lodash-es';
|
||||
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
import type { IDataObject, INodeType, INode, ITaskData } from 'n8n-workflow';
|
||||
import { NodeHelpers, NodeConnectionType } from 'n8n-workflow';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import type { INodeUi, IUser } from '@/Interface';
|
||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import MessageTyping from '@n8n/chat/components/MessageTyping.vue';
|
||||
|
||||
const RunDataAi = defineAsyncComponent(async () => import('@/components/RunDataAi/RunDataAi.vue'));
|
||||
|
||||
interface ChatMessage {
|
||||
@@ -167,6 +173,7 @@ export default defineComponent({
|
||||
name: 'WorkflowLMChat',
|
||||
components: {
|
||||
Modal,
|
||||
MessageTyping,
|
||||
RunDataAi,
|
||||
},
|
||||
mixins: [workflowRun],
|
||||
@@ -246,18 +253,17 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
setConnectedNode() {
|
||||
const workflow = this.getCurrentWorkflow();
|
||||
const triggerNode = workflow.queryNodes(
|
||||
(nodeType: INodeType) => nodeType.description.name === MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||
);
|
||||
const triggerNode = this.getTriggerNode();
|
||||
|
||||
if (!triggerNode.length) {
|
||||
if (!triggerNode) {
|
||||
this.showError(
|
||||
new Error('Chat Trigger Node could not be found!'),
|
||||
'Trigger Node not found',
|
||||
);
|
||||
return;
|
||||
}
|
||||
const workflow = this.getCurrentWorkflow();
|
||||
|
||||
const chatNode = this.workflowsStore.getNodes().find((node: INodeUi): boolean => {
|
||||
const nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||
if (!nodeType) return false;
|
||||
@@ -288,7 +294,7 @@ export default defineComponent({
|
||||
|
||||
const parentNodes = workflow.getParentNodes(node.name);
|
||||
const isChatChild = parentNodes.some(
|
||||
(parentNodeName) => parentNodeName === triggerNode[0].name,
|
||||
(parentNodeName) => parentNodeName === triggerNode.name,
|
||||
);
|
||||
|
||||
return Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent));
|
||||
@@ -311,7 +317,7 @@ export default defineComponent({
|
||||
|
||||
const workflow = this.getCurrentWorkflow();
|
||||
const connectedMemoryInputs =
|
||||
workflow.connectionsByDestinationNode[this.connectedNode.name]?.memory;
|
||||
workflow.connectionsByDestinationNode[this.connectedNode.name][NodeConnectionType.AiMemory];
|
||||
if (!connectedMemoryInputs) return [];
|
||||
|
||||
const memoryConnection = (connectedMemoryInputs ?? []).find((i) => i.length > 0)?.[0];
|
||||
@@ -330,9 +336,9 @@ export default defineComponent({
|
||||
action: string;
|
||||
chatHistory?: unknown[];
|
||||
response?: {
|
||||
chat_history?: unknown[];
|
||||
sessionId?: unknown[];
|
||||
};
|
||||
} => get(data, 'data.memory.0.0.json')!,
|
||||
} => get(data, ['data', NodeConnectionType.AiMemory, 0, 0, 'json'])!,
|
||||
)
|
||||
?.find((data) =>
|
||||
['chatHistory', 'loadMemoryVariables'].includes(data?.action) ? data : undefined,
|
||||
@@ -342,12 +348,12 @@ export default defineComponent({
|
||||
if (memoryOutputData?.chatHistory) {
|
||||
chatHistory = memoryOutputData?.chatHistory as LangChainMessage[];
|
||||
} else if (memoryOutputData?.response) {
|
||||
chatHistory = memoryOutputData?.response.chat_history as LangChainMessage[];
|
||||
chatHistory = memoryOutputData?.response.sessionId as LangChainMessage[];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
return chatHistory.map((message) => {
|
||||
return (chatHistory || []).map((message) => {
|
||||
return {
|
||||
text: message.kwargs.content,
|
||||
sender: last(message.id) === 'HumanMessage' ? 'user' : 'bot',
|
||||
@@ -382,8 +388,8 @@ export default defineComponent({
|
||||
|
||||
getTriggerNode(): INode | null {
|
||||
const workflow = this.getCurrentWorkflow();
|
||||
const triggerNode = workflow.queryNodes(
|
||||
(nodeType: INodeType) => nodeType.description.name === MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||
const triggerNode = workflow.queryNodes((nodeType: INodeType) =>
|
||||
[CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE].includes(nodeType.description.name),
|
||||
);
|
||||
|
||||
if (!triggerNode.length) {
|
||||
@@ -403,7 +409,16 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
const inputKey = triggerNode.typeVersion < 1.1 ? 'input' : 'chat_input';
|
||||
let inputKey = 'chatInput';
|
||||
if (triggerNode.type === MANUAL_CHAT_TRIGGER_NODE_TYPE && triggerNode.typeVersion < 1.1) {
|
||||
inputKey = 'input';
|
||||
}
|
||||
if (triggerNode.type === CHAT_TRIGGER_NODE_TYPE) {
|
||||
inputKey = 'chatInput';
|
||||
}
|
||||
|
||||
const usersStore = useUsersStore();
|
||||
const currentUser = usersStore.currentUser ?? ({} as IUser);
|
||||
|
||||
const nodeData: ITaskData = {
|
||||
startTime: new Date().getTime(),
|
||||
@@ -414,6 +429,8 @@ export default defineComponent({
|
||||
[
|
||||
{
|
||||
json: {
|
||||
sessionId: `test-${currentUser.id || 'unknown'}`,
|
||||
action: 'sendMessage',
|
||||
[inputKey]: message,
|
||||
},
|
||||
},
|
||||
@@ -549,13 +566,18 @@ export default defineComponent({
|
||||
padding-top: 1.5em;
|
||||
margin-right: 1em;
|
||||
|
||||
.chat-message {
|
||||
float: left;
|
||||
margin: var(--spacing-2xs) var(--spacing-s);
|
||||
}
|
||||
|
||||
.message {
|
||||
float: left;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.content {
|
||||
border-radius: var(--border-radius-large);
|
||||
border-radius: var(--border-radius-base);
|
||||
line-height: 1.5;
|
||||
margin: var(--spacing-2xs) var(--spacing-s);
|
||||
max-width: 75%;
|
||||
@@ -565,8 +587,9 @@ export default defineComponent({
|
||||
|
||||
&.bot {
|
||||
background-color: var(--color-lm-chat-bot-background);
|
||||
border: 1px solid var(--color-lm-chat-bot-border);
|
||||
color: var(--color-lm-chat-bot-color);
|
||||
float: left;
|
||||
border-bottom-left-radius: 0;
|
||||
|
||||
.message-options {
|
||||
left: 1.5em;
|
||||
@@ -575,9 +598,10 @@ export default defineComponent({
|
||||
|
||||
&.user {
|
||||
background-color: var(--color-lm-chat-user-background);
|
||||
border: 1px solid var(--color-lm-chat-user-border);
|
||||
color: var(--color-lm-chat-user-color);
|
||||
float: right;
|
||||
text-align: right;
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
.message-options {
|
||||
right: 1.5em;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import WorkflowLMChatModal from '@/components/WorkflowLMChat.vue';
|
||||
import {
|
||||
AGENT_NODE_TYPE,
|
||||
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||
WORKFLOW_LM_CHAT_MODAL_KEY,
|
||||
} from '@/constants';
|
||||
import { AGENT_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE, WORKFLOW_LM_CHAT_MODAL_KEY } from '@/constants';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { fireEvent, waitFor } from '@testing-library/vue';
|
||||
import { uuid } from '@jsplumb/util';
|
||||
@@ -32,7 +28,7 @@ async function createPiniaWithAINodes(options = { withConnections: true, withAge
|
||||
name: 'Test Workflow',
|
||||
connections: withConnections
|
||||
? {
|
||||
'On new manual Chat Message': {
|
||||
'Chat Trigger': {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
@@ -48,8 +44,8 @@ async function createPiniaWithAINodes(options = { withConnections: true, withAge
|
||||
active: true,
|
||||
nodes: [
|
||||
createTestNode({
|
||||
name: 'On new manual Chat Message',
|
||||
type: MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||
name: 'Chat Trigger',
|
||||
type: CHAT_TRIGGER_NODE_TYPE,
|
||||
}),
|
||||
...(withAgentNode
|
||||
? [
|
||||
@@ -71,7 +67,7 @@ async function createPiniaWithAINodes(options = { withConnections: true, withAge
|
||||
|
||||
nodeTypesStore.setNodeTypes(
|
||||
mockNodeTypesToArray({
|
||||
[MANUAL_CHAT_TRIGGER_NODE_TYPE]: testingNodeTypes[MANUAL_CHAT_TRIGGER_NODE_TYPE],
|
||||
[CHAT_TRIGGER_NODE_TYPE]: testingNodeTypes[CHAT_TRIGGER_NODE_TYPE],
|
||||
[AGENT_NODE_TYPE]: testingNodeTypes[AGENT_NODE_TYPE],
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -128,6 +128,7 @@ export const JIRA_TRIGGER_NODE_TYPE = 'n8n-nodes-base.jiraTrigger';
|
||||
export const MICROSOFT_EXCEL_NODE_TYPE = 'n8n-nodes-base.microsoftExcel';
|
||||
export const MANUAL_TRIGGER_NODE_TYPE = 'n8n-nodes-base.manualTrigger';
|
||||
export const MANUAL_CHAT_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.manualChatTrigger';
|
||||
export const CHAT_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.chatTrigger';
|
||||
export const AGENT_NODE_TYPE = '@n8n/n8n-nodes-langchain.agent';
|
||||
export const OPEN_AI_ASSISTANT_NODE_TYPE = '@n8n/n8n-nodes-langchain.openAiAssistant';
|
||||
export const BASIC_CHAIN_NODE_TYPE = '@n8n/n8n-nodes-langchain.chainLlm';
|
||||
@@ -200,7 +201,13 @@ export const NODES_USING_CODE_NODE_EDITOR = [CODE_NODE_TYPE, AI_CODE_NODE_TYPE];
|
||||
|
||||
export const PIN_DATA_NODE_TYPES_DENYLIST = [SPLIT_IN_BATCHES_NODE_TYPE, STICKY_NODE_TYPE];
|
||||
|
||||
export const OPEN_URL_PANEL_TRIGGER_NODE_TYPES = [WEBHOOK_NODE_TYPE, FORM_TRIGGER_NODE_TYPE];
|
||||
export const OPEN_URL_PANEL_TRIGGER_NODE_TYPES = [
|
||||
WEBHOOK_NODE_TYPE,
|
||||
FORM_TRIGGER_NODE_TYPE,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
];
|
||||
|
||||
export const PRODUCTION_ONLY_TRIGGER_NODE_TYPES = [CHAT_TRIGGER_NODE_TYPE];
|
||||
|
||||
// Node creator
|
||||
export const NODE_CREATOR_OPEN_SOURCES: Record<
|
||||
@@ -614,6 +621,7 @@ export const KEEP_AUTH_IN_NDV_FOR_NODES = [
|
||||
WEBHOOK_NODE_TYPE,
|
||||
WAIT_NODE_TYPE,
|
||||
DISCORD_NODE_TYPE,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
];
|
||||
export const MAIN_AUTH_FIELD_NAME = 'authentication';
|
||||
export const NODE_RESOURCE_FIELD_NAME = 'resource';
|
||||
|
||||
@@ -168,6 +168,13 @@ export function resolveParameter(
|
||||
nodeConnection,
|
||||
);
|
||||
|
||||
if (_connectionInputData === null && contextNode && activeNode?.name !== contextNode.name) {
|
||||
// For Sub-Nodes connected to Trigger-Nodes use the data of the root-node
|
||||
// (Gets for example used by the Memory connected to the Chat-Trigger-Node)
|
||||
const _executeData = executeData([contextNode.name], contextNode.name, inputName, 0);
|
||||
_connectionInputData = get(_executeData, ['data', inputName, 0], null);
|
||||
}
|
||||
|
||||
let runExecutionData: IRunExecutionData;
|
||||
if (!executionData?.data) {
|
||||
runExecutionData = {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@import '@n8n/chat/css';
|
||||
@import 'styles/plugins';
|
||||
|
||||
:root {
|
||||
@@ -170,6 +171,8 @@
|
||||
var(--node-type-background-l)
|
||||
);
|
||||
--node-error-output-color: #991818;
|
||||
|
||||
--chat--spacing: var(--spacing-s);
|
||||
}
|
||||
|
||||
.clickable {
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
"chatEmbed.paste.other.file": "main.ts",
|
||||
"chatEmbed.packageInfo.description": "The n8n Chat widget can be easily customized to fit your needs.",
|
||||
"chatEmbed.packageInfo.link": "Read the full documentation",
|
||||
"chatEmbed.chatTriggerNode": "You can use a Chat Trigger Node to embed the chat widget directly into n8n.",
|
||||
"chatEmbed.url": "https://www.npmjs.com/package/{'@'}n8n/chat",
|
||||
"codeEdit.edit": "Edit",
|
||||
"codeNodeEditor.askAi": "✨ Ask AI",
|
||||
@@ -759,6 +760,7 @@
|
||||
"ndv.execute.fetchEvent": "Fetch Test Event",
|
||||
"ndv.execute.fixPrevious": "Fix previous node first",
|
||||
"ndv.execute.listenForTestEvent": "Listen For Test Event",
|
||||
"ndv.execute.testChat": "Test Chat",
|
||||
"ndv.execute.testStep": "Test Step",
|
||||
"ndv.execute.stopListening": "Stop Listening",
|
||||
"ndv.execute.nodeIsDisabled": "Enable node to execute",
|
||||
@@ -1116,18 +1118,24 @@
|
||||
"contextMenu.changeColor": "Change color",
|
||||
"nodeWebhooks.clickToCopyWebhookUrls": "Click to copy webhook URLs",
|
||||
"nodeWebhooks.clickToCopyWebhookUrls.formTrigger": "Click to copy Form URL",
|
||||
"nodeWebhooks.clickToCopyWebhookUrls.chatTrigger": "Click to copy Chat URL",
|
||||
"nodeWebhooks.clickToDisplayWebhookUrls": "Click to display webhook URLs",
|
||||
"nodeWebhooks.clickToDisplayWebhookUrls.formTrigger": "Click to display Form URL",
|
||||
"nodeWebhooks.clickToDisplayWebhookUrls.chatTrigger": "Click to display Chat URL",
|
||||
"nodeWebhooks.clickToHideWebhookUrls": "Click to hide webhook URLs",
|
||||
"nodeWebhooks.clickToHideWebhookUrls.formTrigger": "Click to hide Form URL",
|
||||
"nodeWebhooks.clickToHideWebhookUrls.chatTrigger": "Click to hide Chat URL",
|
||||
"nodeWebhooks.invalidExpression": "[INVALID EXPRESSION]",
|
||||
"nodeWebhooks.productionUrl": "Production URL",
|
||||
"nodeWebhooks.showMessage.title": "URL copied",
|
||||
"nodeWebhooks.showMessage.title.formTrigger": "Form URL copied",
|
||||
"nodeWebhooks.showMessage.title.chatTrigger": "Chat URL copied",
|
||||
"nodeWebhooks.showMessage.message.formTrigger": "Form submissions made via this URL will trigger the workflow when it's activated",
|
||||
"nodeWebhooks.showMessage.message.chatTrigger": "Chat submissions made via this URL will trigger the workflow when it's activated",
|
||||
"nodeWebhooks.testUrl": "Test URL",
|
||||
"nodeWebhooks.webhookUrls": "Webhook URLs",
|
||||
"nodeWebhooks.webhookUrls.formTrigger": "Form URLs",
|
||||
"nodeWebhooks.webhookUrls.chatTrigger": "Chat URL",
|
||||
"onboardingCallSignupModal.title": "Your onboarding session",
|
||||
"onboardingCallSignupModal.description": "Pop in your email and we'll send you some scheduling options",
|
||||
"onboardingCallSignupModal.emailInput.placeholder": "Your work email",
|
||||
@@ -1803,10 +1811,12 @@
|
||||
"ndv.trigger.webhookBasedNode.executionsHelp.inactive": "<b>While building your workflow</b>, click the 'listen' button, then go to {service} and make an event happen. This will trigger an execution, which will show up in this editor.<br /> <br /> <b>Once you're happy with your workflow</b>, <a data-key=\"activate\">activate</a> it. Then every time there's a matching event in {service}, the workflow will execute. These executions will show up in the <a data-key=\"executions\">executions list</a>, but not in the editor.",
|
||||
"ndv.trigger.webhookBasedNode.executionsHelp.active": "<b>While building your workflow</b>, click the 'listen' button, then go to {service} and make an event happen. This will trigger an execution, which will show up in this editor.<br /> <br /> <b>Your workflow will also execute automatically</b>, since it's activated. Every time there’s a matching event in {service}, this node will trigger an execution. These executions will show up in the <a data-key=\"executions\">executions list</a>, but not in the editor. ",
|
||||
"ndv.trigger.webhookNode.listening": "Listening for test event",
|
||||
"ndv.trigger.chatTrigger.openChat": "Open Chat Window",
|
||||
"ndv.trigger.webhookNode.formTrigger.listening": "Listening for a test form submission",
|
||||
"ndv.trigger.webhookBasedNode.listening": "Listening for your trigger event",
|
||||
"ndv.trigger.webhookNode.requestHint": "Make a {type} request to:",
|
||||
"ndv.trigger.webhookBasedNode.serviceHint": "Go to {service} and create an event",
|
||||
"ndv.trigger.webhookBasedNode.chatTrigger.serviceHint": "Send a message in the chat",
|
||||
"ndv.trigger.webhookBasedNode.formTrigger.serviceHint": "Submit the test form that just opened in a new tab",
|
||||
"ndv.trigger.webhookBasedNode.activationHint.inactive": "Once you’ve finished building your workflow, <a data-key=\"activate\">activate it</a> to have it also listen continuously (you just won’t see those executions here).",
|
||||
"ndv.trigger.webhookBasedNode.activationHint.active": "This node will also trigger automatically on new {service} events (but those executions won’t show up here).",
|
||||
|
||||
@@ -7,7 +7,7 @@ export const guestMiddleware: RouterMiddleware<GuestPermissionOptions> = async (
|
||||
const valid = isGuest();
|
||||
if (!valid) {
|
||||
const redirect = to.query.redirect as string;
|
||||
if (redirect && redirect.startsWith('/')) {
|
||||
if (redirect && (redirect.startsWith('/') || redirect.startsWith(window.location.origin))) {
|
||||
return next(redirect);
|
||||
}
|
||||
|
||||
|
||||
@@ -237,6 +237,7 @@ import {
|
||||
EnterpriseEditionFeature,
|
||||
REGULAR_NODE_CREATOR_VIEW,
|
||||
NODE_CREATOR_OPEN_SOURCES,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||
WORKFLOW_LM_CHAT_MODAL_KEY,
|
||||
AI_NODE_CREATOR_VIEW,
|
||||
@@ -692,12 +693,14 @@ export default defineComponent({
|
||||
containsTrigger(): boolean {
|
||||
return this.triggerNodes.length > 0;
|
||||
},
|
||||
isManualChatOnly(): boolean {
|
||||
return this.containsChatNodes && this.triggerNodes.length === 1;
|
||||
},
|
||||
containsChatNodes(): boolean {
|
||||
return !!this.nodes.find(
|
||||
(node) => node.type === MANUAL_CHAT_TRIGGER_NODE_TYPE && node.disabled !== true,
|
||||
return (
|
||||
!this.executionWaitingForWebhook &&
|
||||
!!this.nodes.find(
|
||||
(node) =>
|
||||
[MANUAL_CHAT_TRIGGER_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE].includes(node.type) &&
|
||||
node.disabled !== true,
|
||||
)
|
||||
);
|
||||
},
|
||||
isExecutionDisabled(): boolean {
|
||||
|
||||
@@ -116,7 +116,7 @@ export default defineComponent({
|
||||
},
|
||||
isRedirectSafe() {
|
||||
const redirect = this.getRedirectQueryParameter();
|
||||
return redirect.startsWith('/');
|
||||
return redirect.startsWith('/') || redirect.startsWith(window.location.origin);
|
||||
},
|
||||
getRedirectQueryParameter() {
|
||||
let redirect = '';
|
||||
@@ -152,6 +152,11 @@ export default defineComponent({
|
||||
|
||||
if (this.isRedirectSafe()) {
|
||||
const redirect = this.getRedirectQueryParameter();
|
||||
if (redirect.startsWith('http')) {
|
||||
window.location.href = redirect;
|
||||
return;
|
||||
}
|
||||
|
||||
void this.$router.push(redirect);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"types": ["vitest/globals"],
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"n8n-design-system/*": ["../design-system/src/*"]
|
||||
"n8n-design-system/*": ["../design-system/src/*"],
|
||||
"@n8n/chat/*": ["../@n8n/chat/src/*"]
|
||||
},
|
||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"],
|
||||
// TODO: remove all options below this line
|
||||
|
||||
@@ -5,14 +5,16 @@ import { sentryVitePlugin } from '@sentry/vite-plugin';
|
||||
|
||||
import packageJSON from './package.json';
|
||||
import { vitestConfig } from '../design-system/vite.config.mts';
|
||||
import icons from 'unplugin-icons/vite';
|
||||
|
||||
const vendorChunks = ['vue', 'vue-router'];
|
||||
const n8nChunks = ['n8n-workflow', 'n8n-design-system'];
|
||||
const n8nChunks = ['n8n-workflow', 'n8n-design-system', '@n8n/chat'];
|
||||
const ignoreChunks = [
|
||||
'@fontsource/open-sans',
|
||||
'@vueuse/components',
|
||||
// TODO: remove this. It's currently required by xml2js in NodeErrors
|
||||
'stream-browserify',
|
||||
'vue-markdown-render',
|
||||
];
|
||||
|
||||
const isScopedPackageToIgnore = (str: string) => /@codemirror\//.test(str);
|
||||
@@ -49,6 +51,14 @@ const alias = [
|
||||
find: /^n8n-design-system\//,
|
||||
replacement: resolve(__dirname, '..', 'design-system', 'src') + '/',
|
||||
},
|
||||
{
|
||||
find: /^@n8n\/chat$/,
|
||||
replacement: resolve(__dirname, '..', '@n8n', 'chat', 'src', 'index.ts'),
|
||||
},
|
||||
{
|
||||
find: /^@n8n\/chat\//,
|
||||
replacement: resolve(__dirname, '..', '@n8n', 'chat', 'src') + '/',
|
||||
},
|
||||
...['orderBy', 'camelCase', 'cloneDeep', 'startCase'].map((name) => ({
|
||||
find: new RegExp(`^lodash.${name}$`, 'i'),
|
||||
replacement: `lodash-es/${name}`,
|
||||
@@ -59,7 +69,12 @@ const alias = [
|
||||
},
|
||||
];
|
||||
|
||||
const plugins = [vue()];
|
||||
const plugins = [
|
||||
icons({
|
||||
compiler: 'vue3',
|
||||
}),
|
||||
vue()
|
||||
];
|
||||
|
||||
const { SENTRY_AUTH_TOKEN: authToken, RELEASE: release } = process.env;
|
||||
if (release && authToken) {
|
||||
|
||||
Reference in New Issue
Block a user