diff --git a/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts b/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts index ded6db9e08..8432544cbe 100644 --- a/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts +++ b/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts @@ -6,12 +6,7 @@ import type { INodeTypeBaseDescription, INodeTypeDescription, } from 'n8n-workflow'; -import { - NodeConnectionType, - NodeOperationError, - SEND_AND_WAIT_OPERATION, - WAIT_INDEFINITELY, -} from 'n8n-workflow'; +import { NodeConnectionType, NodeOperationError, SEND_AND_WAIT_OPERATION } from 'n8n-workflow'; import { draftFields, draftOperations } from './DraftDescription'; import { labelFields, labelOperations } from './LabelDescription'; @@ -20,6 +15,7 @@ import { messageFields, messageOperations } from './MessageDescription'; import { threadFields, threadOperations } from './ThreadDescription'; import type { IEmail } from '../../../../utils/sendAndWait/interfaces'; import { + configureWaitTillDate, createEmail, getSendAndWaitProperties, sendAndWaitWebhook, @@ -204,7 +200,9 @@ export class GmailV2 implements INodeType { raw: await encodeEmail(email), }); - await this.putExecutionToWait(WAIT_INDEFINITELY); + const waitTill = configureWaitTillDate(this); + + await this.putExecutionToWait(waitTill); return [this.getInputData()]; } diff --git a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts index 524a36d7b5..7b1484487f 100644 --- a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts +++ b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts @@ -18,7 +18,6 @@ import { NodeConnectionType, NodeOperationError, SEND_AND_WAIT_OPERATION, - WAIT_INDEFINITELY, } from 'n8n-workflow'; import type { Readable } from 'stream'; @@ -42,7 +41,11 @@ import { reactionFields, reactionOperations } from './ReactionDescription'; import { starFields, starOperations } from './StarDescription'; import { userFields, userOperations } from './UserDescription'; import { userGroupFields, userGroupOperations } from './UserGroupDescription'; -import { getSendAndWaitProperties, sendAndWaitWebhook } from '../../../utils/sendAndWait/utils'; +import { + configureWaitTillDate, + getSendAndWaitProperties, + sendAndWaitWebhook, +} from '../../../utils/sendAndWait/utils'; export class SlackV2 implements INodeType { description: INodeTypeDescription; @@ -386,7 +389,9 @@ export class SlackV2 implements INodeType { createSendAndWaitMessageBody(this), ); - await this.putExecutionToWait(WAIT_INDEFINITELY); + const waitTill = configureWaitTillDate(this); + + await this.putExecutionToWait(waitTill); return [this.getInputData()]; } diff --git a/packages/nodes-base/utils/sendAndWait/test/util.test.ts b/packages/nodes-base/utils/sendAndWait/test/util.test.ts index 39a6f16859..c04c0fb4cb 100644 --- a/packages/nodes-base/utils/sendAndWait/test/util.test.ts +++ b/packages/nodes-base/utils/sendAndWait/test/util.test.ts @@ -1,12 +1,13 @@ import { type MockProxy, mock } from 'jest-mock-extended'; import type { IExecuteFunctions, INodeProperties, IWebhookFunctions } from 'n8n-workflow'; -import { NodeOperationError } from 'n8n-workflow'; +import { NodeOperationError, WAIT_INDEFINITELY } from 'n8n-workflow'; import { getSendAndWaitProperties, getSendAndWaitConfig, createEmail, sendAndWaitWebhook, + configureWaitTillDate, } from '../utils'; describe('Send and Wait utils tests', () => { @@ -369,3 +370,100 @@ describe('Send and Wait utils tests', () => { }); }); }); + +describe('configureWaitTillDate', () => { + let mockExecuteFunctions: MockProxy; + + beforeEach(() => { + mockExecuteFunctions = mock(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return WAIT_INDEFINITELY if limitWaitTime is empty', () => { + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); + const result = configureWaitTillDate(mockExecuteFunctions); + expect(result).toBe(WAIT_INDEFINITELY); + }); + + it('should calculate future date correctly for afterTimeInterval with minutes', () => { + const resumeAmount = 5; + const resumeUnit = 'minutes'; + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({ + limitType: 'afterTimeInterval', + resumeAmount, + resumeUnit, + }); + + const result = configureWaitTillDate(mockExecuteFunctions); + const expectedDate = new Date(new Date().getTime() + 5 * 60 * 1000); + expect(result.getTime()).toBeCloseTo(expectedDate.getTime(), -2); // Allowing 100ms difference + }); + + it('should calculate future date correctly for afterTimeInterval with hours', () => { + const resumeAmount = 2; + const resumeUnit = 'hours'; + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({ + limitType: 'afterTimeInterval', + resumeAmount, + resumeUnit, + }); + + const result = configureWaitTillDate(mockExecuteFunctions); + const expectedDate = new Date(new Date().getTime() + 2 * 60 * 60 * 1000); + expect(result.getTime()).toBeCloseTo(expectedDate.getTime(), -2); + }); + + it('should calculate future date correctly for afterTimeInterval with days', () => { + const resumeAmount = 1; + const resumeUnit = 'days'; + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({ + limitType: 'afterTimeInterval', + resumeAmount, + resumeUnit, + }); + + const result = configureWaitTillDate(mockExecuteFunctions); + const expectedDate = new Date(new Date().getTime() + 1 * 24 * 60 * 60 * 1000); + expect(result.getTime()).toBeCloseTo(expectedDate.getTime(), -2); + }); + + it('should return the specified maxDateAndTime for maxDateAndTime limitType', () => { + const maxDateAndTime = '2023-12-31T23:59:59Z'; + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({ + limitType: 'maxDateAndTime', + maxDateAndTime, + }); + + const result = configureWaitTillDate(mockExecuteFunctions); + expect(result).toEqual(new Date(maxDateAndTime)); + }); + + it('should throw NodeOperationError for invalid maxDateAndTime format', () => { + const invalidMaxDateAndTime = 'invalid-date'; + mockExecuteFunctions.getNodeParameter.mockReturnValue({ + limitType: 'maxDateAndTime', + maxDateAndTime: invalidMaxDateAndTime, + }); + + expect(() => configureWaitTillDate(mockExecuteFunctions)).toThrow(NodeOperationError); + expect(() => configureWaitTillDate(mockExecuteFunctions)).toThrow( + 'Could not configure Limit Wait Time', + ); + }); + + it('should throw NodeOperationError for invalid resumeAmount or resumeUnit', () => { + mockExecuteFunctions.getNodeParameter.mockReturnValue({ + limitType: 'afterTimeInterval', + resumeAmount: 'invalid', + resumeUnit: 'minutes', + }); + + expect(() => configureWaitTillDate(mockExecuteFunctions)).toThrow(NodeOperationError); + expect(() => configureWaitTillDate(mockExecuteFunctions)).toThrow( + 'Could not configure Limit Wait Time', + ); + }); +}); diff --git a/packages/nodes-base/utils/sendAndWait/utils.ts b/packages/nodes-base/utils/sendAndWait/utils.ts index 5328c2c8d3..4f79a5918e 100644 --- a/packages/nodes-base/utils/sendAndWait/utils.ts +++ b/packages/nodes-base/utils/sendAndWait/utils.ts @@ -1,8 +1,10 @@ import { + ApplicationError, NodeOperationError, SEND_AND_WAIT_OPERATION, tryToParseJsonToFormFields, updateDisplayOptions, + WAIT_INDEFINITELY, } from 'n8n-workflow'; import type { INodeProperties, @@ -39,6 +41,97 @@ type FormResponseTypeOptions = { const INPUT_FIELD_IDENTIFIER = 'field-0'; +const limitWaitTimeProperties: INodeProperties = { + displayName: 'Limit Wait Time', + name: 'limitWaitTime', + type: 'fixedCollection', + description: + 'Whether the workflow will automatically resume execution after the specified limit type', + default: { values: { limitType: 'afterTimeInterval', resumeAmount: 45, resumeUnit: 'minutes' } }, + options: [ + { + displayName: 'Values', + name: 'values', + values: [ + { + displayName: 'Limit Type', + name: 'limitType', + type: 'options', + default: 'afterTimeInterval', + description: + 'Sets the condition for the execution to resume. Can be a specified date or after some time.', + options: [ + { + name: 'After Time Interval', + description: 'Waits for a certain amount of time', + value: 'afterTimeInterval', + }, + { + name: 'At Specified Time', + description: 'Waits until the set date and time to continue', + value: 'atSpecifiedTime', + }, + ], + }, + { + displayName: 'Amount', + name: 'resumeAmount', + type: 'number', + displayOptions: { + show: { + limitType: ['afterTimeInterval'], + }, + }, + typeOptions: { + minValue: 0, + numberPrecision: 2, + }, + default: 1, + description: 'The time to wait', + }, + { + displayName: 'Unit', + name: 'resumeUnit', + type: 'options', + displayOptions: { + show: { + limitType: ['afterTimeInterval'], + }, + }, + options: [ + { + name: 'Minutes', + value: 'minutes', + }, + { + name: 'Hours', + value: 'hours', + }, + { + name: 'Days', + value: 'days', + }, + ], + default: 'hours', + description: 'Unit of the interval value', + }, + { + displayName: 'Max Date and Time', + name: 'maxDateAndTime', + type: 'dateTime', + displayOptions: { + show: { + limitType: ['atSpecifiedTime'], + }, + }, + default: '', + description: 'Continue execution after the specified date and time', + }, + ], + }, + ], +}; + // Operation Properties ---------------------------------------------------------- export function getSendAndWaitProperties( targetProperties: INodeProperties[], @@ -104,6 +197,15 @@ export function getSendAndWaitProperties( }, ], }, + ...updateDisplayOptions( + { + show: { + responseType: ['customForm'], + }, + }, + formFieldsProperties, + ), + { displayName: 'Approval Options', name: 'approvalOptions', @@ -184,14 +286,19 @@ export function getSendAndWaitProperties( }, }, }, - ...updateDisplayOptions( - { + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add option', + default: {}, + options: [limitWaitTimeProperties], + displayOptions: { show: { - responseType: ['customForm'], + responseType: ['approval'], }, }, - formFieldsProperties, - ), + }, { displayName: 'Options', name: 'options', @@ -225,6 +332,7 @@ export function getSendAndWaitProperties( type: 'string', default: 'Submit', }, + limitWaitTimeProperties, ], displayOptions: { show: { @@ -482,3 +590,46 @@ export function createEmail(context: IExecuteFunctions) { return email; } + +export function configureWaitTillDate(context: IExecuteFunctions) { + let waitTill = WAIT_INDEFINITELY; + const limitWaitTime = context.getNodeParameter('options.limitWaitTime.values', 0, {}) as { + limitType?: string; + resumeAmount?: number; + resumeUnit?: string; + maxDateAndTime?: string; + }; + + if (Object.keys(limitWaitTime).length) { + try { + if (limitWaitTime.limitType === 'afterTimeInterval') { + let waitAmount = limitWaitTime.resumeAmount as number; + + if (limitWaitTime.resumeUnit === 'minutes') { + waitAmount *= 60; + } + if (limitWaitTime.resumeUnit === 'hours') { + waitAmount *= 60 * 60; + } + if (limitWaitTime.resumeUnit === 'days') { + waitAmount *= 60 * 60 * 24; + } + + waitAmount *= 1000; + waitTill = new Date(new Date().getTime() + waitAmount); + } else { + waitTill = new Date(limitWaitTime.maxDateAndTime as string); + } + + if (isNaN(waitTill.getTime())) { + throw new ApplicationError('Invalid date format'); + } + } catch (error) { + throw new NodeOperationError(context.getNode(), 'Could not configure Limit Wait Time', { + description: error.message, + }); + } + } + + return waitTill; +}