diff --git a/packages/nodes-base/credentials/JiraSoftwareCloudApi.credentials.ts b/packages/nodes-base/credentials/JiraSoftwareCloudApi.credentials.ts new file mode 100644 index 0000000000..b1c54bfd54 --- /dev/null +++ b/packages/nodes-base/credentials/JiraSoftwareCloudApi.credentials.ts @@ -0,0 +1,29 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class JiraSoftwareCloudApi implements ICredentialType { + name = 'jiraSoftwareCloudApi'; + displayName = 'Jira Software Cloud API'; + properties = [ + { + displayName: 'Email', + name: 'email', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'API Token', + name: 'apiToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Domain', + name: 'domain', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Jira/GenericFunctions.ts b/packages/nodes-base/nodes/Jira/GenericFunctions.ts new file mode 100644 index 0000000000..b6c6b11674 --- /dev/null +++ b/packages/nodes-base/nodes/Jira/GenericFunctions.ts @@ -0,0 +1,83 @@ +import { OptionsWithUri } from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions, + BINARY_ENCODING +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('jiraSoftwareCloudApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const data = Buffer.from(`${credentials!.email}:${credentials!.apiToken}`).toString(BINARY_ENCODING); + const headerWithAuthentication = Object.assign({}, + { Authorization: `Basic ${data}`, Accept: 'application/json', 'Content-Type': 'application/json' }); + const options: OptionsWithUri = { + headers: headerWithAuthentication, + method, + qs: query, + uri: uri || `${credentials.domain}/rest/api/2${endpoint}`, + body, + json: true + }; + + try { + return await this.helpers.request!(options); + } catch (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 intercom endpoint + * and return all results + */ +export async function jiraSoftwareCloudApiRequestAllItems(this: IHookFunctions | IExecuteFunctions, propertyName: string, endpoint: string, method: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + query.maxResults = 100; + + let uri: string | undefined; + + do { + responseData = await jiraSoftwareCloudApiRequest.call(this, endpoint, method, body, query, uri); + uri = responseData.nextPage; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.isLast !== false && + responseData.nextPage !== undefined && + responseData.nextPage !== null + ); + + return returnData; +} + + +export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any + let result; + try { + result = JSON.parse(json!); + } catch (exception) { + result = ''; + } + return result; +} diff --git a/packages/nodes-base/nodes/Jira/IssueDescription.ts b/packages/nodes-base/nodes/Jira/IssueDescription.ts new file mode 100644 index 0000000000..eb9079603e --- /dev/null +++ b/packages/nodes-base/nodes/Jira/IssueDescription.ts @@ -0,0 +1,857 @@ +import { INodeProperties } from "n8n-workflow"; + +export const issueOpeations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'issue', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new issue', + }, + { + name: 'Update', + value: 'update', + description: 'Update an issue', + }, + { + name: 'Get', + value: 'get', + description: 'Get an issue', + }, + { + name: 'Changelog', + value: 'changelog', + description: 'Get issue changelog', + }, + { + name: 'Notify', + value: 'notify', + description: 'Creates an email notification for an issue and adds it to the mail queue.', + }, + { + name: 'Transitions', + value: 'transitions', + description: `Returns either all transitions or a transition that can be performed by
+ the user on an issue, based on the issue's status.`, + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an issue', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const issueFields = [ + +/* -------------------------------------------------------------------------- */ +/* issue:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Project', + name: 'project', + type: 'options', + required: true, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'create' + ] + }, + }, + typeOptions: { + loadOptionsMethod: 'getProjects', + }, + description: 'Project', + }, + { + displayName: 'Issue Type', + name: 'issueType', + type: 'options', + required: true, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'create' + ] + }, + }, + typeOptions: { + loadOptionsMethod: 'getIssueTypes', + }, + description: 'Issue Types', + }, + { + displayName: 'Summary', + name: 'summary', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'Summary', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Parent Issue Key', + name: 'parentIssueKey', + type: 'string', + required: false, + default: '', + description: 'Parent Issue Key', + }, + { + displayName: 'Labels', + name: 'labels', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getLabels', + }, + default: [], + required : false, + description: 'Labels', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getPriorities', + }, + default: [], + required : false, + description: 'Priority', + }, + { + displayName: 'Assignee', + name: 'assignee', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: [], + required : false, + description: 'Assignee', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + required : false, + description: 'Description', + }, + { + displayName: 'Update History', + name: 'updateHistory', + type: 'boolean', + default: false, + required : false, + description: `Whether the project in which the issue is created is added to the user's
+ Recently viewed project list, as shown under Projects in Jira.`, + }, + ], + }, + +/* -------------------------------------------------------------------------- */ +/* issue:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Issue Key', + name: 'issueKey', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'Issue Key', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Issue Type', + name: 'issueType', + type: 'options', + required: false, + typeOptions: { + loadOptionsMethod: 'getIssueTypes', + }, + default: '', + description: 'Issue Types', + }, + { + displayName: 'Summary', + name: 'summary', + type: 'string', + required: false, + default: '', + description: 'Summary', + }, + { + displayName: 'Parent Issue Key', + name: 'parentIssueKey', + type: 'string', + required: false, + default: '', + description: 'Parent Issue Key', + }, + { + displayName: 'Labels', + name: 'labels', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getLabels', + }, + default: [], + required : false, + description: 'Labels', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getPriorities', + }, + default: [], + required : false, + description: 'Priority', + }, + { + displayName: 'Assignee', + name: 'assignee', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: [], + required : false, + description: 'Assignee', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + required : false, + description: 'Description', + }, + ], + }, + +/* -------------------------------------------------------------------------- */ +/* issue:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Issue Key', + name: 'issueKey', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'delete', + ], + }, + }, + default: '', + description: 'Issue Key', + }, + { + displayName: 'Delete Subtasks', + name: 'deleteSubtasks', + type: 'boolean', + required: true, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'delete', + ], + }, + }, + default: false, + description: 'Delete Subtasks', + }, + +/* -------------------------------------------------------------------------- */ +/* issue:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Issue Key', + name: 'issueKey', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'Issue Key', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + required: false, + default: '', + description: `A list of fields to return for the issue.
+ This parameter accepts a comma-separated list.
+ Use it to retrieve a subset of fields. Allowed values:
+ *all Returns all fields.
+ *navigable Returns navigable fields.
+ Any issue field, prefixed with a minus to exclude.
` + }, + { + displayName: 'Fields By Key', + name: 'fieldsByKey', + type: 'boolean', + required: false, + default: false, + description: `Indicates whether fields in fields are referenced by keys rather than IDs.
+ This parameter is useful where fields have been added by a connect app and a field's key
+ may differ from its ID.`, + }, + { + displayName: 'Expand', + name: 'expand', + type: 'string', + required: false, + default: '', + description: `Use expand to include additional information about the issues in the response.
+ This parameter accepts a comma-separated list. Expand options include:
+ renderedFields Returns field values rendered in HTML format.
+ names Returns the display name of each field.
+ schema Returns the schema describing a field type.
+ transitions Returns all possible transitions for the issue.
+ editmeta Returns information about how each field can be edited.
+ changelog Returns a list of recent updates to an issue, sorted by date, starting from the most recent.
+ versionedRepresentations Returns a JSON array for each version of a field's value, with the highest number
+ representing the most recent version. Note: When included in the request, the fields parameter is ignored.` + }, + { + displayName: 'Properties', + name: 'properties', + type: 'string', + required: false, + default: '', + description: `A list of issue properties to return for the issue.
+ This parameter accepts a comma-separated list. Allowed values:
+ *all Returns all issue properties.
+ Any issue property key, prefixed with a minus to exclude.
+ Examples:
+ *all Returns all properties.
+ *all,-prop1 Returns all properties except prop1.
+ prop1,prop2 Returns prop1 and prop2 properties.
+ This parameter may be specified multiple times. For example, properties=prop1,prop2& properties=prop3.` + }, + { + displayName: 'Update History', + name: 'updateHistory', + type: 'boolean', + required: false, + default: false, + description: `Whether the project in which the issue is created is added to the user's + Recently viewed project list, as shown under Projects in Jira. This also populates the + JQL issues search lastViewed field.`, + }, + ] + }, + +/* -------------------------------------------------------------------------- */ +/* issue:changelog */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Issue Key', + name: 'issueKey', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'changelog', + ], + }, + }, + default: '', + description: 'Issue Key', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'changelog', + ], + }, + }, + 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: [ + 'issue', + ], + operation: [ + 'changelog', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, +/* -------------------------------------------------------------------------- */ +/* issue:notify */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Issue Key', + name: 'issueKey', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'notify', + ], + }, + }, + default: '', + description: 'Issue Key', + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'notify', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'notify', + ], + }, + }, + options: [ + { + displayName: 'Subject', + name: 'subject', + type: 'string', + required: false, + default: '', + description: `The subject of the email notification for the issue. If this is not specified, + then the subject is set to the issue key and summary.` + }, + { + displayName: 'Text Body', + name: 'textBody', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + required: false, + default: '', + description: `The subject of the email notification for the issue. + If this is not specified, then the subject is set to the issue key and summary.` + }, + { + displayName: 'HTML Body', + name: 'htmlBody', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + required: false, + default: '', + description: 'The HTML body of the email notification for the issue.', + }, + ], + }, + { + displayName: 'Notification Recipients', + name: 'notificationRecipientsUi', + type: 'fixedCollection', + placeholder: 'Add Recipients', + typeOptions: { + multipleValues: false, + }, + description: 'The recipients of the email notification for the issue.', + default: {}, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'notify', + ], + jsonParameters: [ + false, + ], + }, + }, + options: [ + { + name: 'notificationRecipientsValues', + displayName: 'Recipients', + values: [ + { + displayName: 'Reporter', + name: 'reporter', + type: 'boolean', + description: `Indicates whether the notification should be sent to the issue's reporter.`, + default: false, + }, + { + displayName: 'Assignee', + name: 'assignee', + type: 'boolean', + default: false, + description: `Indicates whether the notification should be sent to the issue's assignees.`, + }, + { + displayName: 'Watchers', + name: 'watchers', + type: 'boolean', + default: false, + description: `Indicates whether the notification should be sent to the issue's assignees.`, + }, + { + displayName: 'Voters', + name: 'voters', + type: 'boolean', + default: false, + description: `Indicates whether the notification should be sent to the issue's voters.`, + }, + { + displayName: 'Users', + name: 'users', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: [], + description: `List of users to receive the notification.`, + }, + { + displayName: 'Groups', + name: 'groups', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getGroups', + }, + default: [], + description: `List of groups to receive the notification.`, + }, + ] + + } + ] + }, + { + displayName: 'Notification Recipients', + name: 'notificationRecipientsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + required: false, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'notify', + ], + jsonParameters: [ + true, + ] + }, + }, + default: '', + description: 'The recipients of the email notification for the issue.', + }, + { + displayName: 'Notification Recipients Restrictions', + name: 'notificationRecipientsRestrictionsUi', + type: 'fixedCollection', + placeholder: 'Add Recipients Restriction', + typeOptions: { + multipleValues: false, + }, + description: 'Restricts the notifications to users with the specified permissions.', + default: {}, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'notify', + ], + jsonParameters: [ + false, + ], + }, + }, + options: [ + { + name: 'notificationRecipientsRestrictionsValues', + displayName: 'Recipients Restrictions', + values: [ + { + displayName: 'Users', + name: 'users', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: [], + description: `List of users to receive the notification.`, + }, + { + displayName: 'Groups', + name: 'groups', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getGroups', + }, + default: [], + description: `List of groups to receive the notification.`, + }, + ] + + } + ] + }, + { + displayName: 'Notification Recipients Restrictions', + name: 'notificationRecipientsRestrictionsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + required: false, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'notify', + ], + jsonParameters: [ + true, + ] + }, + }, + default: '', + description: 'Restricts the notifications to users with the specified permissions.', + }, + +/* -------------------------------------------------------------------------- */ +/* issue:transitions */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Issue Key', + name: 'issueKey', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'transitions', + ], + }, + }, + default: '', + description: 'Issue Key', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'issue', + ], + operation: [ + 'transitions', + ], + }, + }, + options: [ + { + displayName: 'Expand', + name: 'expand', + type: 'string', + required: false, + default: '', + description: `Use expand to include additional information about transitions in the response.
+ This parameter accepts transitions.fields, which returns information about the fields in the
+ transition screen for each transition. Fields hidden from the screen are not returned. Use this
+ information to populate the fields and update fields in Transition issue.` + }, + { + displayName: 'Transition ID', + name: 'transitionId', + type: 'string', + required: false, + default: '', + description: 'The ID of the transition.', + }, + { + displayName: 'Skip Remote Only Condition', + name: 'skipRemoteOnlyCondition', + type: 'boolean', + required: false, + default: false, + description: `Indicates whether transitions with the condition Hide
+ From User Condition are included in the response.`, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Jira/IssueInterface.ts b/packages/nodes-base/nodes/Jira/IssueInterface.ts new file mode 100644 index 0000000000..59ba0ca1e7 --- /dev/null +++ b/packages/nodes-base/nodes/Jira/IssueInterface.ts @@ -0,0 +1,37 @@ +import { IDataObject } from "n8n-workflow"; + +export interface IFields { + summary?: string; + project?: IDataObject; + issuetype?: IDataObject; + labels?: string[]; + priority?: IDataObject; + assignee?: IDataObject; + description?: string; + parent?: IDataObject; +} + +export interface IIssue { + fields?: IFields; +} + +export interface INotify { + subject?: string; + textBody?: string; + htmlBody?: string; + to?: INotificationRecipients; + restrict?: NotificationRecipientsRestrictions; +} + +export interface INotificationRecipients { + reporter?: boolean; + assignee?: boolean; + watchers?: boolean; + voters?: boolean; + users?: IDataObject[]; + groups?: IDataObject[]; +} + +export interface NotificationRecipientsRestrictions { + groups?: IDataObject[]; +} diff --git a/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts b/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts new file mode 100644 index 0000000000..dcf3276046 --- /dev/null +++ b/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts @@ -0,0 +1,496 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; +import { + jiraSoftwareCloudApiRequest, + jiraSoftwareCloudApiRequestAllItems, + validateJSON, +} from './GenericFunctions'; +import { + issueOpeations, + issueFields, +} from './IssueDescription'; +import { + IIssue, + IFields, + INotify, + INotificationRecipients, + NotificationRecipientsRestrictions, + } from './IssueInterface'; + +export class JiraSoftwareCloud implements INodeType { + description: INodeTypeDescription = { + displayName: 'Jira Software Cloud', + name: 'Jira Software Cloud', + icon: 'file:jira.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Jira Software Cloud API', + defaults: { + name: 'Jira Software Cloud', + color: '#c02428', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'jiraSoftwareCloudApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Issue', + value: 'issue', + description: 'Creates an issue or, where the option to create subtasks is enabled in Jira, a subtask', + }, + ], + default: 'issue', + description: 'Resource to consume.', + }, + ...issueOpeations, + ...issueFields, + ], + }; + + methods = { + loadOptions: { + // Get all the projects to display them to user so that he can + // select them easily + async getProjects(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let projects; + try { + projects = await jiraSoftwareCloudApiRequest.call(this, '/project/search', 'GET'); + } catch (err) { + throw new Error(`Jira Error: ${err}`); + } + for (const project of projects.values) { + const projectName = project.name; + const projectId = project.id; + + returnData.push({ + name: projectName, + value: projectId, + }); + } + return returnData; + }, + + // Get all the issue types to display them to user so that he can + // select them easily + async getIssueTypes(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let issueTypes; + try { + issueTypes = await jiraSoftwareCloudApiRequest.call(this, '/issuetype', 'GET'); + } catch (err) { + throw new Error(`Jira Error: ${err}`); + } + for (const issueType of issueTypes) { + const issueTypeName = issueType.name; + const issueTypeId = issueType.id; + + returnData.push({ + name: issueTypeName, + value: issueTypeId, + }); + } + return returnData; + }, + + // Get all the labels to display them to user so that he can + // select them easily + async getLabels(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let labels; + try { + labels = await jiraSoftwareCloudApiRequest.call(this, '/label', 'GET'); + } catch (err) { + throw new Error(`Jira Error: ${err}`); + } + for (const label of labels.values) { + const labelName = label; + const labelId = label; + + returnData.push({ + name: labelName, + value: labelId, + }); + } + return returnData; + }, + + // Get all the priorities to display them to user so that he can + // select them easily + async getPriorities(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let priorities; + try { + priorities = await jiraSoftwareCloudApiRequest.call(this, '/priority', 'GET'); + } catch (err) { + throw new Error(`Jira Error: ${err}`); + } + for (const priority of priorities) { + const priorityName = priority.name; + const priorityId = priority.id; + + returnData.push({ + name: priorityName, + value: priorityId, + }); + } + return returnData; + }, + + // Get all the users to display them to user so that he can + // select them easily + async getUsers(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let users; + try { + users = await jiraSoftwareCloudApiRequest.call(this, '/users/search', 'GET'); + } catch (err) { + throw new Error(`Jira Error: ${err}`); + } + for (const user of users) { + const userName = user.displayName; + const userId = user.accountId; + + returnData.push({ + name: userName, + value: userId, + }); + } + return returnData; + }, + + // Get all the groups to display them to user so that he can + // select them easily + async getGroups(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let groups; + try { + groups = await jiraSoftwareCloudApiRequest.call(this, '/groups/picker', 'GET'); + } catch (err) { + throw new Error(`Jira Error: ${err}`); + } + for (const group of groups.groups) { + const groupName = group.name; + const groupId = group.name; + + returnData.push({ + name: groupName, + value: groupId, + }); + } + return returnData; + } + } + }; + + 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 = {}; + for (let i = 0; i < length; i++) { + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + if (resource === 'issue') { + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-post + if (operation === 'create') { + const summary = this.getNodeParameter('summary', i) as string; + const projectId = this.getNodeParameter('project', i) as string; + const issueTypeId = this.getNodeParameter('issueType', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IIssue = {}; + const fields: IFields = { + summary, + project: { + id: projectId, + }, + issuetype: { + id: issueTypeId, + }, + }; + if (additionalFields.labels) { + fields.labels = additionalFields.labels as string[]; + } + if (additionalFields.priority) { + fields.priority = { + id: additionalFields.priority as string, + }; + } + if (additionalFields.assignee) { + fields.assignee = { + id: additionalFields.assignee as string, + }; + } + if (additionalFields.description) { + fields.description = additionalFields.description as string; + } + if (additionalFields.updateHistory) { + qs.updateHistory = additionalFields.updateHistory as boolean; + } + const issueTypes = await jiraSoftwareCloudApiRequest.call(this, '/issuetype', 'GET', body, qs); + const subtaskIssues = []; + for (const issueType of issueTypes) { + if (issueType.subtask) { + subtaskIssues.push(issueType.id); + } + } + if (!additionalFields.parentIssueKey + && subtaskIssues.includes(issueTypeId)) { + throw new Error('You must define a Parent Issue Key when Issue type is sub-task'); + + } else if (additionalFields.parentIssueKey + && subtaskIssues.includes(issueTypeId)) { + fields.parent = { + key: (additionalFields.parentIssueKey as string).toUpperCase(), + }; + } + body.fields = fields; + try { + responseData = await jiraSoftwareCloudApiRequest.call(this, '/issue', 'POST', body); + } catch (err) { + throw new Error(`Jira Error: ${JSON.stringify(err)}`); + } + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-put + if (operation === 'update') { + const issueKey = this.getNodeParameter('issueKey', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: IIssue = {}; + const fields: IFields = {}; + if (updateFields.summary) { + fields.summary = updateFields.summary as string; + } + if (updateFields.issueType) { + fields.issuetype = { + id: updateFields.issueType as string, + }; + } + if (updateFields.labels) { + fields.labels = updateFields.labels as string[]; + } + if (updateFields.priority) { + fields.priority = { + id: updateFields.priority as string, + }; + } + if (updateFields.assignee) { + fields.assignee = { + id: updateFields.assignee as string, + }; + } + if (updateFields.description) { + fields.description = updateFields.description as string; + } + const issueTypes = await jiraSoftwareCloudApiRequest.call(this, '/issuetype', 'GET', body); + const subtaskIssues = []; + for (const issueType of issueTypes) { + if (issueType.subtask) { + subtaskIssues.push(issueType.id); + } + } + if (!updateFields.parentIssueKey + && subtaskIssues.includes(updateFields.issueType)) { + throw new Error('You must define a Parent Issue Key when Issue type is sub-task'); + + } else if (updateFields.parentIssueKey + && subtaskIssues.includes(updateFields.issueType)) { + fields.parent = { + key: (updateFields.parentIssueKey as string).toUpperCase(), + }; + } + body.fields = fields; + try { + responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}`, 'PUT', body); + } catch (err) { + throw new Error(`Jira Error: ${JSON.stringify(err)}`); + } + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-get + if (operation === 'get') { + const issueKey = this.getNodeParameter('issueKey', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + if (additionalFields.fields) { + qs.fields = additionalFields.fields as string; + } + if (additionalFields.fieldsByKey) { + qs.fieldsByKey = additionalFields.fieldsByKey as boolean; + } + if (additionalFields.expand) { + qs.expand = additionalFields.expand as string; + } + if (additionalFields.properties) { + qs.properties = additionalFields.properties as string; + } + if (additionalFields.updateHistory) { + qs.updateHistory = additionalFields.updateHistory as string; + } + try { + responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}`, 'GET', {}, qs); + } catch (err) { + throw new Error(`Jira Error: ${JSON.stringify(err)}`); + } + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-changelog-get + if (operation === 'changelog') { + const issueKey = this.getNodeParameter('issueKey', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + try { + if (returnAll) { + responseData = await jiraSoftwareCloudApiRequestAllItems.call(this, 'values',`/issue/${issueKey}/changelog`, 'GET'); + } else { + qs.maxResults = this.getNodeParameter('limit', i) as number; + responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}/changelog`, 'GET', {}, qs); + responseData = responseData.values; + } + } catch (err) { + throw new Error(`Jira Error: ${JSON.stringify(err)}`); + } + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-notify-post + if (operation === 'notify') { + const issueKey = this.getNodeParameter('issueKey', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const jsonActive = this.getNodeParameter('jsonParameters', 0) as boolean; + const body: INotify = {}; + if (additionalFields.textBody) { + body.textBody = additionalFields.textBody as string; + } + if (additionalFields.htmlBody) { + body.htmlBody = additionalFields.htmlBody as string; + } + if (!jsonActive) { + const notificationRecipientsValues = (this.getNodeParameter('notificationRecipientsUi', i) as IDataObject).notificationRecipientsValues as IDataObject[]; + const notificationRecipients: INotificationRecipients = {}; + if (notificationRecipientsValues) { + // @ts-ignore + if (notificationRecipientsValues.reporter) { + // @ts-ignore + notificationRecipients.reporter = notificationRecipientsValues.reporter as boolean; + } + // @ts-ignore + if (notificationRecipientsValues.assignee) { + // @ts-ignore + notificationRecipients.assignee = notificationRecipientsValues.assignee as boolean; + } + // @ts-ignore + if (notificationRecipientsValues.assignee) { + // @ts-ignore + notificationRecipients.watchers = notificationRecipientsValues.watchers as boolean; + } + // @ts-ignore + if (notificationRecipientsValues.voters) { + // @ts-ignore + notificationRecipients.watchers = notificationRecipientsValues.voters as boolean; + } + // @ts-ignore + if (notificationRecipientsValues.users.length > 0) { + // @ts-ignore + notificationRecipients.users = notificationRecipientsValues.users.map(user => { + return { + accountId: user + }; + }); + } + // @ts-ignore + if (notificationRecipientsValues.groups.length > 0) { + // @ts-ignore + notificationRecipients.groups = notificationRecipientsValues.groups.map(group => { + return { + name: group + }; + }); + } + } + body.to = notificationRecipients; + const notificationRecipientsRestrictionsValues = (this.getNodeParameter('notificationRecipientsRestrictionsUi', i) as IDataObject).notificationRecipientsRestrictionsValues as IDataObject[]; + const notificationRecipientsRestrictions: NotificationRecipientsRestrictions = {}; + if (notificationRecipientsRestrictionsValues) { + // @ts-ignore + if (notificationRecipientsRestrictionsValues.groups. length > 0) { + // @ts-ignore + notificationRecipientsRestrictions.groups = notificationRecipientsRestrictionsValues.groups.map(group => { + return { + name: group + }; + }); + } + } + body.restrict = notificationRecipientsRestrictions; + } else { + const notificationRecipientsJson = validateJSON(this.getNodeParameter('notificationRecipientsJson', i) as string); + if (notificationRecipientsJson) { + body.to = notificationRecipientsJson; + } + const notificationRecipientsRestrictionsJson = validateJSON(this.getNodeParameter('notificationRecipientsRestrictionsJson', i) as string); + if (notificationRecipientsRestrictionsJson) { + body.restrict = notificationRecipientsRestrictionsJson; + } + } + try { + responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}/notify`, 'POST', body, qs); + } catch (err) { + throw new Error(`Jira Error: ${JSON.stringify(err)}`); + } + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-transitions-get + if (operation === 'transitions') { + const issueKey = this.getNodeParameter('issueKey', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + if (additionalFields.transitionId) { + qs.transitionId = additionalFields.transitionId as string; + } + if (additionalFields.expand) { + qs.expand = additionalFields.expand as string; + } + if (additionalFields.skipRemoteOnlyCondition) { + qs.skipRemoteOnlyCondition = additionalFields.skipRemoteOnlyCondition as boolean; + } + try { + responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}/transitions`, 'GET', {}, qs); + responseData = responseData.transitions; + } catch (err) { + throw new Error(`Jira Error: ${JSON.stringify(err)}`); + } + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-delete + if (operation === 'delete') { + const issueKey = this.getNodeParameter('issueKey', i) as string; + const deleteSubtasks = this.getNodeParameter('deleteSubtasks', i) as boolean; + qs.deleteSubtasks = deleteSubtasks; + try { + responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}`, 'DELETE', {}, qs); + } catch (err) { + throw new Error(`Jira 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/Jira/jira.png b/packages/nodes-base/nodes/Jira/jira.png new file mode 100644 index 0000000000..db28f59c87 Binary files /dev/null and b/packages/nodes-base/nodes/Jira/jira.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 7a7e51eaa8..60f74c27c2 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -42,7 +42,8 @@ "dist/credentials/HttpDigestAuth.credentials.js", "dist/credentials/HttpHeaderAuth.credentials.js", "dist/credentials/IntercomApi.credentials.js", - "dist/credentials/Imap.credentials.js", + "dist/credentials/Imap.credentials.js", + "dist/credentials/JiraSoftwareCloudApi.credentials.js", "dist/credentials/LinkFishApi.credentials.js", "dist/credentials/MailchimpApi.credentials.js", "dist/credentials/MailgunApi.credentials.js", @@ -102,6 +103,7 @@ "dist/nodes/If.node.js", "dist/nodes/Interval.node.js", "dist/nodes/Intercom/Intercom.node.js", + "dist/nodes/Jira/JiraSoftwareCloud.node.js", "dist/nodes/LinkFish/LinkFish.node.js", "dist/nodes/Mailchimp/Mailchimp.node.js", "dist/nodes/Mailgun/Mailgun.node.js",