-
Notifications
You must be signed in to change notification settings - Fork 8.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(Date & Time Node): Overhaul of the node (#5904)
* Setup versionized node * Fix node naming * Set all possible actions * Add Current Date operation * Add timezone to current date * feat add to date operator * Change output field name to camel case * Fix info box for luxons tip * Feat subtract to date operation * Feat format date operation * Fix to node field for format date * Feat rounding operation * Feat get in between date operation * Feat add extract date operation * Add generic function for parsing date * Remove moment methods from operations * Change moment to luxon for the rest of the operations * Fix Format date operation * Fix format value * Add timezone option for current date * Add tests, improve workflow settings for testing, toString the results * Change icon for V2 * Revert "Change icon for V2" This reverts commit 46b59be. * Change workflow test name * Fix ui bug for custom format * Fix default value for format operation * Fix info box for rounding operation * Change default for units for between time operation * Inprove fields and resort time units * Fix extract week number * Resolve issue with formating and timezones * Fix field name and unit order * ⚡ restored removed test case, sync v1 with curent master * ⚡ parseDate update to support timestamps, tests * Keep same field for substract and add time * Update unit test * Improve visibility, add iso to string option * Update option naming --------- Co-authored-by: Michael Kret <[email protected]>
- Loading branch information
1 parent
40bc74b
commit 7d1d1f7
Showing
16 changed files
with
2,211 additions
and
583 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
590 changes: 590 additions & 0 deletions
590
packages/nodes-base/nodes/DateTime/V1/DateTimeV1.node.ts
Large diffs are not rendered by default.
Oops, something went wrong.
105 changes: 105 additions & 0 deletions
105
packages/nodes-base/nodes/DateTime/V2/AddToDateDescription.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import type { INodeProperties } from 'n8n-workflow'; | ||
|
||
export const AddToDateDescription: INodeProperties[] = [ | ||
{ | ||
displayName: | ||
"You can also do this using an expression, e.g. <code>{{your_date.plus(5, 'minutes')}}</code>. <a target='_blank' href='https://docs.n8n.io/code-examples/expressions/luxon/'>More info</a>", | ||
name: 'notice', | ||
type: 'notice', | ||
default: '', | ||
displayOptions: { | ||
show: { | ||
operation: ['addToDate'], | ||
}, | ||
}, | ||
}, | ||
{ | ||
displayName: 'Date to Add To', | ||
name: 'magnitude', | ||
type: 'string', | ||
description: 'The date that you want to change', | ||
default: '', | ||
displayOptions: { | ||
show: { | ||
operation: ['addToDate'], | ||
}, | ||
}, | ||
required: true, | ||
}, | ||
{ | ||
displayName: 'Time Unit to Add', | ||
name: 'timeUnit', | ||
description: 'Time unit for Duration parameter below', | ||
displayOptions: { | ||
show: { | ||
operation: ['addToDate'], | ||
}, | ||
}, | ||
type: 'options', | ||
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items | ||
options: [ | ||
{ | ||
name: 'Years', | ||
value: 'years', | ||
}, | ||
{ | ||
name: 'Quarters', | ||
value: 'quarters', | ||
}, | ||
{ | ||
name: 'Months', | ||
value: 'months', | ||
}, | ||
{ | ||
name: 'Weeks', | ||
value: 'weeks', | ||
}, | ||
{ | ||
name: 'Days', | ||
value: 'days', | ||
}, | ||
{ | ||
name: 'Hours', | ||
value: 'hours', | ||
}, | ||
{ | ||
name: 'Minutes', | ||
value: 'minutes', | ||
}, | ||
{ | ||
name: 'Seconds', | ||
value: 'seconds', | ||
}, | ||
{ | ||
name: 'Milliseconds', | ||
value: 'milliseconds', | ||
}, | ||
], | ||
default: 'days', | ||
required: true, | ||
}, | ||
{ | ||
displayName: 'Duration', | ||
name: 'duration', | ||
type: 'number', | ||
description: 'The number of time units to add to the date', | ||
default: 0, | ||
displayOptions: { | ||
show: { | ||
operation: ['addToDate'], | ||
}, | ||
}, | ||
}, | ||
{ | ||
displayName: 'Output Field Name', | ||
name: 'outputFieldName', | ||
type: 'string', | ||
default: 'newDate', | ||
description: 'Name of the field to put the output in', | ||
displayOptions: { | ||
show: { | ||
operation: ['addToDate'], | ||
}, | ||
}, | ||
}, | ||
]; |
63 changes: 63 additions & 0 deletions
63
packages/nodes-base/nodes/DateTime/V2/CurrentDateDescription.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import type { INodeProperties } from 'n8n-workflow'; | ||
|
||
export const CurrentDateDescription: INodeProperties[] = [ | ||
{ | ||
displayName: | ||
'You can also refer to the current date in n8n expressions by using <code>{{$now}}</code> or <code>{{$today}}</code>. <a target="_blank" href="https://docs.n8n.io/code-examples/expressions/luxon/">More info</a>', | ||
name: 'notice', | ||
type: 'notice', | ||
default: '', | ||
displayOptions: { | ||
show: { | ||
operation: ['getCurrentDate'], | ||
}, | ||
}, | ||
}, | ||
{ | ||
displayName: 'Include Current Time', | ||
name: 'includeTime', | ||
type: 'boolean', | ||
default: true, | ||
description: 'Whether deactivated, the time will be set to midnight', | ||
displayOptions: { | ||
show: { | ||
operation: ['getCurrentDate'], | ||
}, | ||
}, | ||
}, | ||
{ | ||
displayName: 'Output Field Name', | ||
name: 'outputFieldName', | ||
type: 'string', | ||
default: 'currentDate', | ||
description: 'Name of the field to put the output in', | ||
displayOptions: { | ||
show: { | ||
operation: ['getCurrentDate'], | ||
}, | ||
}, | ||
}, | ||
{ | ||
displayName: 'Options', | ||
name: 'options', | ||
type: 'collection', | ||
placeholder: 'Add Option', | ||
displayOptions: { | ||
show: { | ||
operation: ['getCurrentDate'], | ||
}, | ||
}, | ||
default: {}, | ||
options: [ | ||
{ | ||
displayName: 'Timezone', | ||
name: 'timezone', | ||
type: 'string', | ||
placeholder: 'America/New_York', | ||
default: '', | ||
description: | ||
'The timezone to use. If not set, the timezone of the n8n instance will be used. Use ‘GMT’ for +00:00 timezone.', | ||
}, | ||
], | ||
}, | ||
]; |
210 changes: 210 additions & 0 deletions
210
packages/nodes-base/nodes/DateTime/V2/DateTimeV2.node.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
import type { | ||
IDataObject, | ||
IExecuteFunctions, | ||
INodeExecutionData, | ||
INodeType, | ||
INodeTypeBaseDescription, | ||
INodeTypeDescription, | ||
} from 'n8n-workflow'; | ||
import { NodeOperationError } from 'n8n-workflow'; | ||
|
||
import { CurrentDateDescription } from './CurrentDateDescription'; | ||
import { AddToDateDescription } from './AddToDateDescription'; | ||
import { SubtractFromDateDescription } from './SubtractFromDateDescription'; | ||
import { FormatDateDescription } from './FormatDateDescription'; | ||
import { RoundDateDescription } from './RoundDateDescription'; | ||
import { GetTimeBetweenDatesDescription } from './GetTimeBetweenDates'; | ||
import type { DateTimeUnit, DurationUnit } from 'luxon'; | ||
import { DateTime } from 'luxon'; | ||
import { ExtractDateDescription } from './ExtractDateDescription'; | ||
import { parseDate } from './GenericFunctions'; | ||
|
||
export class DateTimeV2 implements INodeType { | ||
description: INodeTypeDescription; | ||
|
||
constructor(baseDescription: INodeTypeBaseDescription) { | ||
this.description = { | ||
...baseDescription, | ||
version: 2, | ||
defaults: { | ||
name: 'Date & Time', | ||
color: '#408000', | ||
}, | ||
inputs: ['main'], | ||
outputs: ['main'], | ||
properties: [ | ||
{ | ||
displayName: 'Operation', | ||
name: 'operation', | ||
type: 'options', | ||
noDataExpression: true, | ||
options: [ | ||
{ | ||
name: 'Add to a Date', | ||
value: 'addToDate', | ||
}, | ||
{ | ||
name: 'Extract Part of a Date', | ||
value: 'extractDate', | ||
}, | ||
{ | ||
name: 'Format a Date', | ||
value: 'formatDate', | ||
}, | ||
{ | ||
name: 'Get Current Date', | ||
value: 'getCurrentDate', | ||
}, | ||
{ | ||
name: 'Get Time Between Dates', | ||
value: 'getTimeBetweenDates', | ||
}, | ||
{ | ||
name: 'Round a Date', | ||
value: 'roundDate', | ||
}, | ||
{ | ||
name: 'Subtract From a Date', | ||
value: 'subtractFromDate', | ||
}, | ||
], | ||
default: 'getCurrentDate', | ||
}, | ||
...CurrentDateDescription, | ||
...AddToDateDescription, | ||
...SubtractFromDateDescription, | ||
...FormatDateDescription, | ||
...RoundDateDescription, | ||
...GetTimeBetweenDatesDescription, | ||
...ExtractDateDescription, | ||
], | ||
}; | ||
} | ||
|
||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { | ||
const items = this.getInputData(); | ||
const returnData: INodeExecutionData[] = []; | ||
const responseData = []; | ||
const operation = this.getNodeParameter('operation', 0); | ||
const workflowTimezone = this.getTimezone(); | ||
|
||
for (let i = 0; i < items.length; i++) { | ||
if (operation === 'getCurrentDate') { | ||
const includeTime = this.getNodeParameter('includeTime', i) as boolean; | ||
const outputFieldName = this.getNodeParameter('outputFieldName', i) as string; | ||
const { timezone } = this.getNodeParameter('options', i) as { | ||
timezone: string; | ||
}; | ||
|
||
const newLocal = timezone ? timezone : workflowTimezone; | ||
if (DateTime.now().setZone(newLocal).invalidReason === 'unsupported zone') { | ||
throw new NodeOperationError( | ||
this.getNode(), | ||
`The timezone ${newLocal} is not valid. Please check the timezone.`, | ||
); | ||
} | ||
responseData.push( | ||
includeTime | ||
? { [outputFieldName]: DateTime.now().setZone(newLocal).toString() } | ||
: { | ||
[outputFieldName]: DateTime.now().setZone(newLocal).startOf('day').toString(), | ||
}, | ||
); | ||
} else if (operation === 'addToDate') { | ||
const addToDate = this.getNodeParameter('magnitude', i) as string; | ||
const timeUnit = this.getNodeParameter('timeUnit', i) as string; | ||
const duration = this.getNodeParameter('duration', i) as number; | ||
const outputFieldName = this.getNodeParameter('outputFieldName', i) as string; | ||
|
||
const dateToAdd = parseDate.call(this, addToDate, workflowTimezone); | ||
const returnedDate = dateToAdd.plus({ [timeUnit]: duration }); | ||
responseData.push({ [outputFieldName]: returnedDate.toString() }); | ||
} else if (operation === 'subtractFromDate') { | ||
const subtractFromDate = this.getNodeParameter('magnitude', i) as string; | ||
const timeUnit = this.getNodeParameter('timeUnit', i) as string; | ||
const duration = this.getNodeParameter('duration', i) as number; | ||
const outputFieldName = this.getNodeParameter('outputFieldName', i) as string; | ||
|
||
const dateToAdd = parseDate.call(this, subtractFromDate, workflowTimezone); | ||
const returnedDate = dateToAdd.minus({ [timeUnit]: duration }); | ||
responseData.push({ [outputFieldName]: returnedDate.toString() }); | ||
} else if (operation === 'formatDate') { | ||
const date = this.getNodeParameter('date', i) as string; | ||
const format = this.getNodeParameter('format', i) as string; | ||
const outputFieldName = this.getNodeParameter('outputFieldName', i) as string; | ||
const { timezone } = this.getNodeParameter('options', i) as { timezone: boolean }; | ||
|
||
const dateLuxon = timezone | ||
? parseDate.call(this, date, workflowTimezone) | ||
: parseDate.call(this, date); | ||
if (format === 'custom') { | ||
const customFormat = this.getNodeParameter('customFormat', i) as string; | ||
responseData.push({ | ||
[outputFieldName]: dateLuxon.toFormat(customFormat), | ||
}); | ||
} else { | ||
responseData.push({ | ||
[outputFieldName]: dateLuxon.toFormat(format), | ||
}); | ||
} | ||
} else if (operation === 'roundDate') { | ||
const date = this.getNodeParameter('date', i) as string; | ||
const mode = this.getNodeParameter('mode', i) as string; | ||
const outputFieldName = this.getNodeParameter('outputFieldName', i) as string; | ||
|
||
const dateLuxon = parseDate.call(this, date, workflowTimezone); | ||
|
||
if (mode === 'roundDown') { | ||
const toNearest = this.getNodeParameter('toNearest', i) as string; | ||
responseData.push({ | ||
[outputFieldName]: dateLuxon.startOf(toNearest as DateTimeUnit).toString(), | ||
}); | ||
} else if (mode === 'roundUp') { | ||
const to = this.getNodeParameter('to', i) as string; | ||
responseData.push({ | ||
[outputFieldName]: dateLuxon | ||
.plus({ [to]: 1 }) | ||
.startOf(to as DateTimeUnit) | ||
.toString(), | ||
}); | ||
} | ||
} else if (operation === 'getTimeBetweenDates') { | ||
const startDate = this.getNodeParameter('startDate', i) as string; | ||
const endDate = this.getNodeParameter('endDate', i) as string; | ||
const unit = this.getNodeParameter('units', i) as DurationUnit[]; | ||
const outputFieldName = this.getNodeParameter('outputFieldName', i) as string; | ||
const { isoString } = this.getNodeParameter('options', i) as { | ||
isoString: boolean; | ||
}; | ||
|
||
const luxonStartDate = parseDate.call(this, startDate, workflowTimezone); | ||
const luxonEndDate = parseDate.call(this, endDate, workflowTimezone); | ||
const duration = luxonEndDate.diff(luxonStartDate, unit); | ||
isoString | ||
? responseData.push({ | ||
[outputFieldName]: duration.toString(), | ||
}) | ||
: responseData.push({ | ||
[outputFieldName]: duration.toObject(), | ||
}); | ||
} else if (operation === 'extractDate') { | ||
const date = this.getNodeParameter('date', i) as string | DateTime; | ||
const outputFieldName = this.getNodeParameter('outputFieldName', i) as string; | ||
const part = this.getNodeParameter('part', i) as keyof DateTime | 'week'; | ||
|
||
const parsedDate = parseDate.call(this, date, workflowTimezone); | ||
const selectedPart = part === 'week' ? parsedDate.weekNumber : parsedDate.get(part); | ||
responseData.push({ [outputFieldName]: selectedPart }); | ||
} | ||
|
||
const executionData = this.helpers.constructExecutionMetaData( | ||
this.helpers.returnJsonArray(responseData as IDataObject[]), | ||
{ | ||
itemData: { item: i }, | ||
}, | ||
); | ||
returnData.push(...executionData); | ||
} | ||
return this.prepareOutputData(returnData); | ||
} | ||
} |
Oops, something went wrong.