From bbfe59c2111d849ecccef0757d68fcaa1667abb0 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 17 Sep 2020 17:20:12 -0400 Subject: [PATCH] :sparkles: Add Taiga Integration (#939) * Add Taiga integration * :zap: Improvements to Taiga-Node * :zap: Improvements * :zap: Small improvements * :zap: Improvements * :zap: Change project slug for project id * :zap: Small improvement Co-authored-by: renanfilipe --- .../credentials/TaigaCloudApi.credentials.ts | 23 ++ .../credentials/TaigaServerApi.credentials.ts | 30 ++ .../nodes/Taiga/GenericFunctions.ts | 129 +++++++ .../nodes-base/nodes/Taiga/IssueOperations.ts | 40 ++ packages/nodes-base/nodes/Taiga/Taiga.node.ts | 329 ++++++++++++++++ .../nodes/Taiga/TaigaTrigger.node.ts | 225 +++++++++++ .../nodes/Taiga/issueOperationFields.ts | 357 ++++++++++++++++++ packages/nodes-base/nodes/Taiga/taiga.png | Bin 0 -> 4820 bytes packages/nodes-base/package.json | 4 + 9 files changed, 1137 insertions(+) create mode 100644 packages/nodes-base/credentials/TaigaCloudApi.credentials.ts create mode 100644 packages/nodes-base/credentials/TaigaServerApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Taiga/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Taiga/IssueOperations.ts create mode 100644 packages/nodes-base/nodes/Taiga/Taiga.node.ts create mode 100644 packages/nodes-base/nodes/Taiga/TaigaTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Taiga/issueOperationFields.ts create mode 100644 packages/nodes-base/nodes/Taiga/taiga.png diff --git a/packages/nodes-base/credentials/TaigaCloudApi.credentials.ts b/packages/nodes-base/credentials/TaigaCloudApi.credentials.ts new file mode 100644 index 0000000000..8dc8d771dc --- /dev/null +++ b/packages/nodes-base/credentials/TaigaCloudApi.credentials.ts @@ -0,0 +1,23 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class TaigaCloudApi implements ICredentialType { + name = 'taigaCloudApi'; + displayName = 'Taiga Cloud API'; + properties = [ + { + displayName: 'Username', + name: 'username', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Password', + name: 'password', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/credentials/TaigaServerApi.credentials.ts b/packages/nodes-base/credentials/TaigaServerApi.credentials.ts new file mode 100644 index 0000000000..6e57b3b292 --- /dev/null +++ b/packages/nodes-base/credentials/TaigaServerApi.credentials.ts @@ -0,0 +1,30 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class TaigaServerApi implements ICredentialType { + name = 'taigaServerApi'; + displayName = 'Taiga Server API'; + properties = [ + { + displayName: 'Username', + name: 'username', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Password', + name: 'password', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'URL', + name: 'url', + type: 'string' as NodePropertyTypes, + default: '', + placeholder: 'https://taiga.yourdomain.com', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Taiga/GenericFunctions.ts b/packages/nodes-base/nodes/Taiga/GenericFunctions.ts new file mode 100644 index 0000000000..a73ee457b1 --- /dev/null +++ b/packages/nodes-base/nodes/Taiga/GenericFunctions.ts @@ -0,0 +1,129 @@ +import { + OptionsWithUri, + } from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + ICredentialDataDecryptedObject, + IDataObject, + } from 'n8n-workflow'; + + import { + createHash, +} from 'crypto'; + +export async function getAuthorization( + this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, + credentials?: ICredentialDataDecryptedObject, +): Promise { + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const { password, username } = credentials; + const options: OptionsWithUri = { + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + body: { + type: 'normal', + password, + username, + }, + uri: (credentials.url) ? `${credentials.url}/api/v1/auth` : 'https://api.taiga.io/api/v1/auth', + json: true, + }; + + try { + const response = await this.helpers.request!(options); + + return response.auth_token; + } catch (error) { + throw new Error('Taiga Error: ' + error.err || error); + } +} + +export async function taigaApiRequest( + this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, + method: string, + resource: string, + body = {}, + query = {}, + uri?: string | undefined, + option = {}, +): Promise { // tslint:disable-line:no-any + + const version = this.getNodeParameter('version', 0, 'cloud') as string; + + let credentials; + + if (version === 'server') { + credentials = this.getCredentials('taigaServerApi') as ICredentialDataDecryptedObject; + } else { + credentials = this.getCredentials('taigaCloudApi') as ICredentialDataDecryptedObject; + } + + const authToken = await getAuthorization.call(this, credentials); + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + auth: { + bearer: authToken, + }, + qs: query, + method, + body, + uri: uri || (credentials.url) ? `${credentials.url}/api/v1${resource}` : `https://api.taiga.io/api/v1${resource}`, + json: true + }; + + if (Object.keys(option).length !== 0) { + Object.assign(options, option); + } + + try { + return await this.helpers.request!(options); + } catch (error) { + let errorMessage = error; + if (error.response.body && error.response.body._error_message) { + errorMessage = error.response.body._error_message; + } + + throw new Error(`Taigan error response [${error.statusCode}]: ${errorMessage}`); + } +} + +export async function taigaApiRequestAllItems(this: IHookFunctions | IExecuteFunctions| ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + let uri: string | undefined; + + do { + responseData = await taigaApiRequest.call(this, method, resource, body, query, uri, { resolveWithFullResponse: true }); + returnData.push.apply(returnData, responseData.body); + uri = responseData.headers['x-pagination-next']; + if (query.limit && returnData.length >= query.limit) { + return returnData; + } + } while ( + responseData.headers['x-pagination-next'] !== undefined && + responseData.headers['x-pagination-next'] !== '' + ); + return returnData; +} + +export function getAutomaticSecret(credentials: ICredentialDataDecryptedObject) { + const data = `${credentials.username},${credentials.password}`; + return createHash('md5').update(data).digest('hex'); +} diff --git a/packages/nodes-base/nodes/Taiga/IssueOperations.ts b/packages/nodes-base/nodes/Taiga/IssueOperations.ts new file mode 100644 index 0000000000..b20d1e439e --- /dev/null +++ b/packages/nodes-base/nodes/Taiga/IssueOperations.ts @@ -0,0 +1,40 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const issueOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an issue', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an issue', + }, + { + name: 'Get', + value: 'get', + description: 'Get an issue', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all issues', + }, + { + name: 'Update', + value: 'update', + description: 'Update an issue', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Taiga/Taiga.node.ts b/packages/nodes-base/nodes/Taiga/Taiga.node.ts new file mode 100644 index 0000000000..be16ed3572 --- /dev/null +++ b/packages/nodes-base/nodes/Taiga/Taiga.node.ts @@ -0,0 +1,329 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + taigaApiRequest, + taigaApiRequestAllItems, +} from './GenericFunctions'; + +import { + issueOperations, +} from './IssueOperations'; + +import { + issueOperationFields, +} from './issueOperationFields'; + +export class Taiga implements INodeType { + description: INodeTypeDescription = { + displayName: 'Taiga', + name: 'taiga', + icon: 'file:taiga.png', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Taiga API', + defaults: { + name: 'Taiga', + color: '#772244', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'taigaCloudApi', + displayOptions: { + show: { + version: [ + 'cloud', + ], + }, + }, + required: true, + }, + { + name: 'taigaServerApi', + displayOptions: { + show: { + version: [ + 'server', + ], + }, + }, + required: true, + }, + ], + properties: [ + { + displayName: 'Taiga Version', + name: 'version', + type: 'options', + options: [ + { + name: 'Cloud', + value: 'cloud', + }, + { + name: 'Server (Self Hosted)', + value: 'server', + }, + ], + default: 'cloud', + }, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Issue', + value: 'issue', + }, + ], + default: 'issue', + description: 'Resource to consume.', + }, + ...issueOperations, + ...issueOperationFields, + ], + }; + + methods = { + loadOptions: { + // Get all the available tags to display them to user so that he can + // select them easily + async getTypes(this: ILoadOptionsFunctions): Promise { + const projectId = this.getCurrentNodeParameter('projectId') as string; + + const returnData: INodePropertyOptions[] = []; + + const types = await taigaApiRequest.call(this, 'GET', `/issue-types?project=${projectId}`); + for (const type of types) { + const typeName = type.name; + const typeId = type.id; + returnData.push({ + name: typeName, + value: typeId, + }); + } + return returnData; + }, + + // Get all the available statuses to display them to user so that he can + // select them easily + async getStatuses(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + const projectId = this.getCurrentNodeParameter('projectId') as string; + + const statuses = await taigaApiRequest.call(this,'GET', '/issue-statuses', {}, { project: projectId }); + for (const status of statuses) { + const statusName = status.name; + const statusId = status.id; + returnData.push({ + name: statusName, + value: statusId, + }); + } + return returnData; + }, + + // Get all the available users to display them to user so that he can + // select them easily + async getProjectUsers(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + const projectId = this.getCurrentNodeParameter('projectId') as string; + + const users = await taigaApiRequest.call(this,'GET', '/users', {}, { project: projectId }); + for (const user of users) { + const userName = user.username; + const userId = user.id; + returnData.push({ + name: userName, + value: userId, + }); + } + return returnData; + }, + + // Get all the available priorities to display them to user so that he can + // select them easily + async getProjectPriorities(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + const projectId = this.getCurrentNodeParameter('projectId') as string; + + const priorities = await taigaApiRequest.call(this,'GET', '/priorities', {}, { project: projectId }); + for (const priority of priorities) { + const priorityName = priority.name; + const priorityId = priority.id; + returnData.push({ + name: priorityName, + value: priorityId, + }); + } + return returnData; + }, + + // Get all the available severities to display them to user so that he can + // select them easily + async getProjectSeverities(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + const projectId = this.getCurrentNodeParameter('projectId') as string; + + const severities = await taigaApiRequest.call(this,'GET', '/severities', {}, { project: projectId }); + for (const severity of severities) { + const severityName = severity.name; + const severityId = severity.id; + returnData.push({ + name: severityName, + value: severityId, + }); + } + return returnData; + }, + + // Get all the available milestones to display them to user so that he can + // select them easily + async getProjectMilestones(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + const projectId = this.getCurrentNodeParameter('projectId') as string; + + const milestones = await taigaApiRequest.call(this,'GET', '/milestones', {}, { project: projectId }); + for (const milestone of milestones) { + const milestoneName = milestone.name; + const milestoneId = milestone.id; + returnData.push({ + name: milestoneName, + value: milestoneId, + }); + } + return returnData; + }, + + // Get all the available projects to display them to user so that he can + // select them easily + async getUserProjects(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + const { id } = await taigaApiRequest.call(this, 'GET', '/users/me'); + + const projects = await taigaApiRequest.call(this,'GET', '/projects', {}, { member: id }); + for (const project of projects) { + const projectName = project.name; + const projectId = project.id; + returnData.push({ + name: projectName, + value: projectId, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + let responseData; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + const qs: IDataObject = {}; + + for (let i = 0; i < items.length; i++) { + if (resource === 'issue') { + if (operation === 'create') { + const projectId = this.getNodeParameter('projectId', i) as number; + + const subject = this.getNodeParameter('subject', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + project: projectId, + subject, + }; + + Object.assign(body, additionalFields); + + if (body.tags) { + body.tags = (body.tags as string).split(',') as string[]; + } + + responseData = await taigaApiRequest.call(this, 'POST', '/issues', body); + } + + if (operation === 'update') { + + const issueId = this.getNodeParameter('issueId', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + const body: IDataObject = {}; + + Object.assign(body, updateFields); + + if (body.tags) { + body.tags = (body.tags as string).split(',') as string[]; + } + + const { version } = await taigaApiRequest.call(this, 'GET', `/issues/${issueId}`); + + body.version = version; + + responseData = await taigaApiRequest.call(this, 'PATCH', `/issues/${issueId}`, body); + } + + if (operation === 'delete') { + const issueId = this.getNodeParameter('issueId', i) as string; + responseData = await taigaApiRequest.call(this, 'DELETE', `/issues/${issueId}`); + responseData = { success: true }; + } + + if (operation === 'get') { + const issueId = this.getNodeParameter('issueId', i) as string; + responseData = await taigaApiRequest.call(this, 'GET', `/issues/${issueId}`); + } + + if (operation === 'getAll') { + + const projectId = this.getNodeParameter('projectId', i) as number; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + qs.project = projectId; + + if (returnAll === true) { + responseData = await taigaApiRequestAllItems.call(this, 'GET', '/issues', {}, qs); + + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await taigaApiRequestAllItems.call(this, 'GET', '/issues', {}, qs); + responseData = responseData.splice(0, qs.limit); + } + } + } + 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/Taiga/TaigaTrigger.node.ts b/packages/nodes-base/nodes/Taiga/TaigaTrigger.node.ts new file mode 100644 index 0000000000..952ca721a1 --- /dev/null +++ b/packages/nodes-base/nodes/Taiga/TaigaTrigger.node.ts @@ -0,0 +1,225 @@ +import { + ICredentialDataDecryptedObject, + IDataObject, + ILoadOptionsFunctions, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + IWebhookFunctions, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + IHookFunctions, +} from 'n8n-core'; + +import { + taigaApiRequest, + getAutomaticSecret, +} from './GenericFunctions'; + +// import { +// createHmac, +// } from 'crypto'; + +export class TaigaTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Taiga Trigger', + name: 'taigaTrigger', + icon: 'file:taiga.png', + group: ['trigger'], + version: 1, + subtitle: '={{"project:" + $parameter["projectSlug"]}}', + description: 'Handle Taiga events via webhook', + defaults: { + name: 'Taiga Trigger', + color: '#772244', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'taigaCloudApi', + displayOptions: { + show: { + version: [ + 'cloud', + ], + }, + }, + required: true, + }, + { + name: 'taigaServerApi', + displayOptions: { + show: { + version: [ + 'server', + ], + }, + }, + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Taiga Version', + name: 'version', + type: 'options', + options: [ + { + name: 'Cloud', + value: 'cloud', + }, + { + name: 'Server (Self Hosted)', + value: 'server', + }, + ], + default: 'cloud', + }, + { + displayName: 'Project ID', + name: 'projectId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUserProjects', + }, + default: '', + description: 'Project ID', + required: true, + }, + ], + }; + + methods = { + loadOptions: { + // Get all the available projects to display them to user so that he can + // select them easily + async getUserProjects(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + const { id } = await taigaApiRequest.call(this, 'GET', '/users/me'); + + const projects = await taigaApiRequest.call(this,'GET', '/projects', {}, { member: id }); + for (const project of projects) { + const projectName = project.name; + const projectId = project.id; + returnData.push({ + name: projectName, + value: projectId, + }); + } + return returnData; + }, + }, + }; + + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default') as string; + + const webhookData = this.getWorkflowStaticData('node'); + + const endpoint = `/webhooks`; + + const webhooks = await taigaApiRequest.call(this, 'GET', endpoint); + + for (const webhook of webhooks) { + if (webhook.url === webhookUrl) { + webhookData.webhookId = webhook.id; + webhookData.key = webhook.key; + return true; + } + } + + return false; + }, + async create(this: IHookFunctions): Promise { + const version = this.getNodeParameter('version') as string; + + let credentials; + + if (version === 'server') { + credentials = this.getCredentials('taigaServerApi') as ICredentialDataDecryptedObject; + } else { + credentials = this.getCredentials('taigaCloudApi') as ICredentialDataDecryptedObject; + } + + const webhookUrl = this.getNodeWebhookUrl('default') as string; + + const webhookData = this.getWorkflowStaticData('node'); + + const projectId = this.getNodeParameter('projectId') as string; + + const key = getAutomaticSecret(credentials); + + const body: IDataObject = { + name: `n8n-webhook:${webhookUrl}`, + url: webhookUrl, + key, //can't validate the secret, see: https://github.com/taigaio/taiga-back/issues/1031 + project: projectId, + }; + const { id } = await taigaApiRequest.call(this, 'POST', '/webhooks', body); + + webhookData.webhookId = id; + webhookData.key = key; + + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + try { + await taigaApiRequest.call(this, 'DELETE', `/webhooks/${webhookData.webhookId}`); + } catch(error) { + return false; + } + delete webhookData.webhookId; + delete webhookData.key; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + //const webhookData = this.getWorkflowStaticData('node'); + const req = this.getRequestObject(); + const bodyData = req.body; + //const headerData = this.getHeaderData(); + + + // TODO + // Validate signature + // https://github.com/taigaio/taiga-back/issues/1031 + + // //@ts-ignore + // const requestSignature: string = headerData['x-taiga-webhook-signature']; + + // if (requestSignature === undefined) { + // return {}; + // } + + // //@ts-ignore + // const computedSignature = createHmac('sha1', webhookData.key as string).update(JSON.stringify(bodyData)).digest('hex'); + + // if (requestSignature !== computedSignature) { + // return {}; + // } + + return { + workflowData: [ + this.helpers.returnJsonArray(bodyData) + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Taiga/issueOperationFields.ts b/packages/nodes-base/nodes/Taiga/issueOperationFields.ts new file mode 100644 index 0000000000..1989d59d06 --- /dev/null +++ b/packages/nodes-base/nodes/Taiga/issueOperationFields.ts @@ -0,0 +1,357 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const issueOperationFields = [ + { + displayName: 'Project ID', + name: 'projectId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUserProjects', + }, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'create', + 'getAll', + 'update', + ], + }, + }, + default: '', + description: 'The project ID.', + required: true, + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Assigned To', + name: 'assigned_to', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'projectId', + ], + loadOptionsMethod: 'getProjectUsers', + }, + default: '', + description: 'User id to you want assign the issue to', + }, + { + displayName: 'Blocked Note', + name: 'blocked_note', + type: 'string', + default: '', + description: 'Reason why the issue is blocked', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + }, + { + displayName: 'Is Blocked', + name: 'is_blocked', + type: 'boolean', + default: false, + }, + { + displayName: 'Is Closed', + name: 'is_closed', + type: 'boolean', + default: false, + }, + { + displayName: 'Milestone ID', + name: 'milestone', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'projectSlug', + ], + loadOptionsMethod: 'getProjectMilestones', + }, + default: '', + }, + { + displayName: 'Priority ID', + name: 'priority', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'projectSlug', + ], + loadOptionsMethod: 'getProjectPriorities', + }, + default: '', + }, + { + displayName: 'Severity ID', + name: 'severity', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'projectSlug', + ], + loadOptionsMethod: 'getProjectSeverities', + }, + default: '', + }, + { + displayName: 'Status ID', + name: 'status', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getStatuses', + }, + default: '', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + description: 'Tags separated by comma.', + default: '', + placeholder: 'product, sales', + }, + { + displayName: 'Type ID', + name: 'type', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'projectSlug', + ], + loadOptionsMethod: 'getTypes' + }, + default: '', + }, + ], + }, + { + displayName: 'Issue ID', + name: 'issueId', + type: 'string', + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'update', + 'delete', + 'get', + ], + }, + }, + default: '', + required: true, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Assigned To', + name: 'assigned_to', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'projectId', + ], + loadOptionsMethod: 'getProjectUsers', + }, + default: '', + description: 'User id to you want assign the issue to', + }, + { + displayName: 'Blocked Note', + name: 'blocked_note', + type: 'string', + default: '', + description: 'Reason why the issue is blocked', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + }, + { + displayName: 'Is Blocked', + name: 'is_blocked', + type: 'boolean', + default: false, + }, + { + displayName: 'Is Closed', + name: 'is_closed', + type: 'boolean', + default: false, + }, + { + displayName: 'Milestone ID', + name: 'milestone', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'projectSlug', + ], + loadOptionsMethod: 'getProjectMilestones', + }, + default: '', + }, + { + displayName: 'Priority ID', + name: 'priority', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'projectSlug', + ], + loadOptionsMethod: 'getProjectPriorities', + }, + default: '', + }, + { + displayName: 'Severity ID', + name: 'severity', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'projectSlug', + ], + loadOptionsMethod: 'getProjectSeverities', + }, + default: '', + }, + { + displayName: 'Status ID', + name: 'status', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getStatuses', + }, + default: '', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + description: 'Tags separated by comma.', + default: '', + placeholder: 'product, sales', + }, + { + displayName: 'Type ID', + name: 'type', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'projectSlug', + ], + loadOptionsMethod: 'getTypes' + }, + default: '', + }, + ], + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'issue', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'issue', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Taiga/taiga.png b/packages/nodes-base/nodes/Taiga/taiga.png new file mode 100644 index 0000000000000000000000000000000000000000..8976bae6efd011318ae3067112dec40b75db9535 GIT binary patch literal 4820 zcmV;_5-aVAP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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