2024-12-16 07:30:11 -08:00
import {
NodeOperationError ,
SEND_AND_WAIT_OPERATION ,
tryToParseJsonToFormFields ,
updateDisplayOptions ,
} from 'n8n-workflow' ;
import type {
INodeProperties ,
IExecuteFunctions ,
IWebhookFunctions ,
IDataObject ,
FormFieldsParameter ,
} from 'n8n-workflow' ;
2024-10-07 06:45:22 -07:00
import type { IEmail } from './interfaces' ;
import { escapeHtml } from '../utilities' ;
import {
ACTION_RECORDED_PAGE ,
BUTTON_STYLE_PRIMARY ,
BUTTON_STYLE_SECONDARY ,
createEmailBody ,
} from './email-templates' ;
2024-12-16 07:30:11 -08:00
import { prepareFormData , prepareFormReturnItem , resolveRawData } from '../../nodes/Form/utils' ;
import { formFieldsProperties } from '../../nodes/Form/Form.node' ;
2024-10-07 06:45:22 -07:00
type SendAndWaitConfig = {
title : string ;
message : string ;
url : string ;
options : Array < { label : string ; value : string ; style : string } > ;
} ;
2024-12-16 07:30:11 -08:00
type FormResponseTypeOptions = {
messageButtonLabel? : string ;
responseFormTitle? : string ;
responseFormDescription? : string ;
responseFormButtonLabel? : string ;
} ;
const INPUT_FIELD_IDENTIFIER = 'field-0' ;
2024-10-07 06:45:22 -07:00
// Operation Properties ----------------------------------------------------------
export function getSendAndWaitProperties (
targetProperties : INodeProperties [ ] ,
resource : string = 'message' ,
additionalProperties : INodeProperties [ ] = [ ] ,
) {
const buttonStyle : INodeProperties = {
displayName : 'Button Style' ,
name : 'buttonStyle' ,
type : 'options' ,
default : 'primary' ,
options : [
{
name : 'Primary' ,
value : 'primary' ,
} ,
{
name : 'Secondary' ,
value : 'secondary' ,
} ,
] ,
} ;
const sendAndWait : INodeProperties [ ] = [
. . . targetProperties ,
{
displayName : 'Subject' ,
name : 'subject' ,
type : 'string' ,
default : '' ,
required : true ,
placeholder : 'e.g. Approval required' ,
} ,
{
displayName : 'Message' ,
name : 'message' ,
type : 'string' ,
default : '' ,
required : true ,
typeOptions : {
2024-12-16 07:30:11 -08:00
rows : 4 ,
2024-10-07 06:45:22 -07:00
} ,
} ,
2024-12-16 07:30:11 -08:00
{
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' ,
} ,
] ,
} ,
2024-10-07 06:45:22 -07:00
{
displayName : 'Approval Options' ,
name : 'approvalOptions' ,
type : 'fixedCollection' ,
placeholder : 'Add option' ,
default : { } ,
options : [
{
displayName : 'Values' ,
name : 'values' ,
values : [
{
displayName : 'Type of Approval' ,
name : 'approvalType' ,
type : 'options' ,
placeholder : 'Add option' ,
default : 'single' ,
options : [
{
name : 'Approve Only' ,
value : 'single' ,
} ,
{
name : 'Approve and Disapprove' ,
value : 'double' ,
} ,
] ,
} ,
{
displayName : 'Approve Button Label' ,
name : 'approveLabel' ,
type : 'string' ,
default : 'Approve' ,
displayOptions : {
show : {
approvalType : [ 'single' , 'double' ] ,
} ,
} ,
} ,
{
. . . buttonStyle ,
displayName : 'Approve Button Style' ,
name : 'buttonApprovalStyle' ,
displayOptions : {
show : {
approvalType : [ 'single' , 'double' ] ,
} ,
} ,
} ,
{
displayName : 'Disapprove Button Label' ,
name : 'disapproveLabel' ,
type : 'string' ,
default : 'Decline' ,
displayOptions : {
show : {
approvalType : [ 'double' ] ,
} ,
} ,
} ,
{
. . . buttonStyle ,
displayName : 'Disapprove Button Style' ,
name : 'buttonDisapprovalStyle' ,
default : 'secondary' ,
displayOptions : {
show : {
approvalType : [ 'double' ] ,
} ,
} ,
} ,
] ,
} ,
] ,
2024-12-16 07:30:11 -08:00
displayOptions : {
show : {
responseType : [ 'approval' ] ,
} ,
} ,
2024-10-07 06:45:22 -07:00
} ,
2024-12-16 07:30:11 -08:00
. . . updateDisplayOptions (
{
show : {
responseType : [ 'customForm' ] ,
} ,
} ,
formFieldsProperties ,
) ,
2024-10-07 06:45:22 -07:00
{
2024-12-16 07:30:11 -08:00
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 : '' ,
} ,
{
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' ] ,
} ,
} ,
2024-10-07 06:45:22 -07:00
} ,
2024-12-16 07:30:11 -08:00
. . . additionalProperties ,
2024-10-07 06:45:22 -07:00
] ;
return updateDisplayOptions (
{
show : {
resource : [ resource ] ,
operation : [ SEND_AND_WAIT_OPERATION ] ,
} ,
} ,
sendAndWait ,
) ;
}
// Webhook Function --------------------------------------------------------------
2024-12-16 07:30:11 -08:00
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 ,
} ;
} ;
2024-10-07 06:45:22 -07:00
export async function sendAndWaitWebhook ( this : IWebhookFunctions ) {
2024-12-16 07:30:11 -08:00
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 ] ] ,
} ;
}
}
2024-10-07 06:45:22 -07:00
const query = this . getRequestObject ( ) . query as { approved : 'false' | 'true' } ;
const approved = query . approved === 'true' ;
return {
webhookResponse : ACTION_RECORDED_PAGE ,
workflowData : [ [ { json : { data : { approved } } } ] ] ,
} ;
}
// Send and Wait Config -----------------------------------------------------------
export function getSendAndWaitConfig ( context : IExecuteFunctions ) : SendAndWaitConfig {
2024-12-16 07:30:11 -08:00
const message = escapeHtml ( ( context . getNodeParameter ( 'message' , 0 , '' ) as string ) . trim ( ) )
. replace ( /\\n/g , '\n' )
. replace ( /<br>/g , '\n' ) ;
2024-10-07 06:45:22 -07:00
const subject = escapeHtml ( context . getNodeParameter ( 'subject' , 0 , '' ) as string ) ;
const resumeUrl = context . evaluateExpression ( '{{ $execution?.resumeUrl }}' , 0 ) as string ;
const nodeId = context . evaluateExpression ( '{{ $nodeId }}' , 0 ) as string ;
const approvalOptions = context . getNodeParameter ( 'approvalOptions.values' , 0 , { } ) as {
approvalType ? : 'single' | 'double' ;
approveLabel? : string ;
buttonApprovalStyle? : string ;
disapproveLabel? : string ;
buttonDisapprovalStyle? : string ;
} ;
const config : SendAndWaitConfig = {
title : subject ,
message ,
url : ` ${ resumeUrl } / ${ nodeId } ` ,
options : [ ] ,
} ;
2024-12-16 07:30:11 -08:00
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' ) {
2024-10-07 06:45:22 -07:00
const approveLabel = escapeHtml ( approvalOptions . approveLabel || 'Approve' ) ;
const buttonApprovalStyle = approvalOptions . buttonApprovalStyle || 'primary' ;
const disapproveLabel = escapeHtml ( approvalOptions . disapproveLabel || 'Disapprove' ) ;
const buttonDisapprovalStyle = approvalOptions . buttonDisapprovalStyle || 'secondary' ;
config . options . push ( {
label : disapproveLabel ,
value : 'false' ,
style : buttonDisapprovalStyle ,
} ) ;
config . options . push ( {
label : approveLabel ,
value : 'true' ,
style : buttonApprovalStyle ,
} ) ;
} else {
const label = escapeHtml ( approvalOptions . approveLabel || 'Approve' ) ;
const style = approvalOptions . buttonApprovalStyle || 'primary' ;
config . options . push ( {
label ,
value : 'true' ,
style ,
} ) ;
}
return config ;
}
function createButton ( url : string , label : string , approved : string , style : string ) {
let buttonStyle = BUTTON_STYLE_PRIMARY ;
if ( style === 'secondary' ) {
buttonStyle = BUTTON_STYLE_SECONDARY ;
}
return ` <a href=" ${ url } ?approved= ${ approved } " target="_blank" style=" ${ buttonStyle } "> ${ label } </a> ` ;
}
export function createEmail ( context : IExecuteFunctions ) {
const to = ( context . getNodeParameter ( 'sendTo' , 0 , '' ) as string ) . trim ( ) ;
const config = getSendAndWaitConfig ( context ) ;
if ( to . indexOf ( '@' ) === - 1 || ( to . match ( /@/g ) || [ ] ) . length > 1 ) {
const description = ` The email address ' ${ to } ' in the 'To' field isn't valid or contains multiple addresses. Please provide only a single email address. ` ;
throw new NodeOperationError ( context . getNode ( ) , 'Invalid email address' , {
description ,
itemIndex : 0 ,
} ) ;
}
const buttons : string [ ] = [ ] ;
for ( const option of config . options ) {
buttons . push ( createButton ( config . url , option . label , option . value , option . style ) ) ;
}
const instanceId = context . getInstanceId ( ) ;
const email : IEmail = {
to ,
2024-12-16 07:30:11 -08:00
subject : config.title ,
2024-10-07 06:45:22 -07:00
body : '' ,
htmlBody : createEmailBody ( config . message , buttons . join ( '\n' ) , instanceId ) ,
} ;
return email ;
}