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 0000000000..8976bae6ef Binary files /dev/null and b/packages/nodes-base/nodes/Taiga/taiga.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index ba6cb1a53b..ba13e8ff07 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -157,6 +157,8 @@ "dist/credentials/SpotifyOAuth2Api.credentials.js", "dist/credentials/SurveyMonkeyApi.credentials.js", "dist/credentials/SurveyMonkeyOAuth2Api.credentials.js", + "dist/credentials/TaigaCloudApi.credentials.js", + "dist/credentials/TaigaServerApi.credentials.js", "dist/credentials/TelegramApi.credentials.js", "dist/credentials/TodoistApi.credentials.js", "dist/credentials/TodoistOAuth2Api.credentials.js", @@ -342,6 +344,8 @@ "dist/nodes/Salesmate/Salesmate.node.js", "dist/nodes/Segment/Segment.node.js", "dist/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.js", + "dist/nodes/Taiga/Taiga.node.js", + "dist/nodes/Taiga/TaigaTrigger.node.js", "dist/nodes/Telegram/Telegram.node.js", "dist/nodes/Telegram/TelegramTrigger.node.js", "dist/nodes/Todoist/Todoist.node.js",