From 21f9af887620e558cc2ee35e4110235474095459 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sun, 16 May 2021 14:35:11 -0400 Subject: [PATCH] :sparkles: Add iCalendar Node (#1725) * :sparkles: iCalendar Node * :zap: Improvements * :zap: Improvements * iCal node copy touch-up * :zap: Minor improvement Co-authored-by: sirdavidoff <1670123+sirdavidoff@users.noreply.github.com> Co-authored-by: Jan Oberhauser --- packages/editor-ui/src/main.ts | 2 + packages/nodes-base/nodes/DateTime.node.ts | 2 +- packages/nodes-base/nodes/ICalendar.node.ts | 358 ++++++++++++++++++++ packages/nodes-base/package.json | 2 + 4 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 packages/nodes-base/nodes/ICalendar.node.ts diff --git a/packages/editor-ui/src/main.ts b/packages/editor-ui/src/main.ts index af74cd0fcd..bc982d7aa4 100644 --- a/packages/editor-ui/src/main.ts +++ b/packages/editor-ui/src/main.ts @@ -93,6 +93,7 @@ import { faTrash, faUndo, faUsers, + faClock, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; @@ -174,6 +175,7 @@ library.add(faTimes); library.add(faTrash); library.add(faUndo); library.add(faUsers); +library.add(faClock); Vue.component('font-awesome-icon', FontAwesomeIcon); diff --git a/packages/nodes-base/nodes/DateTime.node.ts b/packages/nodes-base/nodes/DateTime.node.ts index 8279d654b4..d0aeb44731 100644 --- a/packages/nodes-base/nodes/DateTime.node.ts +++ b/packages/nodes-base/nodes/DateTime.node.ts @@ -23,7 +23,7 @@ export class DateTime implements INodeType { description: INodeTypeDescription = { displayName: 'Date & Time', name: 'dateTime', - icon: 'fa:calendar', + icon: 'fa:clock', group: ['transform'], version: 1, description: 'Allows you to manipulate date and time values', diff --git a/packages/nodes-base/nodes/ICalendar.node.ts b/packages/nodes-base/nodes/ICalendar.node.ts new file mode 100644 index 0000000000..97e66fd67b --- /dev/null +++ b/packages/nodes-base/nodes/ICalendar.node.ts @@ -0,0 +1,358 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + promisify, +} from 'util'; + +import * as moment from 'moment-timezone'; + +import * as ics from 'ics'; + +const createEvent = promisify(ics.createEvent); + +export class ICalendar implements INodeType { + description: INodeTypeDescription = { + displayName: 'iCalendar', + name: 'iCal', + icon: 'fa:calendar', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"]}}', + description: 'Create iCalendar file', + defaults: { + name: 'iCalendar', + color: '#408000', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Create Event File', + value: 'createEventFile', + }, + ], + default: 'createEventFile', + }, + { + displayName: 'Event Title', + name: 'title', + type: 'string', + default: '', + }, + { + displayName: 'Start', + name: 'start', + type: 'dateTime', + default: '', + required: true, + description: 'Date and time at which the event begins. (For all-day events, the time will be ignored.)', + }, + { + displayName: 'End', + name: 'end', + type: 'dateTime', + default: '', + required: true, + description: 'Date and time at which the event ends. (For all-day events, the time will be ignored.)', + }, + { + displayName: 'All Day', + name: 'allDay', + type: 'boolean', + default: false, + description: 'Whether the event lasts all day or not.', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + description: 'The field that your iCalendar file will be
available under in the output.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'createEventFile', + ], + }, + }, + options: [ + { + displayName: 'Attendees', + name: 'attendeesUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Attendee', + default: {}, + options: [ + { + displayName: 'Attendees', + name: 'attendeeValues', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + default: '', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + default: '', + }, + { + displayName: 'RSVP', + name: 'rsvp', + type: 'boolean', + default: false, + description: `Whether the attendee has to confirm attendance or not.`, + }, + ], + }, + ], + }, + { + displayName: 'Busy Status', + name: 'busyStatus', + type: 'options', + options: [ + { + name: 'Busy', + value: 'BUSY', + }, + { + name: 'Tentative', + value: 'TENTATIVE', + }, + ], + default: '', + description: 'Used to specify busy status for Microsoft applications, like Outlook.', + }, + { + displayName: 'Calendar Name', + name: 'calName', + type: 'string', + default: '', + description: 'Specifies the calendar (not event) name. Used by Apple iCal and Microsoft Outlook (spec).', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + description: 'The name of the file to be generated. Default value is event.ics.', + }, + { + displayName: 'Geolocation', + name: 'geolocationUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + placeholder: 'Add Geolocation', + default: {}, + options: [ + { + displayName: 'Geolocation', + name: 'geolocationValues', + values: [ + { + displayName: 'Latitude', + name: 'lat', + type: 'string', + default: '', + }, + { + displayName: 'Longitude', + name: 'lon', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + description: 'The intended venue.', + }, + { + displayName: 'Recurrence Rule', + name: 'recurrenceRule', + type: 'string', + default: '', + description: `A rule to define the repeat pattern of the event (RRULE). (Rule generator)`, + }, + { + displayName: 'Organizer', + name: 'organizerUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + placeholder: 'Add Organizer', + default: {}, + options: [ + { + displayName: 'Organizer', + name: 'organizerValues', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + required: true, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + required: true, + }, + ], + }, + ], + }, + { + displayName: 'Sequence', + name: 'sequence', + type: 'number', + default: 0, + description: 'When sending an update for an event (with the same uid), defines the revision sequence number.', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Confirmed', + value: 'CONFIRMED', + }, + { + name: 'Cancelled', + value: 'CANCELLED', + }, + { + name: 'Tentative', + value: 'TENTATIVE', + }, + ], + default: 'CONFIRMED', + }, + { + displayName: 'UID', + name: 'uid', + type: 'string', + default: '', + description: `Universally unique id for the event (will be auto-generated if not specified here). Should be globally unique.`, + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + description: 'URL associated with event.', + }, + ], + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const length = (items.length as unknown) as number; + const returnData: INodeExecutionData[] = []; + const operation = this.getNodeParameter('operation', 0) as string; + if (operation === 'createEventFile') { + for (let i = 0; i < length; i++) { + const title = this.getNodeParameter('title', i) as string; + const allDay = this.getNodeParameter('allDay', i) as boolean; + const start = this.getNodeParameter('start', i) as string; + let end = this.getNodeParameter('end', i) as string; + end = (allDay) ? moment(end).utc().add(1, 'day').format() as string : end; + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + let fileName = 'event.ics'; + + if (additionalFields.fileName) { + fileName = additionalFields.fileName as string; + } + + const data: ics.EventAttributes = { + title, + start: (moment(start).toArray().splice(0, (allDay) ? 3 : 6) as ics.DateArray), + end: (moment(end).toArray().splice(0, (allDay) ? 3 : 6) as ics.DateArray), + startInputType: 'utc', + endInputType: 'utc', + }; + + if (additionalFields.geolocationUi) { + data.geo = (additionalFields.geolocationUi as IDataObject).geolocationValues as ics.GeoCoordinates; + delete additionalFields.geolocationUi; + } + + if (additionalFields.organizerUi) { + data.organizer = (additionalFields.organizerUi as IDataObject).organizerValues as ics.Person; + delete additionalFields.organizerUi; + } + + if (additionalFields.attendeesUi) { + data.attendees = (additionalFields.attendeesUi as IDataObject).attendeeValues as ics.Attendee[]; + delete additionalFields.attendeesUi; + } + + Object.assign(data, additionalFields); + const buffer = Buffer.from(await createEvent(data) as string); + const binaryData = await this.helpers.prepareBinaryData(buffer, fileName, 'text/calendar'); + returnData.push( + { + json: {}, + binary: { + [binaryPropertyName]: binaryData, + }, + }, + ); + } + } + return [returnData]; + } +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index fac858637f..9fd9a948ce 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -394,6 +394,7 @@ "dist/nodes/Hubspot/HubspotTrigger.node.js", "dist/nodes/HumanticAI/HumanticAi.node.js", "dist/nodes/Hunter/Hunter.node.js", + "dist/nodes/ICalendar.node.js", "dist/nodes/If.node.js", "dist/nodes/Iterable/Iterable.node.js", "dist/nodes/Intercom/Intercom.node.js", @@ -608,6 +609,7 @@ "glob-promise": "^3.4.0", "gm": "^1.23.1", "iconv-lite": "^0.6.2", + "ics": "^2.27.0", "imap-simple": "^4.3.0", "iso-639-1": "^2.1.3", "jsonwebtoken": "^8.5.1",