mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat: Send and wait operation - freeText and customForm response types (#12106)
This commit is contained in:
@@ -450,7 +450,11 @@
|
|||||||
|
|
||||||
<div class='card' id='submitted-form' style='display: none;'>
|
<div class='card' id='submitted-form' style='display: none;'>
|
||||||
<div class='form-header'>
|
<div class='form-header'>
|
||||||
|
{{#if formSubmittedHeader}}
|
||||||
|
<h1 id='submitted-header'>{{formSubmittedHeader}}</h1>
|
||||||
|
{{else}}
|
||||||
<h1 id='submitted-header'>Form Submitted</h1>
|
<h1 id='submitted-header'>Form Submitted</h1>
|
||||||
|
{{/if}}
|
||||||
{{#if formSubmittedText}}
|
{{#if formSubmittedText}}
|
||||||
<p id='submitted-content'>
|
<p id='submitted-content'>
|
||||||
{{formSubmittedText}}
|
{{formSubmittedText}}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type {
|
|||||||
FormFieldsParameter,
|
FormFieldsParameter,
|
||||||
IExecuteFunctions,
|
IExecuteFunctions,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
|
INodeProperties,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
IWebhookFunctions,
|
IWebhookFunctions,
|
||||||
NodeTypeAndVersion,
|
NodeTypeAndVersion,
|
||||||
@@ -22,13 +23,7 @@ import { formDescription, formFields, formTitle } from '../Form/common.descripti
|
|||||||
import { prepareFormReturnItem, renderForm, resolveRawData } from '../Form/utils';
|
import { prepareFormReturnItem, renderForm, resolveRawData } from '../Form/utils';
|
||||||
import { type CompletionPageConfig } from './interfaces';
|
import { type CompletionPageConfig } from './interfaces';
|
||||||
|
|
||||||
const pageProperties = updateDisplayOptions(
|
export const formFieldsProperties: INodeProperties[] = [
|
||||||
{
|
|
||||||
show: {
|
|
||||||
operation: ['page'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
displayName: 'Define Form',
|
displayName: 'Define Form',
|
||||||
name: 'defineForm',
|
name: 'defineForm',
|
||||||
@@ -65,6 +60,16 @@ const pageProperties = updateDisplayOptions(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ ...formFields, displayOptions: { show: { defineForm: ['fields'] } } },
|
{ ...formFields, displayOptions: { show: { defineForm: ['fields'] } } },
|
||||||
|
];
|
||||||
|
|
||||||
|
const pageProperties = updateDisplayOptions(
|
||||||
|
{
|
||||||
|
show: {
|
||||||
|
operation: ['page'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
...formFieldsProperties,
|
||||||
{
|
{
|
||||||
displayName: 'Options',
|
displayName: 'Options',
|
||||||
name: 'options',
|
name: 'options',
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export type FormTriggerData = {
|
|||||||
validForm: boolean;
|
validForm: boolean;
|
||||||
formTitle: string;
|
formTitle: string;
|
||||||
formDescription?: string;
|
formDescription?: string;
|
||||||
|
formSubmittedHeader?: string;
|
||||||
formSubmittedText?: string;
|
formSubmittedText?: string;
|
||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
n8nWebsiteLink: string;
|
n8nWebsiteLink: string;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { getResolvables } from '../../utils/utilities';
|
|||||||
export function prepareFormData({
|
export function prepareFormData({
|
||||||
formTitle,
|
formTitle,
|
||||||
formDescription,
|
formDescription,
|
||||||
|
formSubmittedHeader,
|
||||||
formSubmittedText,
|
formSubmittedText,
|
||||||
redirectUrl,
|
redirectUrl,
|
||||||
formFields,
|
formFields,
|
||||||
@@ -49,6 +50,7 @@ export function prepareFormData({
|
|||||||
useResponseData?: boolean;
|
useResponseData?: boolean;
|
||||||
appendAttribution?: boolean;
|
appendAttribution?: boolean;
|
||||||
buttonLabel?: string;
|
buttonLabel?: string;
|
||||||
|
formSubmittedHeader?: string;
|
||||||
}) {
|
}) {
|
||||||
const validForm = formFields.length > 0;
|
const validForm = formFields.length > 0;
|
||||||
const utm_campaign = instanceId ? `&utm_campaign=${instanceId}` : '';
|
const utm_campaign = instanceId ? `&utm_campaign=${instanceId}` : '';
|
||||||
@@ -63,6 +65,7 @@ export function prepareFormData({
|
|||||||
validForm,
|
validForm,
|
||||||
formTitle,
|
formTitle,
|
||||||
formDescription,
|
formDescription,
|
||||||
|
formSubmittedHeader,
|
||||||
formSubmittedText,
|
formSubmittedText,
|
||||||
n8nWebsiteLink,
|
n8nWebsiteLink,
|
||||||
formFields: [],
|
formFields: [],
|
||||||
|
|||||||
@@ -52,5 +52,5 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"alias": ["email"]
|
"alias": ["email", "human", "form", "wait"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,15 @@ const versionDescription: INodeTypeDescription = {
|
|||||||
restartWebhook: true,
|
restartWebhook: true,
|
||||||
isFullPath: true,
|
isFullPath: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
httpMethod: 'POST',
|
||||||
|
responseMode: 'onReceived',
|
||||||
|
responseData: '',
|
||||||
|
path: '={{ $nodeId }}',
|
||||||
|
restartWebhook: true,
|
||||||
|
isFullPath: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ export const messageOperations: INodeProperties[] = [
|
|||||||
action: 'Send a message',
|
action: 'Send a message',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Send and Wait for Approval',
|
name: 'Send and Wait for Response',
|
||||||
value: SEND_AND_WAIT_OPERATION,
|
value: SEND_AND_WAIT_OPERATION,
|
||||||
action: 'Send a message and wait for approval',
|
action: 'Send message and wait for response',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
default: 'send',
|
default: 'send',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"nodeVersion": "1.0",
|
"nodeVersion": "1.0",
|
||||||
"codexVersion": "1.0",
|
"codexVersion": "1.0",
|
||||||
"categories": ["Communication"],
|
"categories": ["Communication"],
|
||||||
|
"alias": ["human", "form", "wait"],
|
||||||
"resources": {
|
"resources": {
|
||||||
"credentialDocumentation": [
|
"credentialDocumentation": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ export const messageOperations: INodeProperties[] = [
|
|||||||
action: 'Send a message',
|
action: 'Send a message',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Send and Wait for Approval',
|
name: 'Send and Wait for Response',
|
||||||
value: SEND_AND_WAIT_OPERATION,
|
value: SEND_AND_WAIT_OPERATION,
|
||||||
action: 'Send a message and wait for approval',
|
action: 'Send message and wait for response',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Update',
|
name: 'Update',
|
||||||
|
|||||||
@@ -89,6 +89,15 @@ export class SlackV2 implements INodeType {
|
|||||||
restartWebhook: true,
|
restartWebhook: true,
|
||||||
isFullPath: true,
|
isFullPath: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
httpMethod: 'POST',
|
||||||
|
responseMode: 'onReceived',
|
||||||
|
responseData: '',
|
||||||
|
path: '={{ $nodeId }}',
|
||||||
|
restartWebhook: true,
|
||||||
|
isFullPath: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export function createEmailBody(message: string, buttons: string, instanceId?: s
|
|||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
style="text-align: center; padding-top: 8px; font-family: Arial, sans-serif; font-size: 14px; color: #7e8186;">
|
style="text-align: center; padding-top: 8px; font-family: Arial, sans-serif; font-size: 14px; color: #7e8186;">
|
||||||
<p>${message}</p>
|
<p style="white-space: pre-line;">${message}</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
getSendAndWaitConfig,
|
getSendAndWaitConfig,
|
||||||
createEmail,
|
createEmail,
|
||||||
sendAndWaitWebhook,
|
sendAndWaitWebhook,
|
||||||
MESSAGE_PREFIX,
|
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
|
||||||
describe('Send and Wait utils tests', () => {
|
describe('Send and Wait utils tests', () => {
|
||||||
@@ -159,7 +158,7 @@ describe('Send and Wait utils tests', () => {
|
|||||||
|
|
||||||
expect(email).toEqual({
|
expect(email).toEqual({
|
||||||
to: 'test@example.com',
|
to: 'test@example.com',
|
||||||
subject: `${MESSAGE_PREFIX}Test subject`,
|
subject: 'Test subject',
|
||||||
body: '',
|
body: '',
|
||||||
htmlBody: expect.stringContaining('Test message'),
|
htmlBody: expect.stringContaining('Test message'),
|
||||||
});
|
});
|
||||||
@@ -208,5 +207,162 @@ describe('Send and Wait utils tests', () => {
|
|||||||
workflowData: [[{ json: { data: { approved: false } } }]],
|
workflowData: [[{ json: { data: { approved: false } } }]],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle freeText GET webhook', async () => {
|
||||||
|
const mockRender = jest.fn();
|
||||||
|
|
||||||
|
mockWebhookFunctions.getRequestObject.mockReturnValue({
|
||||||
|
method: 'GET',
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
mockWebhookFunctions.getResponseObject.mockReturnValue({
|
||||||
|
render: mockRender,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||||
|
const params: { [key: string]: any } = {
|
||||||
|
responseType: 'freeText',
|
||||||
|
message: 'Test message',
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
return params[parameterName];
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await sendAndWaitWebhook.call(mockWebhookFunctions);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
noWebhookResponse: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockRender).toHaveBeenCalledWith('form-trigger', {
|
||||||
|
testRun: false,
|
||||||
|
validForm: true,
|
||||||
|
formTitle: '',
|
||||||
|
formDescription: 'Test message',
|
||||||
|
formSubmittedHeader: 'Got it, thanks',
|
||||||
|
formSubmittedText: 'This page can be closed now',
|
||||||
|
n8nWebsiteLink: 'https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger',
|
||||||
|
formFields: [
|
||||||
|
{
|
||||||
|
id: 'field-0',
|
||||||
|
errorId: 'error-field-0',
|
||||||
|
label: 'Response',
|
||||||
|
inputRequired: 'form-required',
|
||||||
|
defaultValue: '',
|
||||||
|
isTextarea: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
appendAttribution: true,
|
||||||
|
buttonLabel: 'Submit',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle freeText POST webhook', async () => {
|
||||||
|
mockWebhookFunctions.getRequestObject.mockReturnValue({
|
||||||
|
method: 'POST',
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
mockWebhookFunctions.getBodyData.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
'field-0': 'test value',
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||||
|
const params: { [key: string]: any } = {
|
||||||
|
responseType: 'freeText',
|
||||||
|
};
|
||||||
|
return params[parameterName];
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await sendAndWaitWebhook.call(mockWebhookFunctions);
|
||||||
|
|
||||||
|
expect(result.workflowData).toEqual([[{ json: { data: { text: 'test value' } } }]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle customForm GET webhook', async () => {
|
||||||
|
const mockRender = jest.fn();
|
||||||
|
|
||||||
|
mockWebhookFunctions.getRequestObject.mockReturnValue({
|
||||||
|
method: 'GET',
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
mockWebhookFunctions.getResponseObject.mockReturnValue({
|
||||||
|
render: mockRender,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||||
|
const params: { [key: string]: any } = {
|
||||||
|
responseType: 'customForm',
|
||||||
|
message: 'Test message',
|
||||||
|
defineForm: 'fields',
|
||||||
|
'formFields.values': [{ label: 'Field 1', fieldType: 'text', requiredField: true }],
|
||||||
|
options: {
|
||||||
|
responseFormTitle: 'Test title',
|
||||||
|
responseFormDescription: 'Test description',
|
||||||
|
responseFormButtonLabel: 'Test button',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return params[parameterName];
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await sendAndWaitWebhook.call(mockWebhookFunctions);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
noWebhookResponse: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockRender).toHaveBeenCalledWith('form-trigger', {
|
||||||
|
testRun: false,
|
||||||
|
validForm: true,
|
||||||
|
formTitle: 'Test title',
|
||||||
|
formDescription: 'Test description',
|
||||||
|
formSubmittedHeader: 'Got it, thanks',
|
||||||
|
formSubmittedText: 'This page can be closed now',
|
||||||
|
n8nWebsiteLink: 'https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger',
|
||||||
|
formFields: [
|
||||||
|
{
|
||||||
|
id: 'field-0',
|
||||||
|
errorId: 'error-field-0',
|
||||||
|
inputRequired: 'form-required',
|
||||||
|
defaultValue: '',
|
||||||
|
isInput: true,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
appendAttribution: true,
|
||||||
|
buttonLabel: 'Test button',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle customForm POST webhook', async () => {
|
||||||
|
mockWebhookFunctions.getRequestObject.mockReturnValue({
|
||||||
|
method: 'POST',
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||||
|
const params: { [key: string]: any } = {
|
||||||
|
responseType: 'customForm',
|
||||||
|
defineForm: 'fields',
|
||||||
|
'formFields.values': [
|
||||||
|
{
|
||||||
|
fieldLabel: 'test 1',
|
||||||
|
fieldType: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
return params[parameterName];
|
||||||
|
});
|
||||||
|
|
||||||
|
mockWebhookFunctions.getBodyData.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
'field-0': 'test value',
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const result = await sendAndWaitWebhook.call(mockWebhookFunctions);
|
||||||
|
|
||||||
|
expect(result.workflowData).toEqual([[{ json: { data: { 'test 1': 'test value' } } }]]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import { NodeOperationError, SEND_AND_WAIT_OPERATION, updateDisplayOptions } from 'n8n-workflow';
|
import {
|
||||||
import type { INodeProperties, IExecuteFunctions, IWebhookFunctions } from 'n8n-workflow';
|
NodeOperationError,
|
||||||
|
SEND_AND_WAIT_OPERATION,
|
||||||
|
tryToParseJsonToFormFields,
|
||||||
|
updateDisplayOptions,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import type {
|
||||||
|
INodeProperties,
|
||||||
|
IExecuteFunctions,
|
||||||
|
IWebhookFunctions,
|
||||||
|
IDataObject,
|
||||||
|
FormFieldsParameter,
|
||||||
|
} from 'n8n-workflow';
|
||||||
import type { IEmail } from './interfaces';
|
import type { IEmail } from './interfaces';
|
||||||
import { escapeHtml } from '../utilities';
|
import { escapeHtml } from '../utilities';
|
||||||
import {
|
import {
|
||||||
@@ -8,6 +19,8 @@ import {
|
|||||||
BUTTON_STYLE_SECONDARY,
|
BUTTON_STYLE_SECONDARY,
|
||||||
createEmailBody,
|
createEmailBody,
|
||||||
} from './email-templates';
|
} from './email-templates';
|
||||||
|
import { prepareFormData, prepareFormReturnItem, resolveRawData } from '../../nodes/Form/utils';
|
||||||
|
import { formFieldsProperties } from '../../nodes/Form/Form.node';
|
||||||
|
|
||||||
type SendAndWaitConfig = {
|
type SendAndWaitConfig = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -16,7 +29,14 @@ type SendAndWaitConfig = {
|
|||||||
options: Array<{ label: string; value: string; style: string }>;
|
options: Array<{ label: string; value: string; style: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MESSAGE_PREFIX = 'ACTION REQUIRED: ';
|
type FormResponseTypeOptions = {
|
||||||
|
messageButtonLabel?: string;
|
||||||
|
responseFormTitle?: string;
|
||||||
|
responseFormDescription?: string;
|
||||||
|
responseFormButtonLabel?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const INPUT_FIELD_IDENTIFIER = 'field-0';
|
||||||
|
|
||||||
// Operation Properties ----------------------------------------------------------
|
// Operation Properties ----------------------------------------------------------
|
||||||
export function getSendAndWaitProperties(
|
export function getSendAndWaitProperties(
|
||||||
@@ -57,9 +77,32 @@ export function getSendAndWaitProperties(
|
|||||||
default: '',
|
default: '',
|
||||||
required: true,
|
required: true,
|
||||||
typeOptions: {
|
typeOptions: {
|
||||||
rows: 5,
|
rows: 4,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Response Type',
|
||||||
|
name: 'responseType',
|
||||||
|
type: 'options',
|
||||||
|
default: 'approval',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Approval',
|
||||||
|
value: 'approval',
|
||||||
|
description: 'User can approve/disapprove from within the message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Free Text',
|
||||||
|
value: 'freeText',
|
||||||
|
description: 'User can submit a response via a form',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Custom Form',
|
||||||
|
value: 'customForm',
|
||||||
|
description: 'User can submit a response via a custom form',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Approval Options',
|
displayName: 'Approval Options',
|
||||||
name: 'approvalOptions',
|
name: 'approvalOptions',
|
||||||
@@ -134,15 +177,61 @@ export function getSendAndWaitProperties(
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
responseType: ['approval'],
|
||||||
},
|
},
|
||||||
...additionalProperties,
|
},
|
||||||
|
},
|
||||||
|
...updateDisplayOptions(
|
||||||
{
|
{
|
||||||
displayName:
|
show: {
|
||||||
'Use the wait node for more complex approval flows. <a href="https://docs.n8n.io/nodes/n8n-nodes-base.wait" target="_blank">More info</a>',
|
responseType: ['customForm'],
|
||||||
name: 'useWaitNotice',
|
},
|
||||||
type: 'notice',
|
},
|
||||||
|
formFieldsProperties,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
displayName: 'Options',
|
||||||
|
name: 'options',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add option',
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Message Button Label',
|
||||||
|
name: 'messageButtonLabel',
|
||||||
|
type: 'string',
|
||||||
|
default: 'Respond',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Response Form Title',
|
||||||
|
name: 'responseFormTitle',
|
||||||
|
description: 'Title of the form that the user can access to provide their response',
|
||||||
|
type: 'string',
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Response Form Description',
|
||||||
|
name: 'responseFormDescription',
|
||||||
|
description: 'Description of the form that the user can access to provide their response',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Response Form Button Label',
|
||||||
|
name: 'responseFormButtonLabel',
|
||||||
|
type: 'string',
|
||||||
|
default: 'Submit',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
responseType: ['freeText', 'customForm'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...additionalProperties,
|
||||||
];
|
];
|
||||||
|
|
||||||
return updateDisplayOptions(
|
return updateDisplayOptions(
|
||||||
@@ -157,7 +246,136 @@ export function getSendAndWaitProperties(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Webhook Function --------------------------------------------------------------
|
// Webhook Function --------------------------------------------------------------
|
||||||
|
const getFormResponseCustomizations = (context: IWebhookFunctions) => {
|
||||||
|
const message = context.getNodeParameter('message', '') as string;
|
||||||
|
const options = context.getNodeParameter('options', {}) as FormResponseTypeOptions;
|
||||||
|
|
||||||
|
let formTitle = '';
|
||||||
|
if (options.responseFormTitle) {
|
||||||
|
formTitle = options.responseFormTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
let formDescription = message;
|
||||||
|
if (options.responseFormDescription) {
|
||||||
|
formDescription = options.responseFormDescription;
|
||||||
|
}
|
||||||
|
formDescription = formDescription.replace(/\\n/g, '\n').replace(/<br>/g, '\n');
|
||||||
|
|
||||||
|
let buttonLabel = 'Submit';
|
||||||
|
if (options.responseFormButtonLabel) {
|
||||||
|
buttonLabel = options.responseFormButtonLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
formTitle,
|
||||||
|
formDescription,
|
||||||
|
buttonLabel,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export async function sendAndWaitWebhook(this: IWebhookFunctions) {
|
export async function sendAndWaitWebhook(this: IWebhookFunctions) {
|
||||||
|
const method = this.getRequestObject().method;
|
||||||
|
const res = this.getResponseObject();
|
||||||
|
const responseType = this.getNodeParameter('responseType', 'approval') as
|
||||||
|
| 'approval'
|
||||||
|
| 'freeText'
|
||||||
|
| 'customForm';
|
||||||
|
|
||||||
|
if (responseType === 'freeText') {
|
||||||
|
if (method === 'GET') {
|
||||||
|
const { formTitle, formDescription, buttonLabel } = getFormResponseCustomizations(this);
|
||||||
|
|
||||||
|
const data = prepareFormData({
|
||||||
|
formTitle,
|
||||||
|
formDescription,
|
||||||
|
formSubmittedHeader: 'Got it, thanks',
|
||||||
|
formSubmittedText: 'This page can be closed now',
|
||||||
|
buttonLabel,
|
||||||
|
redirectUrl: undefined,
|
||||||
|
formFields: [
|
||||||
|
{
|
||||||
|
fieldLabel: 'Response',
|
||||||
|
fieldType: 'textarea',
|
||||||
|
requiredField: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
testRun: false,
|
||||||
|
query: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.render('form-trigger', data);
|
||||||
|
|
||||||
|
return {
|
||||||
|
noWebhookResponse: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (method === 'POST') {
|
||||||
|
const data = this.getBodyData().data as IDataObject;
|
||||||
|
|
||||||
|
return {
|
||||||
|
webhookResponse: ACTION_RECORDED_PAGE,
|
||||||
|
workflowData: [[{ json: { data: { text: data[INPUT_FIELD_IDENTIFIER] } } }]],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseType === 'customForm') {
|
||||||
|
const defineForm = this.getNodeParameter('defineForm', 'fields') as 'fields' | 'json';
|
||||||
|
let fields: FormFieldsParameter = [];
|
||||||
|
|
||||||
|
if (defineForm === 'json') {
|
||||||
|
try {
|
||||||
|
const jsonOutput = this.getNodeParameter('jsonOutput', '', {
|
||||||
|
rawExpressions: true,
|
||||||
|
}) as string;
|
||||||
|
|
||||||
|
fields = tryToParseJsonToFormFields(resolveRawData(this, jsonOutput));
|
||||||
|
} catch (error) {
|
||||||
|
throw new NodeOperationError(this.getNode(), error.message, {
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fields = this.getNodeParameter('formFields.values', []) as FormFieldsParameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === 'GET') {
|
||||||
|
const { formTitle, formDescription, buttonLabel } = getFormResponseCustomizations(this);
|
||||||
|
|
||||||
|
const data = prepareFormData({
|
||||||
|
formTitle,
|
||||||
|
formDescription,
|
||||||
|
formSubmittedHeader: 'Got it, thanks',
|
||||||
|
formSubmittedText: 'This page can be closed now',
|
||||||
|
buttonLabel,
|
||||||
|
redirectUrl: undefined,
|
||||||
|
formFields: fields,
|
||||||
|
testRun: false,
|
||||||
|
query: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.render('form-trigger', data);
|
||||||
|
|
||||||
|
return {
|
||||||
|
noWebhookResponse: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (method === 'POST') {
|
||||||
|
const returnItem = await prepareFormReturnItem(this, fields, 'production', true);
|
||||||
|
const json = returnItem.json as IDataObject;
|
||||||
|
|
||||||
|
delete json.submittedAt;
|
||||||
|
delete json.formMode;
|
||||||
|
|
||||||
|
returnItem.json = { data: json };
|
||||||
|
|
||||||
|
return {
|
||||||
|
webhookResponse: ACTION_RECORDED_PAGE,
|
||||||
|
workflowData: [[returnItem]],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const query = this.getRequestObject().query as { approved: 'false' | 'true' };
|
const query = this.getRequestObject().query as { approved: 'false' | 'true' };
|
||||||
const approved = query.approved === 'true';
|
const approved = query.approved === 'true';
|
||||||
return {
|
return {
|
||||||
@@ -168,7 +386,9 @@ export async function sendAndWaitWebhook(this: IWebhookFunctions) {
|
|||||||
|
|
||||||
// Send and Wait Config -----------------------------------------------------------
|
// Send and Wait Config -----------------------------------------------------------
|
||||||
export function getSendAndWaitConfig(context: IExecuteFunctions): SendAndWaitConfig {
|
export function getSendAndWaitConfig(context: IExecuteFunctions): SendAndWaitConfig {
|
||||||
const message = escapeHtml((context.getNodeParameter('message', 0, '') as string).trim());
|
const message = escapeHtml((context.getNodeParameter('message', 0, '') as string).trim())
|
||||||
|
.replace(/\\n/g, '\n')
|
||||||
|
.replace(/<br>/g, '\n');
|
||||||
const subject = escapeHtml(context.getNodeParameter('subject', 0, '') as string);
|
const subject = escapeHtml(context.getNodeParameter('subject', 0, '') as string);
|
||||||
const resumeUrl = context.evaluateExpression('{{ $execution?.resumeUrl }}', 0) as string;
|
const resumeUrl = context.evaluateExpression('{{ $execution?.resumeUrl }}', 0) as string;
|
||||||
const nodeId = context.evaluateExpression('{{ $nodeId }}', 0) as string;
|
const nodeId = context.evaluateExpression('{{ $nodeId }}', 0) as string;
|
||||||
@@ -187,7 +407,16 @@ export function getSendAndWaitConfig(context: IExecuteFunctions): SendAndWaitCon
|
|||||||
options: [],
|
options: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (approvalOptions.approvalType === 'double') {
|
const responseType = context.getNodeParameter('responseType', 0, 'approval') as string;
|
||||||
|
|
||||||
|
if (responseType === 'freeText' || responseType === 'customForm') {
|
||||||
|
const label = context.getNodeParameter('options.messageButtonLabel', 0, 'Respond') as string;
|
||||||
|
config.options.push({
|
||||||
|
label,
|
||||||
|
value: 'true',
|
||||||
|
style: 'primary',
|
||||||
|
});
|
||||||
|
} else if (approvalOptions.approvalType === 'double') {
|
||||||
const approveLabel = escapeHtml(approvalOptions.approveLabel || 'Approve');
|
const approveLabel = escapeHtml(approvalOptions.approveLabel || 'Approve');
|
||||||
const buttonApprovalStyle = approvalOptions.buttonApprovalStyle || 'primary';
|
const buttonApprovalStyle = approvalOptions.buttonApprovalStyle || 'primary';
|
||||||
const disapproveLabel = escapeHtml(approvalOptions.disapproveLabel || 'Disapprove');
|
const disapproveLabel = escapeHtml(approvalOptions.disapproveLabel || 'Disapprove');
|
||||||
@@ -245,7 +474,7 @@ export function createEmail(context: IExecuteFunctions) {
|
|||||||
|
|
||||||
const email: IEmail = {
|
const email: IEmail = {
|
||||||
to,
|
to,
|
||||||
subject: `${MESSAGE_PREFIX}${config.title}`,
|
subject: config.title,
|
||||||
body: '',
|
body: '',
|
||||||
htmlBody: createEmailBody(config.message, buttons.join('\n'), instanceId),
|
htmlBody: createEmailBody(config.message, buttons.join('\n'), instanceId),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user