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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) : '';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user