diff --git a/packages/nodes-base/nodes/Jira/GenericFunctions.ts b/packages/nodes-base/nodes/Jira/GenericFunctions.ts index 66ca437ded..3f2e6dd82a 100644 --- a/packages/nodes-base/nodes/Jira/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Jira/GenericFunctions.ts @@ -1,4 +1,6 @@ -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, + } from 'request'; import { IExecuteFunctions, @@ -41,11 +43,21 @@ export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecut try { return await this.helpers.request!(options); } catch (error) { - let errorMessage = error; - if (error.error && error.error.errorMessages) { - errorMessage = error.error.errorMessages; + let errorMessage = error.message; + + if (error.response.body) { + if (error.response.body.errorMessages && error.response.body.errorMessages.length) { + errorMessage = JSON.stringify(error.response.body.errorMessages); + } else { + errorMessage = error.response.body.message || error.response.body.error || error.response.body.errors || error.message; + } } - throw new Error(errorMessage); + + if (typeof errorMessage !== 'string') { + errorMessage = JSON.stringify(errorMessage); + } + + throw new Error(`Jira error response [${error.statusCode}]: ${errorMessage}`); } } diff --git a/packages/nodes-base/nodes/Jira/IssueDescription.ts b/packages/nodes-base/nodes/Jira/IssueDescription.ts index ae46db23e3..5f3c18c9d6 100644 --- a/packages/nodes-base/nodes/Jira/IssueDescription.ts +++ b/packages/nodes-base/nodes/Jira/IssueDescription.ts @@ -44,7 +44,7 @@ export const issueOperations = [ description: 'Creates an email notification for an issue and adds it to the mail queue.', }, { - name: 'Transitions', + name: 'Status', 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.`, }, @@ -101,6 +101,9 @@ export const issueFields = [ }, typeOptions: { loadOptionsMethod: 'getIssueTypes', + loadOptionsDependsOn: [ + 'project', + ], }, description: 'Issue Types', }, @@ -139,36 +142,6 @@ export const issueFields = [ }, }, 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', @@ -188,6 +161,36 @@ export const issueFields = [ required : false, description: 'Description', }, + { + displayName: 'Labels', + name: 'labels', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getLabels', + }, + default: [], + required : false, + description: 'Labels', + }, + { + displayName: 'Parent Issue Key', + name: 'parentIssueKey', + type: 'string', + required: false, + default: '', + description: 'Parent Issue Key', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getPriorities', + }, + default: '', + required : false, + description: 'Priority', + }, { displayName: 'Update History', name: 'updateHistory', @@ -238,55 +241,6 @@ export const issueFields = [ }, }, 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', @@ -306,6 +260,66 @@ export const issueFields = [ required : false, description: 'Description', }, + { + displayName: 'Issue Type', + name: 'issueType', + type: 'options', + required: false, + typeOptions: { + loadOptionsMethod: 'getIssueTypes', + }, + default: '', + description: 'Issue Types', + }, + { + displayName: 'Labels', + name: 'labels', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getLabels', + }, + default: [], + required : false, + description: 'Labels', + }, + { + displayName: 'Parent Issue Key', + name: 'parentIssueKey', + type: 'string', + required: false, + default: '', + description: 'Parent Issue Key', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getPriorities', + }, + default: '', + required : false, + description: 'Priority', + }, + { + displayName: 'Summary', + name: 'summary', + type: 'string', + required: false, + default: '', + description: 'Summary', + }, + { + displayName: 'Status ID', + name: 'statusId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTransitions', + }, + required: false, + default: '', + description: 'The ID of the issue status.', + }, ], }, @@ -387,6 +401,23 @@ export const issueFields = [ }, }, options: [ + { + 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: 'Fields', name: 'fields', @@ -410,23 +441,6 @@ export const issueFields = [ 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', @@ -715,6 +729,17 @@ export const issueFields = [ }, }, options: [ + { + 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: 'Subject', name: 'subject', @@ -736,17 +761,6 @@ export const issueFields = [ 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.', - }, ], }, { diff --git a/packages/nodes-base/nodes/Jira/IssueInterface.ts b/packages/nodes-base/nodes/Jira/IssueInterface.ts index 59ba0ca1e7..fd7a948e29 100644 --- a/packages/nodes-base/nodes/Jira/IssueInterface.ts +++ b/packages/nodes-base/nodes/Jira/IssueInterface.ts @@ -1,18 +1,21 @@ -import { IDataObject } from "n8n-workflow"; +import { + IDataObject, + } from 'n8n-workflow'; export interface IFields { - summary?: string; - project?: IDataObject; - issuetype?: IDataObject; - labels?: string[]; - priority?: IDataObject; assignee?: IDataObject; description?: string; + issuetype?: IDataObject; + labels?: string[]; parent?: IDataObject; + priority?: IDataObject; + project?: IDataObject; + summary?: string; } export interface IIssue { fields?: IFields; + transition?: IDataObject; } export interface INotify { diff --git a/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts b/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts index b4e3cf0c2a..7335d24b6b 100644 --- a/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts +++ b/packages/nodes-base/nodes/Jira/JiraSoftwareCloud.node.ts @@ -1,28 +1,32 @@ import { IExecuteFunctions, } from 'n8n-core'; + import { IDataObject, - INodeTypeDescription, - INodeExecutionData, - INodeType, ILoadOptionsFunctions, + INodeExecutionData, INodePropertyOptions, + INodeType, + INodeTypeDescription, } from 'n8n-workflow'; + import { jiraSoftwareCloudApiRequest, jiraSoftwareCloudApiRequestAllItems, validateJSON, } from './GenericFunctions'; + import { issueOperations, issueFields, } from './IssueDescription'; + import { - IIssue, IFields, - INotify, + IIssue, INotificationRecipients, + INotify, NotificationRecipientsRestrictions, } from './IssueInterface'; @@ -37,7 +41,7 @@ export class JiraSoftwareCloud implements INodeType { description: 'Consume Jira Software API', defaults: { name: 'Jira Software', - color: '#c02428', + color: '#4185f7', }, inputs: ['main'], outputs: ['main'], @@ -108,16 +112,12 @@ export class JiraSoftwareCloud implements INodeType { async getProjects(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; const jiraCloudCredentials = this.getCredentials('jiraSoftwareCloudApi'); - let projects; let endpoint = '/project/search'; if (jiraCloudCredentials === undefined) { endpoint = '/project'; } - try { - projects = await jiraSoftwareCloudApiRequest.call(this, endpoint, 'GET'); - } catch (err) { - throw new Error(`Jira Error: ${err}`); - } + let projects = await jiraSoftwareCloudApiRequest.call(this, endpoint, 'GET'); + if (projects.values && Array.isArray(projects.values)) { projects = projects.values; } @@ -135,21 +135,21 @@ export class JiraSoftwareCloud implements INodeType { // Get all the issue types to display them to user so that he can // select them easily async getIssueTypes(this: ILoadOptionsFunctions): Promise { + const projectId = this.getCurrentNodeParameter('project'); 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, - }); + const issueTypes = await jiraSoftwareCloudApiRequest.call(this, '/issuetype', 'GET'); + + for (const issueType of issueTypes) { + if (issueType.scope.project.id === projectId) { + const issueTypeName = issueType.name; + const issueTypeId = issueType.id; + + returnData.push({ + name: issueTypeName, + value: issueTypeId, + }); + } } return returnData; }, @@ -158,12 +158,9 @@ export class JiraSoftwareCloud implements INodeType { // 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}`); - } + + const labels = await jiraSoftwareCloudApiRequest.call(this, '/label', 'GET'); + for (const label of labels.values) { const labelName = label; const labelId = label; @@ -180,12 +177,9 @@ export class JiraSoftwareCloud implements INodeType { // 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}`); - } + + const priorities = await jiraSoftwareCloudApiRequest.call(this, '/priority', 'GET'); + for (const priority of priorities) { const priorityName = priority.name; const priorityId = priority.id; @@ -202,12 +196,9 @@ export class JiraSoftwareCloud implements INodeType { // 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}`); - } + + const users = await jiraSoftwareCloudApiRequest.call(this, '/users/search', 'GET'); + for (const user of users) { const userName = user.displayName; const userId = user.accountId; @@ -224,12 +215,9 @@ export class JiraSoftwareCloud implements INodeType { // 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}`); - } + + const groups = await jiraSoftwareCloudApiRequest.call(this, '/groups/picker', 'GET'); + for (const group of groups.groups) { const groupName = group.name; const groupId = group.name; @@ -240,7 +228,24 @@ export class JiraSoftwareCloud implements INodeType { }); } return returnData; - } + }, + + // Get all the groups to display them to user so that he can + // select them easily + async getTransitions(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + const issueKey = this.getCurrentNodeParameter('issueKey'); + const transitions = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}/transitions`, 'GET'); + + for (const transition of transitions.transitions) { + returnData.push({ + name: transition.name, + value: transition.id, + }); + } + return returnData; + }, } }; @@ -309,11 +314,7 @@ export class JiraSoftwareCloud implements INodeType { }; } body.fields = fields; - try { - responseData = await jiraSoftwareCloudApiRequest.call(this, '/issue', 'POST', body); - } catch (err) { - throw new Error(`Jira Error: ${JSON.stringify(err)}`); - } + responseData = await jiraSoftwareCloudApiRequest.call(this, '/issue', 'POST', body); } //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-put if (operation === 'update') { @@ -363,11 +364,13 @@ export class JiraSoftwareCloud implements INodeType { }; } body.fields = fields; - try { - responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}`, 'PUT', body); - } catch (err) { - throw new Error(`Jira Error: ${JSON.stringify(err)}`); + + if (updateFields.statusId) { + responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}/transitions`, 'POST', { transition: { id: updateFields.statusId } }); } + + responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}`, 'PUT', body); + responseData = { success: true }; } //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-get if (operation === 'get') { @@ -388,11 +391,9 @@ export class JiraSoftwareCloud implements INodeType { 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)}`); - } + + responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}`, 'GET', {}, qs); + } //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-search-post if (operation === 'getAll') { @@ -421,16 +422,12 @@ export class JiraSoftwareCloud implements INodeType { 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)}`); + 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; } } //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-notify-post @@ -513,11 +510,8 @@ export class JiraSoftwareCloud implements INodeType { 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)}`); - } + responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}/notify`, 'POST', body, qs); + } //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-transitions-get if (operation === 'transitions') { @@ -532,23 +526,16 @@ export class JiraSoftwareCloud implements INodeType { 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)}`); - } + responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}/transitions`, 'GET', {}, qs); + responseData = responseData.transitions; + } //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)}`); - } + responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}`, 'DELETE', {}, qs); } } if (Array.isArray(responseData)) {