diff --git a/packages/nodes-base/nodes/Jira/GenericFunctions.ts b/packages/nodes-base/nodes/Jira/GenericFunctions.ts index 3d2198d34f..3ad028c680 100644 --- a/packages/nodes-base/nodes/Jira/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Jira/GenericFunctions.ts @@ -1,6 +1,6 @@ import { OptionsWithUri, - } from 'request'; +} from 'request'; import { IExecuteFunctions, @@ -14,7 +14,7 @@ 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 +export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any let data; let domain; const jiraVersion = this.getNodeParameter('jiraVersion', 0) as string; @@ -43,6 +43,7 @@ export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecut Authorization: `Basic ${data}`, Accept: 'application/json', 'Content-Type': 'application/json', + 'X-Atlassian-Token': 'no-check', }, method, qs: query, @@ -51,6 +52,18 @@ export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecut json: true, }; + if (Object.keys(option).length !== 0) { + Object.assign(options, option); + } + + if (Object.keys(body).length === 0) { + delete options.body; + } + + if (Object.keys(query || {}).length === 0) { + delete options.qs; + } + try { return await this.helpers.request!(options); } catch (error) { @@ -82,7 +95,7 @@ export async function jiraSoftwareCloudApiRequestAllItems(this: IHookFunctions | query.startAt = 0; body.startAt = 0; query.maxResults = 100; - body.maxResults = 100; + body.maxResults = 100; do { responseData = await jiraSoftwareCloudApiRequest.call(this, endpoint, method, body, query); @@ -106,7 +119,7 @@ export function validateJSON(json: string | undefined): any { // tslint:disable- return result; } -export function eventExists (currentEvents : string[], webhookEvents: string[]) { +export function eventExists(currentEvents: string[], webhookEvents: string[]) { for (const currentEvent of currentEvents) { if (!webhookEvents.includes(currentEvent)) { return false; @@ -115,7 +128,7 @@ export function eventExists (currentEvents : string[], webhookEvents: string[]) return true; } -export function getId (url: string) { +export function getId(url: string) { return url.split('/').pop(); } @@ -159,4 +172,4 @@ export const allEvents = [ 'worklog_created', 'worklog_updated', 'worklog_deleted', -]; +]; \ No newline at end of file diff --git a/packages/nodes-base/nodes/Jira/IssueAttachmentDescription.ts b/packages/nodes-base/nodes/Jira/IssueAttachmentDescription.ts new file mode 100644 index 0000000000..39ad30a1a4 --- /dev/null +++ b/packages/nodes-base/nodes/Jira/IssueAttachmentDescription.ts @@ -0,0 +1,266 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const issueAttachmentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add attachment to issue', + }, + { + name: 'Get', + value: 'get', + description: 'Get an attachment', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all attachments', + }, + { + name: 'Remove', + value: 'remove', + description: 'Remove an attachment', + }, + ], + default: 'add', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const issueAttachmentFields = [ + + /* -------------------------------------------------------------------------- */ + /* issueAttachment:add */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Issue Key', + name: 'issueKey', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'add', + ], + }, + }, + default: '', + description: 'Issue Key', + }, + { + displayName: 'Binary Property', + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'add', + ], + }, + }, + name: 'binaryPropertyName', + type: 'string', + default: 'data', + description: 'Object property name which holds binary data.', + required: true, + }, + + /* -------------------------------------------------------------------------- */ + /* issueAttachment:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Attachment ID', + name: 'attachmentId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'The ID of the attachment.', + }, + { + displayName: 'Download', + name: 'download', + type: 'boolean', + default: false, + required: true, + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + default: 'data', + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'get', + ], + download: [ + true, + ], + }, + }, + description: 'Object property name which holds binary data.', + required: true, + }, + /* -------------------------------------------------------------------------- */ + /* issueAttachment:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Issue Key', + name: 'issueKey', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'getAll', + ], + }, + }, + default: '', + description: 'Issue Key', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Download', + name: 'download', + type: 'boolean', + default: false, + required: true, + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + default: 'data', + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'getAll', + ], + download: [ + true, + ], + }, + }, + description: 'Object property name which holds binary data.', + required: true, + }, + /* -------------------------------------------------------------------------- */ + /* issueAttachment:remove */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Attachment ID', + name: 'attachmentId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'remove', + ], + }, + }, + default: '', + description: 'The ID of the attachment.', + }, +] as INodeProperties[]; \ No newline at end of file diff --git a/packages/nodes-base/nodes/Jira/IssueCommentDescription.ts b/packages/nodes-base/nodes/Jira/IssueCommentDescription.ts index 11d12a73a5..e4a491c861 100644 --- a/packages/nodes-base/nodes/Jira/IssueCommentDescription.ts +++ b/packages/nodes-base/nodes/Jira/IssueCommentDescription.ts @@ -485,4 +485,4 @@ export const issueCommentFields = [ }, ], }, -] as INodeProperties[]; +] as INodeProperties[]; \ No newline at end of file diff --git a/packages/nodes-base/nodes/Jira/IssueDescription.ts b/packages/nodes-base/nodes/Jira/IssueDescription.ts index 53e5c5224f..f67375477f 100644 --- a/packages/nodes-base/nodes/Jira/IssueDescription.ts +++ b/packages/nodes-base/nodes/Jira/IssueDescription.ts @@ -1,4 +1,4 @@ -import { +import { INodeProperties, } from 'n8n-workflow'; @@ -63,9 +63,9 @@ export const issueOperations = [ export const issueFields = [ -/* -------------------------------------------------------------------------- */ -/* issue:create */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* issue:create */ + /* -------------------------------------------------------------------------- */ { displayName: 'Project', name: 'project', @@ -155,7 +155,6 @@ export const issueFields = [ loadOptionsMethod: 'getUsers', }, default: '', - required : false, description: 'Assignee', }, { @@ -163,9 +162,46 @@ export const issueFields = [ name: 'description', type: 'string', default: '', - required : false, description: 'Description', }, + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Custom Field', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'customFieldsValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field ID', + name: 'fieldId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomFields', + loadOptionsDependsOn: [ + 'project', + ], + }, + description: 'ID of the field to set.', + default: '', + }, + { + displayName: 'Field Value', + name: 'fieldValue', + type: 'string', + description: 'Value of the field to set.', + default: '', + }, + ], + }, + ], + }, { displayName: 'Labels', name: 'labels', @@ -174,9 +210,8 @@ export const issueFields = [ loadOptionsMethod: 'getLabels', }, default: [], - required : false, description: 'Labels', - displayOptions: { + displayOptions: { show: { '/jiraVersion': [ 'cloud', @@ -189,9 +224,8 @@ export const issueFields = [ name: 'serverLabels', type: 'string', default: [], - required : false, description: 'Labels', - displayOptions: { + displayOptions: { show: { '/jiraVersion': [ 'server', @@ -206,7 +240,6 @@ export const issueFields = [ displayName: 'Parent Issue Key', name: 'parentIssueKey', type: 'string', - required: false, default: '', description: 'Parent Issue Key', }, @@ -218,7 +251,6 @@ export const issueFields = [ loadOptionsMethod: 'getPriorities', }, default: '', - required : false, description: 'Priority', }, { @@ -226,16 +258,15 @@ export const issueFields = [ 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 */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* issue:update */ + /* -------------------------------------------------------------------------- */ { displayName: 'Issue Key', name: 'issueKey', @@ -279,7 +310,6 @@ export const issueFields = [ loadOptionsMethod: 'getUsers', }, default: '', - required : false, description: 'Assignee', }, { @@ -287,14 +317,50 @@ export const issueFields = [ name: 'description', type: 'string', default: '', - required : false, description: 'Description', }, + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Custom Field', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'customFieldsValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field ID', + name: 'fieldId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomFields', + loadOptionsDependsOn: [ + 'issueKey', + ], + }, + description: 'ID of the field to set.', + default: '', + }, + { + displayName: 'Field Value', + name: 'fieldValue', + type: 'string', + description: 'Value of the field to set.', + default: '', + }, + ], + }, + ], + }, { displayName: 'Issue Type', name: 'issueType', type: 'options', - required: false, typeOptions: { loadOptionsMethod: 'getIssueTypes', }, @@ -309,9 +375,8 @@ export const issueFields = [ loadOptionsMethod: 'getLabels', }, default: [], - required : false, description: 'Labels', - displayOptions: { + displayOptions: { show: { '/jiraVersion': [ 'cloud', @@ -324,9 +389,8 @@ export const issueFields = [ name: 'serverLabels', type: 'string', default: [], - required : false, description: 'Labels', - displayOptions: { + displayOptions: { show: { '/jiraVersion': [ 'server', @@ -341,7 +405,6 @@ export const issueFields = [ displayName: 'Parent Issue Key', name: 'parentIssueKey', type: 'string', - required: false, default: '', description: 'Parent Issue Key', }, @@ -353,14 +416,12 @@ export const issueFields = [ loadOptionsMethod: 'getPriorities', }, default: '', - required : false, description: 'Priority', }, { displayName: 'Summary', name: 'summary', type: 'string', - required: false, default: '', description: 'Summary', }, @@ -371,16 +432,15 @@ export const issueFields = [ typeOptions: { loadOptionsMethod: 'getTransitions', }, - required: false, default: '', description: 'The ID of the issue status.', }, ], }, -/* -------------------------------------------------------------------------- */ -/* issue:delete */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* issue:delete */ + /* -------------------------------------------------------------------------- */ { displayName: 'Issue Key', name: 'issueKey', @@ -418,9 +478,9 @@ export const issueFields = [ description: 'Delete Subtasks', }, -/* -------------------------------------------------------------------------- */ -/* issue:get */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* issue:get */ + /* -------------------------------------------------------------------------- */ { displayName: 'Issue Key', name: 'issueKey', @@ -460,7 +520,6 @@ export const issueFields = [ 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:
@@ -477,7 +536,6 @@ export const issueFields = [ 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.
@@ -490,7 +548,6 @@ export const issueFields = [ 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
@@ -500,7 +557,6 @@ export const issueFields = [ 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:
@@ -516,7 +572,6 @@ export const issueFields = [ 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 @@ -525,9 +580,9 @@ export const issueFields = [ ], }, -/* -------------------------------------------------------------------------- */ -/* issue:getAll */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* issue:getAll */ + /* -------------------------------------------------------------------------- */ { displayName: 'Return All', name: 'returnAll', @@ -649,7 +704,6 @@ export const issueFields = [ 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
@@ -667,9 +721,9 @@ export const issueFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* issue:changelog */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* issue:changelog */ + /* -------------------------------------------------------------------------- */ { displayName: 'Issue Key', name: 'issueKey', @@ -729,9 +783,9 @@ export const issueFields = [ default: 50, description: 'How many results to return.', }, -/* -------------------------------------------------------------------------- */ -/* issue:notify */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* issue:notify */ + /* -------------------------------------------------------------------------- */ { displayName: 'Issue Key', name: 'issueKey', @@ -791,7 +845,6 @@ export const issueFields = [ typeOptions: { alwaysOpenEditWindow: true, }, - required: false, default: '', description: 'The HTML body of the email notification for the issue.', }, @@ -799,7 +852,6 @@ export const issueFields = [ 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.`, @@ -811,7 +863,6 @@ export const issueFields = [ 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.`, @@ -906,7 +957,6 @@ export const issueFields = [ typeOptions: { alwaysOpenEditWindow: true, }, - required: false, displayOptions: { show: { resource: [ @@ -983,7 +1033,6 @@ export const issueFields = [ typeOptions: { alwaysOpenEditWindow: true, }, - required: false, displayOptions: { show: { resource: [ @@ -1001,9 +1050,9 @@ export const issueFields = [ description: 'Restricts the notifications to users with the specified permissions.', }, -/* -------------------------------------------------------------------------- */ -/* issue:transitions */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* issue:transitions */ + /* -------------------------------------------------------------------------- */ { displayName: 'Issue Key', name: 'issueKey', @@ -1043,7 +1092,6 @@ export const issueFields = [ 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
@@ -1054,7 +1102,6 @@ export const issueFields = [ displayName: 'Transition ID', name: 'transitionId', type: 'string', - required: false, default: '', description: 'The ID of the transition.', }, @@ -1062,11 +1109,10 @@ export const issueFields = [ 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[]; +] as INodeProperties[]; \ No newline at end of file diff --git a/packages/nodes-base/nodes/Jira/IssueInterface.ts b/packages/nodes-base/nodes/Jira/IssueInterface.ts index fd7a948e29..a3a75f2d59 100644 --- a/packages/nodes-base/nodes/Jira/IssueInterface.ts +++ b/packages/nodes-base/nodes/Jira/IssueInterface.ts @@ -1,6 +1,6 @@ import { IDataObject, - } from 'n8n-workflow'; +} from 'n8n-workflow'; export interface IFields { assignee?: IDataObject; diff --git a/packages/nodes-base/nodes/Jira/Jira.node.ts b/packages/nodes-base/nodes/Jira/Jira.node.ts index da959be857..6a89019268 100644 --- a/packages/nodes-base/nodes/Jira/Jira.node.ts +++ b/packages/nodes-base/nodes/Jira/Jira.node.ts @@ -1,8 +1,11 @@ import { + BINARY_ENCODING, IExecuteFunctions, } from 'n8n-core'; import { + IBinaryData, + IBinaryKeyData, IDataObject, ILoadOptionsFunctions, INodeExecutionData, @@ -17,10 +20,15 @@ import { validateJSON, } from './GenericFunctions'; +import { + issueAttachmentFields, + issueAttachmentOperations, +} from './IssueAttachmentDescription'; + import { issueCommentFields, issueCommentOperations, - } from './IssueCommentDescription'; +} from './IssueCommentDescription'; import { issueFields, @@ -33,13 +41,13 @@ import { INotificationRecipients, INotify, NotificationRecipientsRestrictions, - } from './IssueInterface'; +} from './IssueInterface'; export class Jira implements INodeType { description: INodeTypeDescription = { displayName: 'Jira Software', name: 'jira', - icon: 'file:jira.png', + icon: 'file:jira.svg', group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', @@ -101,6 +109,11 @@ export class Jira implements INodeType { value: 'issue', description: 'Creates an issue or, where the option to create subtasks is enabled in Jira, a subtask', }, + { + name: 'Issue Attachment', + value: 'issueAttachment', + description: 'Add, remove, and get an attachment from an issue.', + }, { name: 'Issue Comment', value: 'issueComment', @@ -112,6 +125,8 @@ export class Jira implements INodeType { }, ...issueOperations, ...issueFields, + ...issueAttachmentOperations, + ...issueAttachmentFields, ...issueCommentOperations, ...issueCommentFields, ], @@ -176,7 +191,7 @@ export class Jira implements INodeType { } } else { for (const issueType of issueTypes) { - if (issueType.scope === undefined || issueType.scope.project.id === projectId) { + if (issueType.scope !== undefined && issueType.scope.project.id === projectId) { const issueTypeName = issueType.name; const issueTypeId = issueType.id; @@ -193,7 +208,6 @@ export class Jira implements INodeType { if (a.name > b.name) { return 1; } return 0; }); - return returnData; }, @@ -342,6 +356,32 @@ export class Jira implements INodeType { return returnData; }, + + // Get all the custom fields to display them to user so that he can + // select them easily + async getCustomFields(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const operation = this.getCurrentNodeParameter('operation') as string; + let projectId; + if (operation === 'create') { + projectId = this.getCurrentNodeParameter('project'); + } else { + const issueKey = this.getCurrentNodeParameter('issueKey'); + const { fields: { project: { id } } } = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}`, 'GET', {}, {}); + projectId = id; + } + + const fields = await jiraSoftwareCloudApiRequest.call(this, `/api/2/field`, 'GET'); + for (const field of fields) { + if (field.custom === true && field.scope && field.scope.project && field.scope.project.id === projectId) { + returnData.push({ + name: field.name, + value: field.id, + }); + } + } + return returnData; + }, }, }; @@ -356,11 +396,10 @@ export class Jira implements INodeType { const operation = this.getNodeParameter('operation', 0) as string; const jiraVersion = this.getNodeParameter('jiraVersion', 0) as string; - - for (let i = 0; i < length; i++) { - if (resource === 'issue') { - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-post - if (operation === 'create') { + if (resource === 'issue') { + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-post + if (operation === 'create') { + for (let i = 0; i < length; i++) { const summary = this.getNodeParameter('summary', i) as string; const projectId = this.getNodeParameter('project', i) as string; const issueTypeId = this.getNodeParameter('issueType', i) as string; @@ -403,6 +442,13 @@ export class Jira implements INodeType { if (additionalFields.updateHistory) { qs.updateHistory = additionalFields.updateHistory as boolean; } + if (additionalFields.customFieldsUi) { + const customFields = (additionalFields.customFieldsUi as IDataObject).customFieldsValues as IDataObject[]; + if (customFields) { + const data = customFields.reduce((obj, value) => Object.assign(obj, { [`${value.fieldId}`]: value.fieldValue }), {}); + Object.assign(fields, data); + } + } const issueTypes = await jiraSoftwareCloudApiRequest.call(this, '/api/2/issuetype', 'GET', body, qs); const subtaskIssues = []; for (const issueType of issueTypes) { @@ -422,9 +468,12 @@ export class Jira implements INodeType { } body.fields = fields; responseData = await jiraSoftwareCloudApiRequest.call(this, '/api/2/issue', 'POST', body); + returnData.push(responseData); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-put - if (operation === 'update') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-put + if (operation === 'update') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; const body: IIssue = {}; @@ -462,6 +511,13 @@ export class Jira implements INodeType { if (updateFields.description) { fields.description = updateFields.description as string; } + if (updateFields.customFieldsUi) { + const customFields = (updateFields.customFieldsUi as IDataObject).customFieldsValues as IDataObject[]; + if (customFields) { + const data = customFields.reduce((obj, value) => Object.assign(obj, { [`${value.fieldId}`]: value.fieldValue }), {}); + Object.assign(fields, data); + } + } const issueTypes = await jiraSoftwareCloudApiRequest.call(this, '/api/2/issuetype', 'GET', body); const subtaskIssues = []; for (const issueType of issueTypes) { @@ -486,10 +542,12 @@ export class Jira implements INodeType { } responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}`, 'PUT', body); - responseData = { success: true }; + returnData.push({ success: true }); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-get - if (operation === 'get') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-get + if (operation === 'get') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; if (additionalFields.fields) { @@ -507,12 +565,13 @@ export class Jira implements INodeType { if (additionalFields.updateHistory) { qs.updateHistory = additionalFields.updateHistory as string; } - responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}`, 'GET', {}, qs); - + returnData.push(responseData); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-search-post - if (operation === 'getAll') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-search-post + if (operation === 'getAll') { + for (let i = 0; i < length; i++) { const returnAll = this.getNodeParameter('returnAll', i) as boolean; const options = this.getNodeParameter('options', i) as IDataObject; const body: IDataObject = {}; @@ -533,21 +592,27 @@ export class Jira implements INodeType { responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/search`, 'POST', body); responseData = responseData.issues; } + returnData.push.apply(returnData, responseData); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-changelog-get - if (operation === 'changelog') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-changelog-get + if (operation === 'changelog') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const returnAll = this.getNodeParameter('returnAll', i) as boolean; if (returnAll) { - responseData = await jiraSoftwareCloudApiRequestAllItems.call(this, 'values',`/api/2/issue/${issueKey}/changelog`, 'GET'); + responseData = await jiraSoftwareCloudApiRequestAllItems.call(this, 'values', `/api/2/issue/${issueKey}/changelog`, 'GET'); } else { qs.maxResults = this.getNodeParameter('limit', i) as number; responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}/changelog`, 'GET', {}, qs); responseData = responseData.values; } + returnData.push.apply(returnData, responseData); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-notify-post - if (operation === 'notify') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-notify-post + if (operation === 'notify') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; const jsonActive = this.getNodeParameter('jsonParameters', 0) as boolean; @@ -606,7 +671,7 @@ export class Jira implements INodeType { const notificationRecipientsRestrictions: NotificationRecipientsRestrictions = {}; if (notificationRecipientsRestrictionsValues) { // @ts-ignore - if (notificationRecipientsRestrictionsValues.groups. length > 0) { + if (notificationRecipientsRestrictionsValues.groups.length > 0) { // @ts-ignore notificationRecipientsRestrictions.groups = notificationRecipientsRestrictionsValues.groups.map(group => { return { @@ -627,10 +692,12 @@ export class Jira implements INodeType { } } responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}/notify`, 'POST', body, qs); - + returnData.push(responseData); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-transitions-get - if (operation === 'transitions') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-transitions-get + if (operation === 'transitions') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; if (additionalFields.transitionId) { @@ -644,19 +711,118 @@ export class Jira implements INodeType { } responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}/transitions`, 'GET', {}, qs); responseData = responseData.transitions; - + returnData.push.apply(returnData, responseData); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-delete - if (operation === 'delete') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-delete + if (operation === 'delete') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const deleteSubtasks = this.getNodeParameter('deleteSubtasks', i) as boolean; qs.deleteSubtasks = deleteSubtasks; responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}`, 'DELETE', {}, qs); + returnData.push({ success: true }); } } - if (resource === 'issueComment') { - //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-post - if (operation === 'add') { + } + if (resource === 'issueAttachment') { + //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-attachments/#api-rest-api-3-issue-issueidorkey-attachments-post + if (operation === 'add') { + for (let i = 0; i < length; i++) { + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + const issueKey = this.getNodeParameter('issueKey', i) as string; + + if (items[i].binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + const item = items[i].binary as IBinaryKeyData; + + const binaryData = item[binaryPropertyName] as IBinaryData; + + if (binaryData === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + responseData = await jiraSoftwareCloudApiRequest.call( + this, + `/api/3/issue/${issueKey}/attachments`, + 'POST', + {}, + {}, + undefined, + { + formData: { + file: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + }, + }, + }, + }, + ); + returnData.push.apply(returnData, responseData); + } + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-attachments/#api-rest-api-3-attachment-id-delete + if (operation === 'remove') { + for (let i = 0; i < length; i++) { + const attachmentId = this.getNodeParameter('attachmentId', i) as string; + responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/attachment/${attachmentId}`, 'DELETE', {}, qs); + returnData.push({ success: true }); + } + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-attachments/#api-rest-api-3-attachment-id-get + if (operation === 'get') { + const download = this.getNodeParameter('download', 0) as boolean; + for (let i = 0; i < length; i++) { + const attachmentId = this.getNodeParameter('attachmentId', i) as string; + responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/attachment/${attachmentId}`, 'GET', {}, qs); + returnData.push({ json: responseData }); + } + if (download) { + const binaryPropertyName = this.getNodeParameter('binaryProperty', 0) as string; + for (const [index, attachment] of returnData.entries()) { + returnData[index]['binary'] = {}; + //@ts-ignore + const buffer = await jiraSoftwareCloudApiRequest.call(this, '', 'GET', {}, {}, attachment?.json!.content, { json: false, encoding: null }); + //@ts-ignore + returnData[index]['binary'][binaryPropertyName] = await this.helpers.prepareBinaryData(buffer, attachment.json.filename, attachment.json.mimeType); + } + } + } + if (operation === 'getAll') { + const download = this.getNodeParameter('download', 0) as boolean; + for (let i = 0; i < length; i++) { + const issueKey = this.getNodeParameter('issueKey', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const { fields: { attachment } } = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}`, 'GET', {}, qs); + responseData = attachment; + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.slice(0, limit); + } + responseData = responseData.map((data: IDataObject) => ({ json: data })); + returnData.push.apply(returnData, responseData); + } + if (download) { + const binaryPropertyName = this.getNodeParameter('binaryProperty', 0) as string; + for (const [index, attachment] of returnData.entries()) { + returnData[index]['binary'] = {}; + //@ts-ignore + const buffer = await jiraSoftwareCloudApiRequest.call(this, '', 'GET', {}, {}, attachment.json.content, { json: false, encoding: null }); + //@ts-ignore + returnData[index]['binary'][binaryPropertyName] = await this.helpers.prepareBinaryData(buffer, attachment.json.filename, attachment.json.mimeType); + } + } + } + } + + if (resource === 'issueComment') { + //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-post + if (operation === 'add') { + for (let i = 0; i < length; i++) { const jsonParameters = this.getNodeParameter('jsonParameters', 0) as boolean; const issueKey = this.getNodeParameter('issueKey', i) as string; const options = this.getNodeParameter('options', i) as IDataObject; @@ -697,18 +863,23 @@ export class Jira implements INodeType { } responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/issue/${issueKey}/comment`, 'POST', body, qs); + returnData.push(responseData); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-get - if (operation === 'get') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-get + if (operation === 'get') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const commentId = this.getNodeParameter('commentId', i) as string; const options = this.getNodeParameter('options', i) as IDataObject; Object.assign(qs, options); responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/issue/${issueKey}/comment/${commentId}`, 'GET', {}, qs); - + returnData.push(responseData); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-get - if (operation === 'getAll') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-get + if (operation === 'getAll') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const returnAll = this.getNodeParameter('returnAll', i) as boolean; const options = this.getNodeParameter('options', i) as IDataObject; @@ -722,16 +893,21 @@ export class Jira implements INodeType { responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/issue/${issueKey}/comment`, 'GET', body, qs); responseData = responseData.comments; } + returnData.push.apply(returnData, responseData); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-id-delete - if (operation === 'remove') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-id-delete + if (operation === 'remove') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const commentId = this.getNodeParameter('commentId', i) as string; responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/issue/${issueKey}/comment/${commentId}`, 'DELETE', {}, qs); - responseData = { success: true }; + returnData.push({ success: true }); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-id-put - if (operation === 'update') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-id-put + if (operation === 'update') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const commentId = this.getNodeParameter('commentId', i) as string; const options = this.getNodeParameter('options', i) as IDataObject; @@ -771,14 +947,15 @@ export class Jira implements INodeType { Object.assign(body, { body: json }); } responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/issue/${issueKey}/comment/${commentId}`, 'PUT', body, qs); + returnData.push(responseData); } } - if (Array.isArray(responseData)) { - returnData.push.apply(returnData, responseData as IDataObject[]); - } else { - returnData.push(responseData as IDataObject); - } } - return [this.helpers.returnJsonArray(returnData)]; + + if (resource === 'issueAttachment' && (operation === 'getAll' || operation === 'get')) { + return this.prepareOutputData(returnData as unknown as INodeExecutionData[]); + } else { + return [this.helpers.returnJsonArray(returnData)]; + } } -} +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Jira/JiraTrigger.node.ts b/packages/nodes-base/nodes/Jira/JiraTrigger.node.ts index 72923f9ae8..68ada4ecf0 100644 --- a/packages/nodes-base/nodes/Jira/JiraTrigger.node.ts +++ b/packages/nodes-base/nodes/Jira/JiraTrigger.node.ts @@ -23,7 +23,7 @@ export class JiraTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'Jira Trigger', name: 'jiraTrigger', - icon: 'file:jira.png', + icon: 'file:jira.svg', group: ['trigger'], version: 1, description: 'Starts the workflow when Jira events occurs.', diff --git a/packages/nodes-base/nodes/Jira/jira.png b/packages/nodes-base/nodes/Jira/jira.png deleted file mode 100644 index 977265189f..0000000000 Binary files a/packages/nodes-base/nodes/Jira/jira.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Jira/jira.svg b/packages/nodes-base/nodes/Jira/jira.svg new file mode 100644 index 0000000000..c1ee7fe198 --- /dev/null +++ b/packages/nodes-base/nodes/Jira/jira.svg @@ -0,0 +1 @@ + \ No newline at end of file