feat(n8n Form Trigger Node): New node (#7130)

Github issue / Community forum post (link here to close automatically):

based on https://github.com/joffcom/n8n-nodes-form-trigger

---------

Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
This commit is contained in:
Michael Kret
2023-10-17 07:09:30 +03:00
committed by GitHub
parent 869b8f14ca
commit 3ddc176dfa
26 changed files with 1328 additions and 32 deletions

View File

@@ -24,7 +24,9 @@
:key="property.name + index"
class="parameter-item"
>
<div class="parameter-item-wrapper">
<div
:class="index ? 'border-top-dashed parameter-item-wrapper ' : 'parameter-item-wrapper'"
>
<div class="delete-option" v-if="!isReadOnly">
<font-awesome-icon
icon="trash"
@@ -375,8 +377,6 @@ export default defineComponent({
+ .parameter-item {
.parameter-item-wrapper {
border-top: 1px dashed #999;
.delete-option {
top: 14px;
}
@@ -384,6 +384,10 @@ export default defineComponent({
}
}
.border-top-dashed {
border-top: 1px dashed #999;
}
.no-items-exist {
margin: var(--spacing-xs) 0;
}

View File

@@ -175,6 +175,7 @@ import {
LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG,
MANUAL_TRIGGER_NODE_TYPE,
NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS,
NOT_DUPLICATABE_NODE_TYPES,
WAIT_TIME_UNLIMITED,
} from '@/constants';
import { externalHooks } from '@/mixins/externalHooks';
@@ -221,6 +222,7 @@ export default defineComponent({
},
isDuplicatable(): boolean {
if (!this.nodeType) return true;
if (NOT_DUPLICATABE_NODE_TYPES.includes(this.nodeType.name)) return false;
return (
this.nodeType.maxNodes === undefined || this.sameTypeNodes.length < this.nodeType.maxNodes
);

View File

@@ -76,7 +76,7 @@ describe('NodesListPanel', () => {
await fireEvent.click(container.querySelector('.backButton')!);
await nextTick();
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(6);
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(7);
});
it('should render regular nodes', async () => {

View File

@@ -3,6 +3,7 @@ import {
WEBHOOK_NODE_TYPE,
OTHER_TRIGGER_NODES_SUBCATEGORY,
EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
FORM_TRIGGER_NODE_TYPE,
MANUAL_TRIGGER_NODE_TYPE,
SCHEDULE_TRIGGER_NODE_TYPE,
REGULAR_NODE_CREATOR_VIEW,
@@ -264,6 +265,22 @@ export function TriggerView(nodes: SimplifiedNodeType[]) {
},
},
},
{
key: FORM_TRIGGER_NODE_TYPE,
type: 'node',
category: [CORE_NODES_CATEGORY],
properties: {
group: [],
name: FORM_TRIGGER_NODE_TYPE,
displayName: i18n.baseText('nodeCreator.triggerHelperPanel.formTriggerDisplayName'),
description: i18n.baseText('nodeCreator.triggerHelperPanel.formTriggerDescription'),
iconData: {
type: 'file',
icon: 'form',
fileBuffer: '/static/form-grey.svg',
},
},
},
{
key: MANUAL_TRIGGER_NODE_TYPE,
type: 'node',

View File

@@ -12,6 +12,7 @@
:label="buttonLabel"
:type="type"
:size="size"
:icon="isFormTriggerNode && 'flask'"
:transparentBackground="transparent"
@click="onClick"
/>
@@ -23,7 +24,12 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import { WEBHOOK_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE, MODAL_CONFIRM } from '@/constants';
import {
WEBHOOK_NODE_TYPE,
MANUAL_TRIGGER_NODE_TYPE,
MODAL_CONFIRM,
FORM_TRIGGER_NODE_TYPE,
} from '@/constants';
import type { INodeUi } from '@/Interface';
import type { INodeTypeDescription } from 'n8n-workflow';
import { workflowRun } from '@/mixins/workflowRun';
@@ -97,6 +103,9 @@ export default defineComponent({
isManualTriggerNode(): boolean {
return Boolean(this.nodeType && this.nodeType.name === MANUAL_TRIGGER_NODE_TYPE);
},
isFormTriggerNode(): boolean {
return Boolean(this.nodeType && this.nodeType.name === FORM_TRIGGER_NODE_TYPE);
},
isPollingTypeNode(): boolean {
return !!this.nodeType?.polling;
},
@@ -168,11 +177,20 @@ export default defineComponent({
return this.$locale.baseText('ndv.execute.listenForTestEvent');
}
if (this.isFormTriggerNode) {
return this.$locale.baseText('ndv.execute.testStep');
}
if (this.isPollingTypeNode || this.nodeType?.mockManualExecution) {
return this.$locale.baseText('ndv.execute.fetchEvent');
}
if (this.isTriggerNode && !this.isScheduleTrigger && !this.isManualTriggerNode) {
if (
this.isTriggerNode &&
!this.isScheduleTrigger &&
!this.isManualTriggerNode &&
!this.isFormTriggerNode
) {
return this.$locale.baseText('ndv.execute.listenForEvent');
}

View File

@@ -4,14 +4,10 @@
class="clickable headline"
:class="{ expanded: !isMinimized }"
@click="isMinimized = !isMinimized"
:title="
isMinimized
? $locale.baseText('nodeWebhooks.clickToDisplayWebhookUrls')
: $locale.baseText('nodeWebhooks.clickToHideWebhookUrls')
"
:title="isMinimized ? baseText.clickToDisplay : baseText.clickToHide"
>
<font-awesome-icon icon="angle-down" class="minimize-button minimize-icon" />
{{ $locale.baseText('nodeWebhooks.webhookUrls') }}
{{ baseText.toggleTitle }}
</div>
<el-collapse-transition>
<div class="node-webhooks" v-if="!isMinimized">
@@ -21,9 +17,9 @@
<n8n-radio-buttons
v-model="showUrlFor"
:options="[
{ label: this.$locale.baseText('nodeWebhooks.testUrl'), value: 'test' },
{ label: baseText.testUrl, value: 'test' },
{
label: this.$locale.baseText('nodeWebhooks.productionUrl'),
label: baseText.productionUrl,
value: 'production',
},
]"
@@ -33,13 +29,13 @@
</div>
<n8n-tooltip
v-for="(webhook, index) in webhooksNode"
v-for="(webhook, index) in webhooksNode.filter((webhook) => !webhook.ndvHideUrl)"
:key="index"
class="item"
:content="$locale.baseText('nodeWebhooks.clickToCopyWebhookUrls')"
:content="baseText.clickToCopy"
placement="left"
>
<div class="webhook-wrapper">
<div v-if="!webhook.ndvHideMethod" class="webhook-wrapper">
<div class="http-field">
<div class="http-method">
{{ getWebhookExpressionValue(webhook, 'httpMethod') }}<br />
@@ -51,6 +47,13 @@
</div>
</div>
</div>
<div v-else class="webhook-wrapper">
<div class="url-field-full-width">
<div class="webhook-url left-ellipsis clickable" @click="copyWebhookUrl(webhook)">
{{ getWebhookUrlDisplay(webhook) }}<br />
</div>
</div>
</div>
</n8n-tooltip>
</div>
</el-collapse-transition>
@@ -61,7 +64,7 @@
import { defineComponent } from 'vue';
import type { INodeTypeDescription, IWebhookDescription } from 'n8n-workflow';
import { WEBHOOK_NODE_TYPE } from '@/constants';
import { FORM_TRIGGER_NODE_TYPE, OPEN_URL_PANEL_TRIGGER_NODE_TYPES } from '@/constants';
import { copyPaste } from '@/mixins/copyPaste';
import { useToast } from '@/composables';
import { workflowHelpers } from '@/mixins/workflowHelpers';
@@ -80,7 +83,7 @@ export default defineComponent({
},
data() {
return {
isMinimized: this.nodeType && this.nodeType.name !== WEBHOOK_NODE_TYPE,
isMinimized: this.nodeType && !OPEN_URL_PANEL_TRIGGER_NODE_TYPES.includes(this.nodeType.name),
showUrlFor: 'test',
};
},
@@ -94,6 +97,36 @@ export default defineComponent({
(webhookData) => webhookData.restartWebhook !== true,
);
},
baseText() {
const nodeType = this.nodeType.name;
switch (nodeType) {
case FORM_TRIGGER_NODE_TYPE:
return {
toggleTitle: this.$locale.baseText('nodeWebhooks.webhookUrls.formTrigger'),
clickToDisplay: this.$locale.baseText(
'nodeWebhooks.clickToDisplayWebhookUrls.formTrigger',
),
clickToHide: this.$locale.baseText('nodeWebhooks.clickToHideWebhookUrls.formTrigger'),
clickToCopy: this.$locale.baseText('nodeWebhooks.clickToCopyWebhookUrls.formTrigger'),
testUrl: this.$locale.baseText('nodeWebhooks.testUrl'),
productionUrl: this.$locale.baseText('nodeWebhooks.productionUrl'),
copyTitle: this.$locale.baseText('nodeWebhooks.showMessage.title.formTrigger'),
copyMessage: this.$locale.baseText('nodeWebhooks.showMessage.message.formTrigger'),
};
default:
return {
toggleTitle: this.$locale.baseText('nodeWebhooks.webhookUrls'),
clickToDisplay: this.$locale.baseText('nodeWebhooks.clickToDisplayWebhookUrls'),
clickToHide: this.$locale.baseText('nodeWebhooks.clickToHideWebhookUrls'),
clickToCopy: this.$locale.baseText('nodeWebhooks.clickToCopyWebhookUrls'),
testUrl: this.$locale.baseText('nodeWebhooks.testUrl'),
productionUrl: this.$locale.baseText('nodeWebhooks.productionUrl'),
copyTitle: this.$locale.baseText('nodeWebhooks.showMessage.title'),
copyMessage: undefined,
};
}
},
},
methods: {
copyWebhookUrl(webhookData: IWebhookDescription): void {
@@ -101,7 +134,8 @@ export default defineComponent({
this.copyToClipboard(webhookUrl);
this.showMessage({
title: this.$locale.baseText('nodeWebhooks.showMessage.title'),
title: this.baseText.copyTitle,
message: this.baseText.copyMessage,
type: 'success',
});
this.$telemetry.track('User copied webhook URL', {
@@ -118,7 +152,7 @@ export default defineComponent({
},
watch: {
node() {
this.isMinimized = this.nodeType.name !== WEBHOOK_NODE_TYPE;
this.isMinimized = !OPEN_URL_PANEL_TRIGGER_NODE_TYPES.includes(this.nodeType.name);
},
},
});
@@ -175,6 +209,10 @@ export default defineComponent({
width: calc(100% - 60px);
margin-left: 55px;
}
.url-field-full-width {
display: inline-block;
width: 100%;
}
.url-selection {
margin-top: var(--spacing-xs);

View File

@@ -38,15 +38,11 @@
</div>
<div v-else>
<n8n-text tag="div" size="large" color="text-dark" class="mb-2xs" bold>{{
$locale.baseText('ndv.trigger.webhookBasedNode.listening')
listeningTitle
}}</n8n-text>
<div :class="[$style.shake, 'mb-xs']">
<n8n-text tag="div">
{{
$locale.baseText('ndv.trigger.webhookBasedNode.serviceHint', {
interpolate: { service: serviceName },
})
}}
{{ listeningHint }}
</n8n-text>
</div>
<NodeExecuteButton
@@ -108,7 +104,12 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import { VIEWS, WEBHOOK_NODE_TYPE, WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants';
import {
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';
@@ -150,7 +151,7 @@ export default defineComponent({
computed: {
...mapStores(useNodeTypesStore, useNDVStore, useUIStore, useWorkflowsStore),
node(): INodeUi | null {
return this.workflowsStore.getNodeByName(this.nodeName);
return this.workflowsStore.getNodeByName(this.nodeName as string);
},
nodeType(): INodeTypeDescription | null {
if (this.node) {
@@ -216,6 +217,18 @@ export default defineComponent({
isWorkflowActive(): boolean {
return this.workflowsStore.isWorkflowActive;
},
listeningTitle(): string {
return this.nodeType?.name === FORM_TRIGGER_NODE_TYPE
? this.$locale.baseText('ndv.trigger.webhookNode.formTrigger.listening')
: 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', {
interpolate: { service: this.serviceName },
});
},
header(): string {
const serviceName = this.nodeType ? getTriggerNodeServiceName(this.nodeType) : '';