mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
547 lines
13 KiB
Vue
547 lines
13 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref } from 'vue';
|
|
import {
|
|
CHAT_TRIGGER_NODE_TYPE,
|
|
VIEWS,
|
|
WEBHOOK_NODE_TYPE,
|
|
WORKFLOW_SETTINGS_MODAL_KEY,
|
|
FORM_TRIGGER_NODE_TYPE,
|
|
} from '@/constants';
|
|
import type { INodeUi } from '@/Interface';
|
|
import type { INodeTypeDescription } from 'n8n-workflow';
|
|
import { getTriggerNodeServiceName } from '@/utils/nodeTypesUtils';
|
|
import NodeExecuteButton from '@/components/NodeExecuteButton.vue';
|
|
import CopyInput from '@/components/CopyInput.vue';
|
|
import NodeIcon from '@/components/NodeIcon.vue';
|
|
import { useUIStore } from '@/stores/ui.store';
|
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
import { useNDVStore } from '@/stores/ndv.store';
|
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
|
import { createEventBus } from '@n8n/utils/event-bus';
|
|
import { useRouter } from 'vue-router';
|
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
|
import { isTriggerPanelObject } from '@/utils/typeGuards';
|
|
import { useI18n } from '@n8n/i18n';
|
|
import { useTelemetry } from '@/composables/useTelemetry';
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
nodeName: string;
|
|
pushRef: string;
|
|
}>(),
|
|
{
|
|
pushRef: '',
|
|
},
|
|
);
|
|
|
|
const emit = defineEmits<{
|
|
activate: [];
|
|
execute: [];
|
|
}>();
|
|
|
|
const nodesTypeStore = useNodeTypesStore();
|
|
const uiStore = useUIStore();
|
|
const workflowsStore = useWorkflowsStore();
|
|
const ndvStore = useNDVStore();
|
|
|
|
const router = useRouter();
|
|
const workflowHelpers = useWorkflowHelpers();
|
|
const i18n = useI18n();
|
|
const telemetry = useTelemetry();
|
|
|
|
const executionsHelpEventBus = createEventBus();
|
|
|
|
const help = ref<HTMLElement | null>(null);
|
|
|
|
const node = computed<INodeUi | null>(() => workflowsStore.getNodeByName(props.nodeName));
|
|
|
|
const nodeType = computed<INodeTypeDescription | null>(() => {
|
|
if (node.value) {
|
|
return nodesTypeStore.getNodeType(node.value.type, node.value.typeVersion);
|
|
}
|
|
|
|
return null;
|
|
});
|
|
|
|
const triggerPanel = computed(() => {
|
|
const panel = nodeType.value?.triggerPanel;
|
|
if (isTriggerPanelObject(panel)) {
|
|
return panel;
|
|
}
|
|
return undefined;
|
|
});
|
|
|
|
const hideContent = computed(() => {
|
|
const hideContent = triggerPanel.value?.hideContent;
|
|
if (typeof hideContent === 'boolean') {
|
|
return hideContent;
|
|
}
|
|
|
|
if (node.value) {
|
|
const hideContentValue = workflowHelpers
|
|
.getCurrentWorkflow()
|
|
.expression.getSimpleParameterValue(node.value, hideContent, 'internal', {});
|
|
|
|
if (typeof hideContentValue === 'boolean') {
|
|
return hideContentValue;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
const hasIssues = computed(() => {
|
|
return Boolean(
|
|
node.value?.issues && (node.value.issues.parameters ?? node.value.issues.credentials),
|
|
);
|
|
});
|
|
|
|
const serviceName = computed(() => {
|
|
if (nodeType.value) {
|
|
return getTriggerNodeServiceName(nodeType.value);
|
|
}
|
|
|
|
return '';
|
|
});
|
|
|
|
const displayChatButton = computed(() => {
|
|
return Boolean(
|
|
node.value &&
|
|
node.value.type === CHAT_TRIGGER_NODE_TYPE &&
|
|
node.value.parameters.mode !== 'webhook',
|
|
);
|
|
});
|
|
|
|
const isWebhookNode = computed(() => {
|
|
return Boolean(node.value && node.value.type === WEBHOOK_NODE_TYPE);
|
|
});
|
|
|
|
const webhookHttpMethod = computed(() => {
|
|
if (!node.value || !nodeType.value?.webhooks?.length) {
|
|
return undefined;
|
|
}
|
|
|
|
const httpMethod = workflowHelpers.getWebhookExpressionValue(
|
|
nodeType.value.webhooks[0],
|
|
'httpMethod',
|
|
false,
|
|
);
|
|
|
|
if (Array.isArray(httpMethod)) {
|
|
return httpMethod.join(', ');
|
|
}
|
|
|
|
return httpMethod;
|
|
});
|
|
|
|
const webhookTestUrl = computed(() => {
|
|
if (!node.value || !nodeType.value?.webhooks?.length) {
|
|
return undefined;
|
|
}
|
|
|
|
return workflowHelpers.getWebhookUrl(nodeType.value.webhooks[0], node.value, 'test');
|
|
});
|
|
|
|
const isWebhookBasedNode = computed(() => {
|
|
return Boolean(nodeType.value?.webhooks?.length);
|
|
});
|
|
|
|
const isPollingNode = computed(() => {
|
|
return Boolean(nodeType.value?.polling);
|
|
});
|
|
|
|
const isListeningForEvents = computed(() => {
|
|
const waitingOnWebhook = workflowsStore.executionWaitingForWebhook;
|
|
const executedNode = workflowsStore.executedNode;
|
|
return (
|
|
!!node.value &&
|
|
!node.value.disabled &&
|
|
isWebhookBasedNode.value &&
|
|
waitingOnWebhook &&
|
|
(!executedNode || executedNode === props.nodeName)
|
|
);
|
|
});
|
|
|
|
const workflowRunning = computed(() => workflowsStore.isWorkflowRunning);
|
|
|
|
const isActivelyPolling = computed(() => {
|
|
const triggeredNode = workflowsStore.executedNode;
|
|
|
|
return workflowRunning.value && isPollingNode.value && props.nodeName === triggeredNode;
|
|
});
|
|
|
|
const isWorkflowActive = computed(() => {
|
|
return workflowsStore.isWorkflowActive;
|
|
});
|
|
|
|
const listeningTitle = computed(() => {
|
|
return nodeType.value?.name === FORM_TRIGGER_NODE_TYPE
|
|
? i18n.baseText('ndv.trigger.webhookNode.formTrigger.listening')
|
|
: i18n.baseText('ndv.trigger.webhookNode.listening');
|
|
});
|
|
|
|
const listeningHint = computed(() => {
|
|
switch (nodeType.value?.name) {
|
|
case CHAT_TRIGGER_NODE_TYPE:
|
|
return i18n.baseText('ndv.trigger.webhookBasedNode.chatTrigger.serviceHint');
|
|
case FORM_TRIGGER_NODE_TYPE:
|
|
return i18n.baseText('ndv.trigger.webhookBasedNode.formTrigger.serviceHint');
|
|
default:
|
|
return i18n.baseText('ndv.trigger.webhookBasedNode.serviceHint', {
|
|
interpolate: { service: serviceName.value },
|
|
});
|
|
}
|
|
});
|
|
|
|
const header = computed(() => {
|
|
if (isActivelyPolling.value) {
|
|
return i18n.baseText('ndv.trigger.pollingNode.fetchingEvent');
|
|
}
|
|
|
|
if (triggerPanel.value?.header) {
|
|
return triggerPanel.value.header;
|
|
}
|
|
|
|
if (isWebhookBasedNode.value) {
|
|
return i18n.baseText('ndv.trigger.webhookBasedNode.action', {
|
|
interpolate: { name: serviceName.value },
|
|
});
|
|
}
|
|
|
|
return '';
|
|
});
|
|
|
|
const subheader = computed(() => {
|
|
if (isActivelyPolling.value) {
|
|
return i18n.baseText('ndv.trigger.pollingNode.fetchingHint', {
|
|
interpolate: { name: serviceName.value },
|
|
});
|
|
}
|
|
|
|
return '';
|
|
});
|
|
|
|
const executionsHelp = computed(() => {
|
|
if (triggerPanel.value?.executionsHelp) {
|
|
if (typeof triggerPanel.value.executionsHelp === 'string') {
|
|
return triggerPanel.value.executionsHelp;
|
|
}
|
|
if (!isWorkflowActive.value && triggerPanel.value.executionsHelp.inactive) {
|
|
return triggerPanel.value.executionsHelp.inactive;
|
|
}
|
|
if (isWorkflowActive.value && triggerPanel.value.executionsHelp.active) {
|
|
return triggerPanel.value.executionsHelp.active;
|
|
}
|
|
}
|
|
|
|
if (isWebhookBasedNode.value) {
|
|
if (isWorkflowActive.value) {
|
|
return i18n.baseText('ndv.trigger.webhookBasedNode.executionsHelp.active', {
|
|
interpolate: { service: serviceName.value },
|
|
});
|
|
} else {
|
|
return i18n.baseText('ndv.trigger.webhookBasedNode.executionsHelp.inactive', {
|
|
interpolate: { service: serviceName.value },
|
|
});
|
|
}
|
|
}
|
|
|
|
if (isPollingNode.value) {
|
|
if (isWorkflowActive.value) {
|
|
return i18n.baseText('ndv.trigger.pollingNode.executionsHelp.active', {
|
|
interpolate: { service: serviceName.value },
|
|
});
|
|
} else {
|
|
return i18n.baseText('ndv.trigger.pollingNode.executionsHelp.inactive', {
|
|
interpolate: { service: serviceName.value },
|
|
});
|
|
}
|
|
}
|
|
|
|
return '';
|
|
});
|
|
|
|
const activationHint = computed(() => {
|
|
if (isActivelyPolling.value || !triggerPanel.value) {
|
|
return '';
|
|
}
|
|
|
|
if (triggerPanel.value.activationHint) {
|
|
if (typeof triggerPanel.value.activationHint === 'string') {
|
|
return triggerPanel.value.activationHint;
|
|
}
|
|
if (!isWorkflowActive.value && typeof triggerPanel.value.activationHint.inactive === 'string') {
|
|
return triggerPanel.value.activationHint.inactive;
|
|
}
|
|
if (isWorkflowActive.value && typeof triggerPanel.value.activationHint.active === 'string') {
|
|
return triggerPanel.value.activationHint.active;
|
|
}
|
|
}
|
|
|
|
if (isWebhookBasedNode.value) {
|
|
if (isWorkflowActive.value) {
|
|
return i18n.baseText('ndv.trigger.webhookBasedNode.activationHint.active', {
|
|
interpolate: { service: serviceName.value },
|
|
});
|
|
} else {
|
|
return i18n.baseText('ndv.trigger.webhookBasedNode.activationHint.inactive', {
|
|
interpolate: { service: serviceName.value },
|
|
});
|
|
}
|
|
}
|
|
|
|
if (isPollingNode.value) {
|
|
if (isWorkflowActive.value) {
|
|
return i18n.baseText('ndv.trigger.pollingNode.activationHint.active', {
|
|
interpolate: { service: serviceName.value },
|
|
});
|
|
} else {
|
|
return i18n.baseText('ndv.trigger.pollingNode.activationHint.inactive', {
|
|
interpolate: { service: serviceName.value },
|
|
});
|
|
}
|
|
}
|
|
|
|
return '';
|
|
});
|
|
|
|
const expandExecutionHelp = () => {
|
|
if (help.value) {
|
|
executionsHelpEventBus.emit('expand');
|
|
}
|
|
};
|
|
|
|
const openWebhookUrl = () => {
|
|
telemetry.track('User clicked ndv link', {
|
|
workflow_id: workflowsStore.workflowId,
|
|
push_ref: props.pushRef,
|
|
pane: 'input',
|
|
type: 'open-chat',
|
|
});
|
|
window.open(webhookTestUrl.value, '_blank', 'noreferrer');
|
|
};
|
|
|
|
const onLinkClick = (e: MouseEvent) => {
|
|
if (!e.target) {
|
|
return;
|
|
}
|
|
const target = e.target as HTMLElement;
|
|
if (target.localName !== 'a') return;
|
|
|
|
if (target.dataset?.key) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
|
|
if (target.dataset.key === 'activate') {
|
|
emit('activate');
|
|
} else if (target.dataset.key === 'executions') {
|
|
telemetry.track('User clicked ndv link', {
|
|
workflow_id: workflowsStore.workflowId,
|
|
push_ref: props.pushRef,
|
|
pane: 'input',
|
|
type: 'open-executions-log',
|
|
});
|
|
ndvStore.activeNodeName = null;
|
|
void router.push({
|
|
name: VIEWS.EXECUTIONS,
|
|
});
|
|
} else if (target.dataset.key === 'settings') {
|
|
uiStore.openModal(WORKFLOW_SETTINGS_MODAL_KEY);
|
|
}
|
|
}
|
|
};
|
|
|
|
const onTestLinkCopied = () => {
|
|
telemetry.track('User copied webhook URL', {
|
|
pane: 'inputs',
|
|
type: 'test url',
|
|
});
|
|
};
|
|
|
|
const onNodeExecute = () => {
|
|
emit('execute');
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div :class="$style.container">
|
|
<transition name="fade" mode="out-in">
|
|
<div v-if="hasIssues || hideContent" key="empty"></div>
|
|
<div v-else-if="isListeningForEvents" key="listening">
|
|
<n8n-pulse>
|
|
<NodeIcon :node-type="nodeType" :size="40"></NodeIcon>
|
|
</n8n-pulse>
|
|
<div v-if="isWebhookNode">
|
|
<n8n-text tag="div" size="large" color="text-dark" class="mb-2xs" bold>{{
|
|
i18n.baseText('ndv.trigger.webhookNode.listening')
|
|
}}</n8n-text>
|
|
<div :class="[$style.shake, 'mb-xs']">
|
|
<n8n-text>
|
|
{{
|
|
i18n.baseText('ndv.trigger.webhookNode.requestHint', {
|
|
interpolate: { type: webhookHttpMethod ?? '' },
|
|
})
|
|
}}
|
|
</n8n-text>
|
|
</div>
|
|
<CopyInput
|
|
:value="webhookTestUrl"
|
|
:toast-title="i18n.baseText('ndv.trigger.copiedTestUrl')"
|
|
class="mb-2xl"
|
|
size="medium"
|
|
:collapse="true"
|
|
:copy-button-text="i18n.baseText('generic.clickToCopy')"
|
|
@copy="onTestLinkCopied"
|
|
></CopyInput>
|
|
<NodeExecuteButton
|
|
data-test-id="trigger-execute-button"
|
|
:node-name="nodeName"
|
|
size="medium"
|
|
telemetry-source="inputs"
|
|
@execute="onNodeExecute"
|
|
/>
|
|
</div>
|
|
<div v-else>
|
|
<n8n-text tag="div" size="large" color="text-dark" class="mb-2xs" bold>{{
|
|
listeningTitle
|
|
}}</n8n-text>
|
|
<div :class="[$style.shake, 'mb-xs']">
|
|
<n8n-text tag="div">
|
|
{{ listeningHint }}
|
|
</n8n-text>
|
|
</div>
|
|
<div v-if="displayChatButton">
|
|
<n8n-button class="mb-xl" @click="openWebhookUrl()">
|
|
{{ i18n.baseText('ndv.trigger.chatTrigger.openChat') }}
|
|
</n8n-button>
|
|
</div>
|
|
|
|
<NodeExecuteButton
|
|
data-test-id="trigger-execute-button"
|
|
:node-name="nodeName"
|
|
size="medium"
|
|
telemetry-source="inputs"
|
|
@execute="onNodeExecute"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div v-else key="default">
|
|
<div v-if="isActivelyPolling" class="mb-xl">
|
|
<n8n-spinner type="ring" />
|
|
</div>
|
|
|
|
<div :class="$style.action">
|
|
<div :class="$style.header">
|
|
<n8n-heading v-if="header" tag="h1" bold>
|
|
{{ header }}
|
|
</n8n-heading>
|
|
<n8n-text v-if="subheader">
|
|
<span v-text="subheader" />
|
|
</n8n-text>
|
|
</div>
|
|
|
|
<NodeExecuteButton
|
|
data-test-id="trigger-execute-button"
|
|
:node-name="nodeName"
|
|
size="medium"
|
|
telemetry-source="inputs"
|
|
@execute="onNodeExecute"
|
|
/>
|
|
</div>
|
|
|
|
<n8n-text v-if="activationHint" size="small" @click="onLinkClick">
|
|
<span v-n8n-html="activationHint"></span>
|
|
</n8n-text>
|
|
<n8n-link
|
|
v-if="activationHint && executionsHelp"
|
|
size="small"
|
|
@click="expandExecutionHelp"
|
|
>{{ i18n.baseText('ndv.trigger.moreInfo') }}</n8n-link
|
|
>
|
|
<n8n-info-accordion
|
|
v-if="executionsHelp"
|
|
ref="help"
|
|
:class="$style.accordion"
|
|
:title="i18n.baseText('ndv.trigger.executionsHint.question')"
|
|
:description="executionsHelp"
|
|
:event-bus="executionsHelpEventBus"
|
|
@click:body="onLinkClick"
|
|
></n8n-info-accordion>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" module>
|
|
.container {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: var(--color-background-base);
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: var(--spacing-s) var(--spacing-s) var(--spacing-xl) var(--spacing-s);
|
|
text-align: center;
|
|
overflow: hidden;
|
|
|
|
> * {
|
|
width: 100%;
|
|
}
|
|
}
|
|
|
|
.header {
|
|
margin-bottom: var(--spacing-s);
|
|
|
|
> * {
|
|
margin-bottom: var(--spacing-2xs);
|
|
}
|
|
}
|
|
|
|
.action {
|
|
margin-bottom: var(--spacing-2xl);
|
|
}
|
|
|
|
.shake {
|
|
animation: shake 8s infinite;
|
|
}
|
|
|
|
@keyframes shake {
|
|
90% {
|
|
transform: translateX(0);
|
|
}
|
|
92.5% {
|
|
transform: translateX(6px);
|
|
}
|
|
95% {
|
|
transform: translateX(-6px);
|
|
}
|
|
97.5% {
|
|
transform: translateX(6px);
|
|
}
|
|
100% {
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
.accordion {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
}
|
|
</style>
|
|
|
|
<style lang="scss" scoped>
|
|
.fade-enter-active,
|
|
.fade-leave-active {
|
|
transition: opacity 200ms;
|
|
}
|
|
.fade-enter,
|
|
.fade-leave-to {
|
|
opacity: 0;
|
|
}
|
|
</style>
|