From 99c016e31f3caa4596001f614e08146ddb146450 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 6 Dec 2019 12:04:50 -0500 Subject: [PATCH] :sparkles: flow node and trigger --- .../credentials/FlowApi.credentials.ts | 18 + packages/nodes-base/nodes/Flow/Flow.node.ts | 278 +++++++ .../nodes-base/nodes/Flow/FlowTrigger.node.ts | 215 +++++ .../nodes-base/nodes/Flow/GenericFunctions.ts | 67 ++ .../nodes-base/nodes/Flow/TaskDescription.ts | 734 ++++++++++++++++++ .../nodes-base/nodes/Flow/TaskInterface.ts | 27 + packages/nodes-base/nodes/Flow/flow.png | Bin 0 -> 3148 bytes packages/nodes-base/package.json | 3 + 8 files changed, 1342 insertions(+) create mode 100644 packages/nodes-base/credentials/FlowApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Flow/Flow.node.ts create mode 100644 packages/nodes-base/nodes/Flow/FlowTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Flow/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Flow/TaskDescription.ts create mode 100644 packages/nodes-base/nodes/Flow/TaskInterface.ts create mode 100644 packages/nodes-base/nodes/Flow/flow.png diff --git a/packages/nodes-base/credentials/FlowApi.credentials.ts b/packages/nodes-base/credentials/FlowApi.credentials.ts new file mode 100644 index 0000000000..80423fdd14 --- /dev/null +++ b/packages/nodes-base/credentials/FlowApi.credentials.ts @@ -0,0 +1,18 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class FlowApi implements ICredentialType { + name = 'flowApi'; + displayName = 'Flow API'; + properties = [ + { + displayName: 'Access Token', + name: 'accessToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Flow/Flow.node.ts b/packages/nodes-base/nodes/Flow/Flow.node.ts new file mode 100644 index 0000000000..2cf11298e1 --- /dev/null +++ b/packages/nodes-base/nodes/Flow/Flow.node.ts @@ -0,0 +1,278 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, +} from 'n8n-workflow'; +import { + flowApiRequest, + FlowApiRequestAllItems, +} from './GenericFunctions'; +import { + taskOpeations, + taskFields, +} from './TaskDescription'; +import { + ITask, TaskInfo, + } from './TaskInterface'; +import { response } from 'express'; + +export class Flow implements INodeType { + description: INodeTypeDescription = { + displayName: 'Flow', + name: 'Flow', + icon: 'file:flow.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Flow API', + defaults: { + name: 'Flow', + color: '#c02428', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'flowApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Task', + value: 'task', + description: `The primary unit within Flow; tasks track units of work and can be assigned, sorted, nested, and tagged.
+ Tasks can either be part of a List, or "private" (meaning "without a list", essentially).
+ Through this endpoint you are able to do anything you wish to your tasks in Flow, including create new ones.`, + }, + ], + default: 'task', + description: 'Resource to consume.', + }, + ...taskOpeations, + ...taskFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + let responseData; + const qs: IDataObject = {}; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + for (let i = 0; i < length; i++) { + if (resource === 'task') { + //https://developer.getflow.com/api/#tasks_create-task + if (operation === 'create') { + const organizationId = this.getNodeParameter('organizationId', i) as string; + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const name = this.getNodeParameter('name', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: ITask = { + organization_id: parseInt(organizationId, 10), + }; + const task: TaskInfo = { + name, + workspace_id: parseInt(workspaceId, 10) + }; + if (additionalFields.ownerId) { + task.owner_id = parseInt(additionalFields.ownerId as string, 10); + } + if (additionalFields.listId) { + task.list_id = parseInt(additionalFields.listId as string, 10); + } + if (additionalFields.startsOn) { + task.starts_on = additionalFields.startsOn as string; + } + if (additionalFields.dueOn) { + task.due_on = additionalFields.dueOn as string; + } + if (additionalFields.mirrorParentSubscribers) { + task.mirror_parent_subscribers = additionalFields.mirrorParentSubscribers as boolean; + } + if (additionalFields.mirrorParentTags) { + task.mirror_parent_tags = additionalFields.mirrorParentTags as boolean; + } + if (additionalFields.noteContent) { + task.note_content = additionalFields.noteContent as string; + } + if (additionalFields.noteMimeType) { + task.note_mime_type = additionalFields.noteMimeType as string; + } + if (additionalFields.parentId) { + task.parent_id = parseInt(additionalFields.parentId as string, 10); + } + if (additionalFields.positionList) { + task.position_list = additionalFields.positionList as number; + } + if (additionalFields.positionUpcoming) { + task.position_upcoming = additionalFields.positionUpcoming as number; + } + if (additionalFields.position) { + task.position = additionalFields.position as number; + } + if (additionalFields.sectionId) { + task.section_id = additionalFields.sectionId as number; + } + if (additionalFields.tags) { + task.tags = (additionalFields.tags as string).split(','); + } + body.task = task; + try { + responseData = await flowApiRequest.call(this, 'POST', '/tasks', body); + responseData = responseData.task; + } catch (err) { + throw new Error(`Flow Error: ${JSON.stringify(err)}`); + } + } + //https://developer.getflow.com/api/#tasks_update-a-task + if (operation === 'update') { + const organizationId = this.getNodeParameter('organizationId', i) as string; + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const taskId = this.getNodeParameter('taskId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: ITask = { + organization_id: parseInt(organizationId, 10), + }; + const task: TaskInfo = { + workspace_id: parseInt(workspaceId, 10), + id: parseInt(taskId, 10), + }; + if (updateFields.name) { + task.name = updateFields.name as string; + } + if (updateFields.ownerId) { + task.owner_id = parseInt(updateFields.ownerId as string, 10); + } + if (updateFields.listId) { + task.list_id = parseInt(updateFields.listId as string, 10); + } + if (updateFields.startsOn) { + task.starts_on = updateFields.startsOn as string; + } + if (updateFields.dueOn) { + task.due_on = updateFields.dueOn as string; + } + if (updateFields.mirrorParentSubscribers) { + task.mirror_parent_subscribers = updateFields.mirrorParentSubscribers as boolean; + } + if (updateFields.mirrorParentTags) { + task.mirror_parent_tags = updateFields.mirrorParentTags as boolean; + } + if (updateFields.noteContent) { + task.note_content = updateFields.noteContent as string; + } + if (updateFields.noteMimeType) { + task.note_mime_type = updateFields.noteMimeType as string; + } + if (updateFields.parentId) { + task.parent_id = parseInt(updateFields.parentId as string, 10); + } + if (updateFields.positionList) { + task.position_list = updateFields.positionList as number; + } + if (updateFields.positionUpcoming) { + task.position_upcoming = updateFields.positionUpcoming as number; + } + if (updateFields.position) { + task.position = updateFields.position as number; + } + if (updateFields.sectionId) { + task.section_id = updateFields.sectionId as number; + } + if (updateFields.tags) { + task.tags = (updateFields.tags as string).split(','); + } + if (updateFields.completed) { + task.completed = updateFields.completed as boolean; + } + body.task = task; + try { + responseData = await flowApiRequest.call(this, 'PUT', `/tasks/${taskId}`, body); + responseData = responseData.task; + } catch (err) { + throw new Error(`Flow Error: ${JSON.stringify(err)}`); + } + } + //https://developer.getflow.com/api/#tasks_get-task + if (operation === 'get') { + const organizationId = this.getNodeParameter('organizationId', i) as string; + const taskId = this.getNodeParameter('taskId', i) as string; + const filters = this.getNodeParameter('filters', i) as IDataObject; + qs.organization_id = organizationId; + if (filters.include) { + qs.include = (filters.include as string[]).join(','); + } + try { + responseData = await flowApiRequest.call(this,'GET', `/tasks/${taskId}`, {}, qs); + } catch (err) { + throw new Error(`Flow Error: ${JSON.stringify(err)}`); + } + } + //https://developer.getflow.com/api/#tasks_get-tasks + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const organizationId = this.getNodeParameter('organizationId', i) as string; + const filters = this.getNodeParameter('filters', i) as IDataObject; + qs.organization_id = organizationId; + if (filters.include) { + qs.include = (filters.include as string[]).join(','); + } + if (filters.order) { + qs.order = filters.order as string; + } + if (filters.workspaceId) { + qs.workspace_id = filters.workspaceId as string; + } + if (filters.createdBefore) { + qs.created_before = filters.createdBefore as string; + } + if (filters.createdAfter) { + qs.created_after = filters.createdAfter as string; + } + if (filters.updateBefore) { + qs.updated_before = filters.updateBefore as string; + } + if (filters.updateAfter) { + qs.updated_after = filters.updateAfter as string; + } + if (filters.deleted) { + qs.deleted = filters.deleted as boolean; + } + if (filters.cleared) { + qs.cleared = filters.cleared as boolean; + } + try { + if (returnAll === true) { + responseData = await FlowApiRequestAllItems.call(this, 'tasks', 'GET', '/tasks', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await flowApiRequest.call(this, 'GET', '/tasks', {}, qs); + responseData = responseData.tasks; + } + } catch (err) { + throw new Error(`Flow Error: ${JSON.stringify(err)}`); + } + } + } + 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/nodes/Flow/FlowTrigger.node.ts b/packages/nodes-base/nodes/Flow/FlowTrigger.node.ts new file mode 100644 index 0000000000..510d1e88ba --- /dev/null +++ b/packages/nodes-base/nodes/Flow/FlowTrigger.node.ts @@ -0,0 +1,215 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeTypeDescription, + INodeType, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + flowApiRequest, +} from './GenericFunctions'; + +export class FlowTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Flow Trigger', + name: 'flow', + icon: 'file:flow.png', + group: ['trigger'], + version: 1, + description: 'Handle Flow events via webhooks', + defaults: { + name: 'Flow Trigger', + color: '#559922', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'flowApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + required: true, + default: '', + description: 'Organization', + }, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + default: '', + options: + [ + { + name: 'Project', + value: 'list' + }, + { + name: 'Task', + value: 'task' + }, + ], + description: 'Resource that triggers the webhook', + }, + { + displayName: 'Project ID', + name: 'listIds', + type: 'string', + required: true, + default: [], + displayOptions: { + show: { + resource:[ + 'list' + ] + }, + hide: { + resource: [ + 'task' + ] + } + }, + description: `Lists ids, perhaps known better as "Projects" separated by ,`, + }, + { + displayName: 'Task ID', + name: 'taskIds', + type: 'string', + required: true, + default: [], + displayOptions: { + show: { + resource:[ + 'task' + ] + }, + hide: { + resource: [ + 'list' + ] + } + }, + description: `Taks ids separated by ,`, + }, + ], + + }; + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + let webhooks; + const qs: IDataObject = {}; + const webhookData = this.getWorkflowStaticData('node'); + if (!Array.isArray(webhookData.webhookIds)) { + webhookData.webhookIds = []; + } + if (!(webhookData.webhookIds as [number]).length) { + return false; + } + qs.organization_id = this.getNodeParameter('organizationId') as string; + const endpoint = `/integration_webhooks`; + try { + webhooks = await flowApiRequest.call(this, 'GET', endpoint, {}, qs); + webhooks = webhooks.integration_webhooks; + } catch (e) { + throw e; + } + for (const webhook of webhooks) { + // @ts-ignore + if (webhookData.webhookIds.includes(webhook.id)) { + continue; + } else { + return false; + } + } + return true; + }, + async create(this: IHookFunctions): Promise { + let resourceIds, body, responseData; + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const organizationId = this.getNodeParameter('organizationId') as string; + const resource = this.getNodeParameter('resource') as string; + const endpoint = `/integration_webhooks`; + if (resource === 'list') { + resourceIds = (this.getNodeParameter('listIds') as string).split(','); + } + if (resource === 'task') { + resourceIds = (this.getNodeParameter('taskIds') as string).split(','); + } + // @ts-ignore + for (const resourceId of resourceIds ) { + body = { + organization_id: organizationId, + integration_webhook: { + name: 'n8n-trigger', + url: webhookUrl, + resource_type: resource, + resource_id: parseInt(resourceId, 10), + } + }; + try { + responseData = await flowApiRequest.call(this, 'POST', endpoint, body); + } catch(error) { + return false; + } + if (responseData.integration_webhook === undefined + || responseData.integration_webhook.id === undefined) { + // Required data is missing so was not successful + return false; + } + // @ts-ignore + webhookData.webhookIds.push(responseData.integration_webhook.id); + } + return true; + }, + async delete(this: IHookFunctions): Promise { + const qs: IDataObject = {}; + const webhookData = this.getWorkflowStaticData('node'); + qs.organization_id = this.getNodeParameter('organizationId') as string; + // @ts-ignore + if (webhookData.webhookIds.length > 0) { + // @ts-ignore + for (const webhookId of webhookData.webhookIds ) { + const endpoint = `/integration_webhooks/${webhookId}`; + try { + await flowApiRequest.call(this, 'DELETE', endpoint, {}, qs); + } catch (e) { + return false; + } + } + delete webhookData.webhookIds; + } + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + return { + workflowData: [ + this.helpers.returnJsonArray(req.body) + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Flow/GenericFunctions.ts b/packages/nodes-base/nodes/Flow/GenericFunctions.ts new file mode 100644 index 0000000000..dc248326ec --- /dev/null +++ b/packages/nodes-base/nodes/Flow/GenericFunctions.ts @@ -0,0 +1,67 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions, +} from 'n8n-core'; +import { IDataObject } from 'n8n-workflow'; + +export async function flowApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('flowApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + let options: OptionsWithUri = { + headers: { 'Authorization': `Bearer ${credentials.accessToken}`}, + method, + qs, + body, + uri: uri ||`https://api.getflow.com/v2${resource}`, + json: true + }; + options = Object.assign({}, options, option); + if (Object.keys(options.body).length === 0) { + delete options.body; + } + try { + return await this.helpers.request!(options); + } catch (error) { + console.error(error); + + const errorMessage = error.response.body.message || error.response.body.Message; + + if (errorMessage !== undefined) { + throw errorMessage; + } + throw error.response.body; + } +} + +/** + * Make an API request to paginated flow endpoint + * and return all results + */ +export async function FlowApiRequestAllItems(this: IHookFunctions | IExecuteFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + query.limit = 100; + + let uri: string | undefined; + + do { + responseData = await flowApiRequest.call(this, method, resource, body, query, uri, { resolveWithFullResponse: true }); + uri = responseData.headers.link; + // @ts-ignore + returnData.push.apply(returnData, responseData.body[propertyName]); + } while ( + responseData.headers.link !== undefined && + responseData.headers.link !== '' + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Flow/TaskDescription.ts b/packages/nodes-base/nodes/Flow/TaskDescription.ts new file mode 100644 index 0000000000..b2d77fac4f --- /dev/null +++ b/packages/nodes-base/nodes/Flow/TaskDescription.ts @@ -0,0 +1,734 @@ +import { INodeProperties } from "n8n-workflow"; + +export const taskOpeations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'task', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new task', + }, + { + name: 'Update', + value: 'update', + description: 'Update task', + }, + { + name: 'Get', + value: 'get', + description: 'Get task', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all the tasks', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const taskFields = [ + +/* -------------------------------------------------------------------------- */ +/* task:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create' + ] + }, + }, + description: 'Select resources belonging to an organization.', + }, + { + displayName: 'Workspace ID', + name: 'workspaceId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create' + ] + }, + }, + description: 'Create resources under the given workspace.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create' + ] + }, + }, + description: 'The title of the task.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Owner ID', + name: 'ownerid', + type: 'string', + required: false, + default: '', + description: 'The ID of the account to whom this task will be assigned.', + }, + { + displayName: 'List ID', + name: 'listID', + type: 'string', + default: [], + required : false, + description: 'Put the new task in a list ("project"). Omit this param to have the task be private.', + }, + { + displayName: 'Starts On', + name: 'startsOn', + type: 'dateTime', + default: '', + required : false, + description: 'The date on which the task should start.', + }, + { + displayName: 'Due On', + name: 'dueOn', + type: 'dateTime', + default: '', + required : false, + description: 'The date on which the task should be due.', + }, + { + displayName: 'Mirror Parent Subscribers', + name: 'mirrorParentSubscribers', + type: 'boolean', + default: false, + required : false, + description: `If this task will be a subtask, and this is true, the parent tasks's subscribers will be mirrored to this one.`, + }, + { + displayName: 'Mirror Parent Tags', + name: 'mirrorParentTags', + type: 'boolean', + default: false, + required : false, + description: `If this task will be a subtask, and this is true, the parent tasks's tags will be mirrored to this one.`, + }, + { + displayName: 'Note Content', + name: 'noteContent', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + required : false, + description: `Provide the content for the task's note.`, + }, + { + displayName: 'Note Mime Type', + name: 'noteMimeType', + type: 'options', + default: [], + options: [ + { + name: 'text/plain', + value: 'text/plain', + }, + { + name: 'text/x-markdown', + value: 'text/x-markdown', + }, + { + name: 'text/html', + value: 'text/html', + } + ], + description: `Identify which markup language is used to format the given note`, + }, + { + displayName: 'Parent ID', + name: 'parentId', + type: 'string', + default: '', + required : false, + description: `If provided, this task will become a subtask of the given task.`, + }, + { + displayName: 'Position List', + name: 'positionList', + type: 'number', + default: 0, + required : false, + description: `Determines the sort order when showing tasks in, or grouped by, a list.`, + }, + { + displayName: 'Position Upcoming', + name: 'positionUpcoming', + type: 'number', + default: 0, + required : false, + description: `Determines the sort order when showing tasks grouped by their due_date.`, + }, + { + displayName: 'Position', + name: 'position', + type: 'number', + default: 0, + required : false, + description: `Determines the sort order of tasks.`, + }, + { + displayName: 'Section ID', + name: 'sectionId', + type: 'string', + default: '', + required : false, + description: `Specify which section under which to create this task.`, + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + default: '', + required : false, + description: `A list of tag names to apply to the new task separated by ,`, + }, + ], + }, + +/* -------------------------------------------------------------------------- */ +/* task:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'update' + ] + }, + }, + description: 'Select resources belonging to an organization.', + }, + { + displayName: 'Workspace ID', + name: 'workspaceId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'update' + ] + }, + }, + description: 'Create resources under the given workspace.', + }, + { + displayName: 'Task ID', + name: 'taskId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'update' + ] + }, + }, + description: '', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Update Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'The title of the task.', + }, + { + displayName: 'Completed', + name: 'completed', + type: 'boolean', + default: false, + description: `If set to true, will complete the task.`, + }, + { + displayName: 'Owner ID', + name: 'ownerid', + type: 'string', + default: '', + description: 'The ID of the account to whom this task will be assigned.', + }, + { + displayName: 'List ID', + name: 'listID', + type: 'string', + default: '', + description: 'Put the new task in a list ("project"). Omit this param to have the task be private.', + }, + { + displayName: 'Starts On', + name: 'startsOn', + type: 'dateTime', + default: '', + description: 'The date on which the task should start.', + }, + { + displayName: 'Due On', + name: 'dueOn', + type: 'dateTime', + default: '', + description: 'The date on which the task should be due.', + }, + { + displayName: 'Mirror Parent Subscribers', + name: 'mirrorParentSubscribers', + type: 'boolean', + default: false, + required : false, + description: `If this task will be a subtask, and this is true, the parent tasks's subscribers will be mirrored to this one.`, + }, + { + displayName: 'Mirror Parent Tags', + name: 'mirrorParentTags', + type: 'boolean', + default: false, + description: `If this task will be a subtask, and this is true, the parent tasks's tags will be mirrored to this one.`, + }, + { + displayName: 'Note Content', + name: 'noteContent', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Provide the content for the task's note.`, + }, + { + displayName: 'Note Mime Type', + name: 'noteMimeType', + type: 'options', + default: [], + options: [ + { + name: 'text/plain', + value: 'text/plain', + }, + { + name: 'text/x-markdown', + value: 'text/x-markdown', + }, + { + name: 'text/html', + value: 'text/html', + } + ], + description: `Identify which markup language is used to format the given note`, + }, + { + displayName: 'Parent ID', + name: 'parentId', + type: 'string', + default: '', + description: `If provided, this task will become a subtask of the given task.`, + }, + { + displayName: 'Position List', + name: 'positionList', + type: 'number', + default: 0, + description: `Determines the sort order when showing tasks in, or grouped by, a list.`, + }, + { + displayName: 'Position Upcoming', + name: 'positionUpcoming', + type: 'number', + default: 0, + description: `Determines the sort order when showing tasks grouped by their due_date.`, + }, + { + displayName: 'Position', + name: 'position', + type: 'number', + default: 0, + description: `Determines the sort order of tasks.`, + }, + { + displayName: 'Section ID', + name: 'sectionId', + type: 'string', + default: '', + description: `Specify which section under which to create this task.`, + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + default: '', + description: `A list of tag names to apply to the new task separated by ,`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* task:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'get' + ] + }, + }, + description: 'Select resources belonging to an organization.', + }, + { + displayName: 'Task ID', + name: 'taskId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'get' + ] + }, + }, + description: '', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Include', + name: 'include', + type: 'multiOptions', + default: [], + options: [ + { + name: 'schedule', + value: 'schedule', + }, + { + name: 'files', + value: 'files', + }, + { + name: 'file Associations', + value: 'file_associations', + }, + { + name: 'Parent', + value: 'parent', + }, + ] + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* task:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'getAll' + ] + }, + }, + description: 'Select resources belonging to an organization.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'getAll' + ] + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'getAll' + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Include', + name: 'include', + type: 'multiOptions', + default: [], + options: [ + { + name: 'schedule', + value: 'schedule', + }, + { + name: 'files', + value: 'files', + }, + { + name: 'file Associations', + value: 'file_associations', + }, + { + name: 'Parent', + value: 'parent', + }, + ] + }, + { + displayName: 'Order', + name: 'order', + type: 'options', + default: [], + options: [ + { + name: 'Due On', + value: 'due_on', + }, + { + name: 'Starts On', + value: 'starts_on', + }, + { + name: 'Created At', + value: 'created_at', + }, + { + name: 'Position', + value: 'position', + }, + { + name: 'Account ID', + value: 'account_id', + }, + { + name: 'List ID', + value: 'list_id', + }, + { + name: 'Section ID', + value: 'section_id', + }, + { + name: 'Owner ID', + value: 'owner_id', + }, + { + name: 'Name', + value: 'name', + }, + { + name: 'Completed At', + value: 'completed_at', + }, + { + name: 'Updated At', + value: 'updated_at', + }, + ] + }, + { + displayName: 'Workspace ID', + name: 'workspaceId', + type: 'string', + default: '', + description: 'Create resources under the given workspace.', + }, + { + displayName: 'Created Before', + name: 'createdBefore', + type: 'dateTime', + default: '', + description: 'Select resources created before a certain time.', + }, + { + displayName: 'Created After', + name: 'createdAfter', + type: 'dateTime', + default: '', + description: 'Select resources created after a certain time.', + }, + { + displayName: 'Update Before', + name: 'updateBefore', + type: 'dateTime', + default: '', + description: 'Select resources updated before a certain time.', + }, + { + displayName: 'Update After', + name: 'updateAfter', + type: 'dateTime', + default: '', + description: 'Select resources updated after a certain time.', + }, + { + displayName: 'Deleted', + name: 'deleted', + type: 'boolean', + default: false, + description: 'Select deleted resources.', + }, + { + displayName: 'Cleared', + name: 'cleared', + type: 'boolean', + default: false, + description: 'Select cleared resources.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Flow/TaskInterface.ts b/packages/nodes-base/nodes/Flow/TaskInterface.ts new file mode 100644 index 0000000000..7cfe1c5d97 --- /dev/null +++ b/packages/nodes-base/nodes/Flow/TaskInterface.ts @@ -0,0 +1,27 @@ + +export interface ITask { + organization_id?: number; + task?: TaskInfo; +} + +export interface TaskInfo { + workspace_id?: number; + id?: number; + name?: string; + owner_id?: number; + list_id?: number; + starts_on?: string; + due_on?: string; + mirror_parent_subscribers?: boolean; + mirror_parent_tags?: boolean; + note_content?: string; + note_mime_type?: string; + parent_id?: number; + position_list?: number; + position_upcoming?: number; + position?: number; + section_id?: number; + subscriptions?: number; + tags?: string[]; + completed?: boolean; +} diff --git a/packages/nodes-base/nodes/Flow/flow.png b/packages/nodes-base/nodes/Flow/flow.png new file mode 100644 index 0000000000000000000000000000000000000000..2dd6944a34ba24c1503ad827967ea742d5b27845 GIT binary patch literal 3148 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 cbA