🔀 Merge branch 'feature/jira' of https://github.com/RicardoE105/n8n into RicardoE105-feature/jira

This commit is contained in:
Jan Oberhauser 2020-04-24 08:39:08 +02:00
commit 57d296d30a
4 changed files with 194 additions and 202 deletions

View file

@ -1,4 +1,6 @@
import { OptionsWithUri } from 'request'; import {
OptionsWithUri,
} from 'request';
import { import {
IExecuteFunctions, IExecuteFunctions,
@ -41,11 +43,11 @@ export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecut
try { try {
return await this.helpers.request!(options); return await this.helpers.request!(options);
} catch (error) { } catch (error) {
let errorMessage = error; const errorMessage = error.response.body.message || error.response.body.error || error.response.body.errors;
if (error.error && error.error.errorMessages) { if (errorMessage !== undefined) {
errorMessage = error.error.errorMessages; throw new Error(errorMessage);
} }
throw new Error(errorMessage); throw error;
} }
} }

View file

@ -44,7 +44,7 @@ export const issueOperations = [
description: 'Creates an email notification for an issue and adds it to the mail queue.', description: 'Creates an email notification for an issue and adds it to the mail queue.',
}, },
{ {
name: 'Transitions', name: 'Status',
value: '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.`, 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: { typeOptions: {
loadOptionsMethod: 'getIssueTypes', loadOptionsMethod: 'getIssueTypes',
loadOptionsDependsOn: [
'project',
],
}, },
description: 'Issue Types', description: 'Issue Types',
}, },
@ -139,36 +142,6 @@ export const issueFields = [
}, },
}, },
options: [ 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', displayName: 'Assignee',
name: 'assignee', name: 'assignee',
@ -188,6 +161,36 @@ export const issueFields = [
required : false, required : false,
description: 'Description', 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', displayName: 'Update History',
name: 'updateHistory', name: 'updateHistory',
@ -238,55 +241,6 @@ export const issueFields = [
}, },
}, },
options: [ 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', displayName: 'Assignee',
name: 'assignee', name: 'assignee',
@ -306,6 +260,63 @@ export const issueFields = [
required : false, required : false,
description: 'Description', 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: 'string',
required: false,
default: '',
description: 'The ID of the issue status.',
},
], ],
}, },
@ -387,6 +398,23 @@ export const issueFields = [
}, },
}, },
options: [ options: [
{
displayName: 'Expand',
name: 'expand',
type: 'string',
required: false,
default: '',
description: `Use expand to include additional information about the issues in the response.<br/>
This parameter accepts a comma-separated list. Expand options include:<br/>
renderedFields Returns field values rendered in HTML format.<br/>
names Returns the display name of each field.<br/>
schema Returns the schema describing a field type.<br/>
transitions Returns all possible transitions for the issue.<br/>
editmeta Returns information about how each field can be edited.<br/>
changelog Returns a list of recent updates to an issue, sorted by date, starting from the most recent.<br/>
versionedRepresentations Returns a JSON array for each version of a field's value, with the highest number<br/>
representing the most recent version. Note: When included in the request, the fields parameter is ignored.`
},
{ {
displayName: 'Fields', displayName: 'Fields',
name: 'fields', name: 'fields',
@ -410,23 +438,6 @@ export const issueFields = [
This parameter is useful where fields have been added by a connect app and a field's key<br/> This parameter is useful where fields have been added by a connect app and a field's key<br/>
may differ from its ID.`, 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.<br/>
This parameter accepts a comma-separated list. Expand options include:<br/>
renderedFields Returns field values rendered in HTML format.<br/>
names Returns the display name of each field.<br/>
schema Returns the schema describing a field type.<br/>
transitions Returns all possible transitions for the issue.<br/>
editmeta Returns information about how each field can be edited.<br/>
changelog Returns a list of recent updates to an issue, sorted by date, starting from the most recent.<br/>
versionedRepresentations Returns a JSON array for each version of a field's value, with the highest number<br/>
representing the most recent version. Note: When included in the request, the fields parameter is ignored.`
},
{ {
displayName: 'Properties', displayName: 'Properties',
name: 'properties', name: 'properties',
@ -715,6 +726,17 @@ export const issueFields = [
}, },
}, },
options: [ 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', displayName: 'Subject',
name: 'subject', name: 'subject',
@ -736,17 +758,6 @@ export const issueFields = [
description: `The subject of the email notification for the issue. 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.` 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.',
},
], ],
}, },
{ {

View file

@ -1,18 +1,21 @@
import { IDataObject } from "n8n-workflow"; import {
IDataObject,
} from 'n8n-workflow';
export interface IFields { export interface IFields {
summary?: string;
project?: IDataObject;
issuetype?: IDataObject;
labels?: string[];
priority?: IDataObject;
assignee?: IDataObject; assignee?: IDataObject;
description?: string; description?: string;
issuetype?: IDataObject;
labels?: string[];
parent?: IDataObject; parent?: IDataObject;
priority?: IDataObject;
project?: IDataObject;
summary?: string;
} }
export interface IIssue { export interface IIssue {
fields?: IFields; fields?: IFields;
transition?: IDataObject;
} }
export interface INotify { export interface INotify {

View file

@ -1,28 +1,32 @@
import { import {
IExecuteFunctions, IExecuteFunctions,
} from 'n8n-core'; } from 'n8n-core';
import { import {
IDataObject, IDataObject,
INodeTypeDescription,
INodeExecutionData,
INodeType,
ILoadOptionsFunctions, ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions, INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
jiraSoftwareCloudApiRequest, jiraSoftwareCloudApiRequest,
jiraSoftwareCloudApiRequestAllItems, jiraSoftwareCloudApiRequestAllItems,
validateJSON, validateJSON,
} from './GenericFunctions'; } from './GenericFunctions';
import { import {
issueOperations, issueOperations,
issueFields, issueFields,
} from './IssueDescription'; } from './IssueDescription';
import { import {
IIssue,
IFields, IFields,
INotify, IIssue,
INotificationRecipients, INotificationRecipients,
INotify,
NotificationRecipientsRestrictions, NotificationRecipientsRestrictions,
} from './IssueInterface'; } from './IssueInterface';
@ -37,7 +41,7 @@ export class JiraSoftwareCloud implements INodeType {
description: 'Consume Jira Software API', description: 'Consume Jira Software API',
defaults: { defaults: {
name: 'Jira Software', name: 'Jira Software',
color: '#c02428', color: '#4185f7',
}, },
inputs: ['main'], inputs: ['main'],
outputs: ['main'], outputs: ['main'],
@ -113,11 +117,8 @@ export class JiraSoftwareCloud implements INodeType {
if (jiraCloudCredentials === undefined) { if (jiraCloudCredentials === undefined) {
endpoint = '/project'; endpoint = '/project';
} }
try { projects = await jiraSoftwareCloudApiRequest.call(this, endpoint, 'GET');
projects = await jiraSoftwareCloudApiRequest.call(this, endpoint, 'GET');
} catch (err) {
throw new Error(`Jira Error: ${err}`);
}
if (projects.values && Array.isArray(projects.values)) { if (projects.values && Array.isArray(projects.values)) {
projects = projects.values; projects = projects.values;
} }
@ -135,21 +136,22 @@ export class JiraSoftwareCloud implements INodeType {
// Get all the issue types to display them to user so that he can // Get all the issue types to display them to user so that he can
// select them easily // select them easily
async getIssueTypes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { async getIssueTypes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const projectId = this.getCurrentNodeParameter('project');
const returnData: INodePropertyOptions[] = []; const returnData: INodePropertyOptions[] = [];
let issueTypes; 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({ issueTypes = await jiraSoftwareCloudApiRequest.call(this, '/issuetype', 'GET');
name: issueTypeName,
value: issueTypeId, 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; return returnData;
}, },
@ -159,11 +161,9 @@ export class JiraSoftwareCloud implements INodeType {
async getLabels(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { async getLabels(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = []; const returnData: INodePropertyOptions[] = [];
let labels; let labels;
try {
labels = await jiraSoftwareCloudApiRequest.call(this, '/label', 'GET'); labels = await jiraSoftwareCloudApiRequest.call(this, '/label', 'GET');
} catch (err) {
throw new Error(`Jira Error: ${err}`);
}
for (const label of labels.values) { for (const label of labels.values) {
const labelName = label; const labelName = label;
const labelId = label; const labelId = label;
@ -181,11 +181,9 @@ export class JiraSoftwareCloud implements INodeType {
async getPriorities(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { async getPriorities(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = []; const returnData: INodePropertyOptions[] = [];
let priorities; let priorities;
try {
priorities = await jiraSoftwareCloudApiRequest.call(this, '/priority', 'GET'); priorities = await jiraSoftwareCloudApiRequest.call(this, '/priority', 'GET');
} catch (err) {
throw new Error(`Jira Error: ${err}`);
}
for (const priority of priorities) { for (const priority of priorities) {
const priorityName = priority.name; const priorityName = priority.name;
const priorityId = priority.id; const priorityId = priority.id;
@ -203,11 +201,9 @@ export class JiraSoftwareCloud implements INodeType {
async getUsers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { async getUsers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = []; const returnData: INodePropertyOptions[] = [];
let users; let users;
try {
users = await jiraSoftwareCloudApiRequest.call(this, '/users/search', 'GET'); users = await jiraSoftwareCloudApiRequest.call(this, '/users/search', 'GET');
} catch (err) {
throw new Error(`Jira Error: ${err}`);
}
for (const user of users) { for (const user of users) {
const userName = user.displayName; const userName = user.displayName;
const userId = user.accountId; const userId = user.accountId;
@ -225,11 +221,9 @@ export class JiraSoftwareCloud implements INodeType {
async getGroups(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { async getGroups(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = []; const returnData: INodePropertyOptions[] = [];
let groups; let groups;
try {
groups = await jiraSoftwareCloudApiRequest.call(this, '/groups/picker', 'GET'); groups = await jiraSoftwareCloudApiRequest.call(this, '/groups/picker', 'GET');
} catch (err) {
throw new Error(`Jira Error: ${err}`);
}
for (const group of groups.groups) { for (const group of groups.groups) {
const groupName = group.name; const groupName = group.name;
const groupId = group.name; const groupId = group.name;
@ -309,11 +303,7 @@ export class JiraSoftwareCloud implements INodeType {
}; };
} }
body.fields = fields; body.fields = fields;
try { responseData = await jiraSoftwareCloudApiRequest.call(this, '/issue', 'POST', body);
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 //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-put
if (operation === 'update') { if (operation === 'update') {
@ -363,11 +353,13 @@ export class JiraSoftwareCloud implements INodeType {
}; };
} }
body.fields = fields; body.fields = fields;
try {
responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}`, 'PUT', body); if (updateFields.statusId) {
} catch (err) { responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}/transitions`, 'POST', { transition: { id: updateFields.statusId } });
throw new Error(`Jira Error: ${JSON.stringify(err)}`);
} }
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 //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-get
if (operation === 'get') { if (operation === 'get') {
@ -388,11 +380,9 @@ export class JiraSoftwareCloud implements INodeType {
if (additionalFields.updateHistory) { if (additionalFields.updateHistory) {
qs.updateHistory = additionalFields.updateHistory as string; qs.updateHistory = additionalFields.updateHistory as string;
} }
try {
responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}`, 'GET', {}, qs); 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-search-post //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-search-post
if (operation === 'getAll') { if (operation === 'getAll') {
@ -421,16 +411,12 @@ export class JiraSoftwareCloud implements INodeType {
if (operation === 'changelog') { if (operation === 'changelog') {
const issueKey = this.getNodeParameter('issueKey', i) as string; const issueKey = this.getNodeParameter('issueKey', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean; const returnAll = this.getNodeParameter('returnAll', i) as boolean;
try { if (returnAll) {
if (returnAll) { responseData = await jiraSoftwareCloudApiRequestAllItems.call(this, 'values',`/issue/${issueKey}/changelog`, 'GET');
responseData = await jiraSoftwareCloudApiRequestAllItems.call(this, 'values',`/issue/${issueKey}/changelog`, 'GET'); } else {
} else { qs.maxResults = this.getNodeParameter('limit', i) as number;
qs.maxResults = this.getNodeParameter('limit', i) as number; responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}/changelog`, 'GET', {}, qs);
responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}/changelog`, 'GET', {}, qs); responseData = responseData.values;
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 //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-notify-post
@ -513,11 +499,8 @@ export class JiraSoftwareCloud implements INodeType {
body.restrict = notificationRecipientsRestrictionsJson; body.restrict = notificationRecipientsRestrictionsJson;
} }
} }
try { responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}/notify`, 'POST', body, qs);
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 //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-transitions-get
if (operation === 'transitions') { if (operation === 'transitions') {
@ -532,23 +515,16 @@ export class JiraSoftwareCloud implements INodeType {
if (additionalFields.skipRemoteOnlyCondition) { if (additionalFields.skipRemoteOnlyCondition) {
qs.skipRemoteOnlyCondition = additionalFields.skipRemoteOnlyCondition as boolean; qs.skipRemoteOnlyCondition = additionalFields.skipRemoteOnlyCondition as boolean;
} }
try { responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}/transitions`, 'GET', {}, qs);
responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}/transitions`, 'GET', {}, qs); responseData = responseData.transitions;
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 //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-delete
if (operation === 'delete') { if (operation === 'delete') {
const issueKey = this.getNodeParameter('issueKey', i) as string; const issueKey = this.getNodeParameter('issueKey', i) as string;
const deleteSubtasks = this.getNodeParameter('deleteSubtasks', i) as boolean; const deleteSubtasks = this.getNodeParameter('deleteSubtasks', i) as boolean;
qs.deleteSubtasks = deleteSubtasks; qs.deleteSubtasks = deleteSubtasks;
try { responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}`, 'DELETE', {}, qs);
responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}`, 'DELETE', {}, qs);
} catch (err) {
throw new Error(`Jira Error: ${JSON.stringify(err)}`);
}
} }
} }
if (Array.isArray(responseData)) { if (Array.isArray(responseData)) {