From 91e0697d9712ea757c477860414d257d081eea8a Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 23 Jan 2020 19:07:18 -0500 Subject: [PATCH 1/3] :sparkles: date format node --- packages/nodes-base/nodes/DateTime.node.ts | 227 +++++++++++++++++++++ packages/nodes-base/package.json | 2 + 2 files changed, 229 insertions(+) create mode 100644 packages/nodes-base/nodes/DateTime.node.ts diff --git a/packages/nodes-base/nodes/DateTime.node.ts b/packages/nodes-base/nodes/DateTime.node.ts new file mode 100644 index 000000000..f86016238 --- /dev/null +++ b/packages/nodes-base/nodes/DateTime.node.ts @@ -0,0 +1,227 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { + INodeExecutionData, + INodeType, + INodeTypeDescription, + INodePropertyOptions, + ILoadOptionsFunctions, + IDataObject, +} from 'n8n-workflow'; + +import * as moment from 'moment-timezone'; + +export class DateTime implements INodeType { + description: INodeTypeDescription = { + displayName: 'Date & Time', + name: 'dateTime', + icon: 'fa:calendar', + group: ['transform'], + version: 1, + description: 'Allows you to manipulate date and time values', + defaults: { + name: 'Date & Time', + color: '#408000', + }, + inputs: ['main'], + outputs: ['main'], + properties: [ + { + displayName: 'Action', + name: 'action', + type: 'options', + options: [ + { + name: 'Format a Date', + description: 'Apply to a date a diferent format', + value: 'format' + }, + ], + default: 'format', + }, + { + displayName: 'Field Name', + name: 'fieldName', + displayOptions: { + show: { + action:[ + 'format' + ], + }, + }, + type: 'string', + default: '', + required: true, + }, + { + displayName: 'To Format', + name: 'toFormat', + type: 'options', + displayOptions: { + show: { + action:[ + 'format' + ], + }, + }, + options: [ + { + name: 'MM/DD/YYYY', + value: 'MM/DD/YYYY', + description: 'Example: 09/04/1986', + }, + { + name: 'YYYY/MM/DD', + value: 'YYYY/MM/DD', + description: 'Example: 1986/04/09', + }, + { + name: 'MMMM DD YYYY', + value: 'MMMM DD YYYY', + description: 'Example: April 09 1986', + }, + { + name: 'MM-DD-YYYY', + value: 'MM-DD-YYYY', + description: 'Example: 09-04-1986', + }, + { + name: 'YYYY-MM-DD', + value: 'YYYY-MM-DD', + description: 'Example: 1986-04-09', + }, + { + name: 'Unix Timestamp', + value: 'X', + description: 'Example: 513388800.879', + }, + { + name: 'Unix Ms Timestamp', + value: 'x', + description: 'Example: 513388800', + }, + ], + default: 'MM/DD/YYYY', + }, + { + displayName: 'To Timezone', + name: 'toTimezone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + displayOptions: { + show: { + action:[ + 'format' + ], + }, + }, + default: 'UTC', + }, + { + displayName: 'Options', + name: 'options', + displayOptions: { + show: { + action:[ + 'format' + ], + }, + }, + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'From Timezone', + name: 'fromTimezone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: 'UTC', + }, + { + displayName: 'From Format', + name: 'fromFormat', + type: 'string', + default: '', + description: 'In case the input format is not recognized you can provide the format ', + }, + { + displayName: 'Keep Old Date', + name: 'keepOldDate', + type: 'boolean', + default: false, + }, + ], + }, + ], + }; + + methods = { + loadOptions: { + // Get all the timezones to display them to user so that he can + // select them easily + async getTimezones(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + for (const timezone of moment.tz.names()) { + const timezoneName = timezone; + const timezoneId = timezone; + returnData.push({ + name: timezoneName, + value: timezoneId, + }); + } + return returnData; + }, + } + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + let responseData; + for (let i = 0; i < length; i++) { + const action = this.getNodeParameter('action', 0) as string; + if (action === 'format') { + const fieldName = this.getNodeParameter('fieldName', i) as string; + const toTimezone = this.getNodeParameter('toTimezone', i) as string; + const toFormat = this.getNodeParameter('toFormat', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + let newDate; + let clone = { ...items[i].json }; + if (clone[fieldName] === undefined) { + throw new Error(`The field ${fieldName} does not exist on the input data`); + } + if (!moment(clone[fieldName] as string | number).isValid()) { + throw new Error('The date input format is not recognized, please set the "From Format" field'); + } + if (Number.isInteger(clone[fieldName] as number)) { + newDate = moment.unix(clone[fieldName] as number).tz(toTimezone).format(toFormat); + } else { + newDate = moment(clone[fieldName] as string).tz(toTimezone).format(toFormat); + if (options.fromTimezone) { + newDate = moment.tz(clone[fieldName] as string, options.fromTimezone as string).tz(toTimezone).format(toFormat); + } + if (options.fromFormat) { + newDate = moment(clone[fieldName] as string, options.fromFormat as string).tz(toTimezone).format(toFormat); + } + } + if (!options.keepOldDate) { + clone[fieldName] = newDate; + } else { + clone['newDate'] = newDate; + } + responseData = clone; + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 9cd7f397c..1cff3fde1 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -106,6 +106,7 @@ "dist/nodes/Cron.node.js", "dist/nodes/Discord/Discord.node.js", "dist/nodes/Dropbox/Dropbox.node.js", + "dist/nodes/DateTime.node.js", "dist/nodes/EditImage.node.js", "dist/nodes/EmailReadImap.node.js", "dist/nodes/EmailSend.node.js", @@ -201,6 +202,7 @@ "@types/imap-simple": "^4.2.0", "@types/jest": "^24.0.18", "@types/lodash.set": "^4.3.6", + "@types/moment-timezone": "^0.5.12", "@types/mongodb": "^3.3.6", "@types/node": "^10.10.1", "@types/nodemailer": "^4.6.5", From bbd3a3bc8b0c1ba3fc596e65ed7df35e4bc51349 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 25 Jan 2020 17:14:54 -0500 Subject: [PATCH 2/3] :zap: added custom format --- packages/nodes-base/nodes/DateTime.node.ts | 36 ++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/DateTime.node.ts b/packages/nodes-base/nodes/DateTime.node.ts index f86016238..50f9e07ed 100644 --- a/packages/nodes-base/nodes/DateTime.node.ts +++ b/packages/nodes-base/nodes/DateTime.node.ts @@ -44,7 +44,7 @@ export class DateTime implements INodeType { displayOptions: { show: { action:[ - 'format' + 'format', ], }, }, @@ -52,6 +52,35 @@ export class DateTime implements INodeType { default: '', required: true, }, + { + displayName: 'Custom Format', + name: 'custom', + displayOptions: { + show: { + action:[ + 'format', + ], + }, + }, + type: 'boolean', + default: false, + }, + { + displayName: 'To Format', + name: 'toFormat', + displayOptions: { + show: { + action:[ + 'format', + ], + custom: [ + true, + ], + }, + }, + type: 'string', + default: '', + }, { displayName: 'To Format', name: 'toFormat', @@ -59,7 +88,10 @@ export class DateTime implements INodeType { displayOptions: { show: { action:[ - 'format' + 'format', + ], + custom:[ + false, ], }, }, From c258c96e384b34bf55b7315ac19696f513bda59e Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 29 Jan 2020 23:17:42 -0800 Subject: [PATCH 3/3] :zap: Improved DateTime-Node --- packages/nodes-base/nodes/DateTime.node.ts | 147 +++++++++++++-------- 1 file changed, 93 insertions(+), 54 deletions(-) diff --git a/packages/nodes-base/nodes/DateTime.node.ts b/packages/nodes-base/nodes/DateTime.node.ts index 50f9e07ed..ac1a85e27 100644 --- a/packages/nodes-base/nodes/DateTime.node.ts +++ b/packages/nodes-base/nodes/DateTime.node.ts @@ -1,14 +1,16 @@ +import * as moment from 'moment-timezone'; +import { get, set } from 'lodash'; + import { IExecuteFunctions } from 'n8n-core'; import { + IDataObject, + ILoadOptionsFunctions, INodeExecutionData, INodeType, INodeTypeDescription, INodePropertyOptions, - ILoadOptionsFunctions, - IDataObject, } from 'n8n-workflow'; -import * as moment from 'moment-timezone'; export class DateTime implements INodeType { description: INodeTypeDescription = { @@ -32,15 +34,15 @@ export class DateTime implements INodeType { options: [ { name: 'Format a Date', - description: 'Apply to a date a diferent format', + description: 'Convert a date to a different format', value: 'format' }, ], default: 'format', }, { - displayName: 'Field Name', - name: 'fieldName', + displayName: 'Key Name', + name: 'keyName', displayOptions: { show: { action:[ @@ -50,6 +52,7 @@ export class DateTime implements INodeType { }, type: 'string', default: '', + description: 'The name of the key of which the value should be converted.', required: true, }, { @@ -64,6 +67,7 @@ export class DateTime implements INodeType { }, type: 'boolean', default: false, + description: 'If a predefined format should be selected or custom format entered.', }, { displayName: 'To Format', @@ -80,6 +84,8 @@ export class DateTime implements INodeType { }, type: 'string', default: '', + placeholder: 'YYYY-MM-DD', + description: 'The format to convert the date to.', }, { displayName: 'To Format', @@ -133,22 +139,7 @@ export class DateTime implements INodeType { }, ], default: 'MM/DD/YYYY', - }, - { - displayName: 'To Timezone', - name: 'toTimezone', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getTimezones', - }, - displayOptions: { - show: { - action:[ - 'format' - ], - }, - }, - default: 'UTC', + description: 'The format to convert the date to.', }, { displayName: 'Options', @@ -172,6 +163,7 @@ export class DateTime implements INodeType { loadOptionsMethod: 'getTimezones', }, default: 'UTC', + description: 'The timezone to convert from.', }, { displayName: 'From Format', @@ -181,10 +173,21 @@ export class DateTime implements INodeType { description: 'In case the input format is not recognized you can provide the format ', }, { - displayName: 'Keep Old Date', - name: 'keepOldDate', - type: 'boolean', - default: false, + displayName: 'New Key Name', + name: 'newKeyName', + type: 'string', + default: 'newDate', + description: 'If set will the new date be added under the new key name and the existing one will not be touched.', + }, + { + displayName: 'To Timezone', + name: 'toTimezone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: 'UTC', + description: 'The timezone to convert to.', }, ], }, @@ -212,48 +215,84 @@ export class DateTime implements INodeType { async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); - const returnData: IDataObject[] = []; const length = items.length as unknown as number; - let responseData; + const returnData: INodeExecutionData[] = []; + + const workflowTimezone = this.getTimezone(); + let item: INodeExecutionData; + for (let i = 0; i < length; i++) { const action = this.getNodeParameter('action', 0) as string; + item = items[i]; + if (action === 'format') { - const fieldName = this.getNodeParameter('fieldName', i) as string; - const toTimezone = this.getNodeParameter('toTimezone', i) as string; + const keyName = this.getNodeParameter('keyName', i) as string; const toFormat = this.getNodeParameter('toFormat', i) as string; const options = this.getNodeParameter('options', i) as IDataObject; let newDate; - let clone = { ...items[i].json }; - if (clone[fieldName] === undefined) { - throw new Error(`The field ${fieldName} does not exist on the input data`); + const currentDate = get(item.json, keyName); + + if (currentDate === undefined) { + throw new Error(`The key ${keyName} does not exist on the input data`); } - if (!moment(clone[fieldName] as string | number).isValid()) { - throw new Error('The date input format is not recognized, please set the "From Format" field'); + if (!moment(currentDate as string | number).isValid()) { + throw new Error('The date input format could not be recognized. Please set the "From Format" field'); } - if (Number.isInteger(clone[fieldName] as number)) { - newDate = moment.unix(clone[fieldName] as number).tz(toTimezone).format(toFormat); + if (Number.isInteger(currentDate as number)) { + newDate = moment.unix(currentDate as number); } else { - newDate = moment(clone[fieldName] as string).tz(toTimezone).format(toFormat); - if (options.fromTimezone) { - newDate = moment.tz(clone[fieldName] as string, options.fromTimezone as string).tz(toTimezone).format(toFormat); - } - if (options.fromFormat) { - newDate = moment(clone[fieldName] as string, options.fromFormat as string).tz(toTimezone).format(toFormat); + if (options.fromTimezone || options.toTimezone) { + const fromTimezone = options.fromTimezone || workflowTimezone; + if (options.fromFormat) { + newDate = moment.tz(currentDate as string, options.fromFormat as string, fromTimezone as string); + } else { + newDate = moment.tz(currentDate as string, fromTimezone as string); + } + } else { + if (options.fromFormat) { + newDate = moment(currentDate as string, options.fromFormat as string); + } else { + newDate = moment(currentDate as string); + } } } - if (!options.keepOldDate) { - clone[fieldName] = newDate; - } else { - clone['newDate'] = newDate; + + if (options.toTimezone || options.fromTimezone) { + // If either a source or a target timezone got defined the + // timezone of the date has to be changed. If a target-timezone + // is set use it else fall back to workflow timezone. + newDate = newDate.tz(options.toTimezone as string || workflowTimezone); } - responseData = clone; - } - if (Array.isArray(responseData)) { - returnData.push.apply(returnData, responseData as IDataObject[]); - } else { - returnData.push(responseData as IDataObject); + + newDate = newDate.format(toFormat); + + let newItem: INodeExecutionData; + if (keyName.includes('.')) { + // Uses dot notation so copy all data + newItem = { + json: JSON.parse(JSON.stringify(items[i].json)), + }; + } else { + // Does not use dot notation so shallow copy is enough + newItem = { + json: { ...items[i].json }, + }; + } + + if (item.binary !== undefined) { + newItem.binary = item.binary; + } + + if (options.newKeyName) { + set(newItem, `json.${options.newKeyName}`, newDate); + } else { + set(newItem, `json.${keyName}`, newDate); + } + + returnData.push(newItem); } } - return [this.helpers.returnJsonArray(returnData)]; + + return this.prepareOutputData(returnData); } }