From da7d43b037301f2d6732b47418cd03212359cedb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Wed, 6 Dec 2023 17:50:26 +0100 Subject: [PATCH 01/12] Add first comment implementation --- .../EpicDetailView/EpicDetailView.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/components/EpicDetailView/EpicDetailView.tsx b/src/components/EpicDetailView/EpicDetailView.tsx index 3e9ef189..3dca23f5 100644 --- a/src/components/EpicDetailView/EpicDetailView.tsx +++ b/src/components/EpicDetailView/EpicDetailView.tsx @@ -19,6 +19,7 @@ import { ReporterMenu } from "../DetailView/Components/ReporterMenu" import { DeleteIssue } from "../DetailView/Components/DeleteIssue" import { ColorSchemeToggle } from "../common/ColorSchemeToggle" import { IssueIcon } from "../BacklogView/Issue/IssueIcon" +import {CommentSection} from "../DetailView/Components/CommentSection"; export function EpicDetailView({ issueKey, @@ -28,6 +29,7 @@ export function EpicDetailView({ description, created, updated, + comment, closeModal, }: { issueKey: string @@ -37,6 +39,17 @@ export function EpicDetailView({ description: string created: string updated: string + comment: { + comments: [ + { + id: string + author: User + body: string + created: string + updated: string + } + ] + } closeModal: () => void }) { const queryClient = useQueryClient() @@ -114,6 +127,16 @@ export function EpicDetailView({ + + + + Comments + + + + + + Created{" "} {dateFormat.format(new Date(created))} From fdd357544a3d40cdfd2441925cad17195c33003e Mon Sep 17 00:00:00 2001 From: ayman <118556179+aymka@users.noreply.github.com> Date: Mon, 11 Dec 2023 18:00:22 +0100 Subject: [PATCH 02/12] commentSection implemented - edit/delete/add may take long(needs fixing) --- .../jira-cloud-provider/JiraCloudProvider.ts | 701 +++++++++------ .../JiraServerProvider.ts | 817 +++++++++++------- types/jira.ts | 74 +- 3 files changed, 982 insertions(+), 610 deletions(-) diff --git a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts index e837ec4a..638a6dfc 100644 --- a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts +++ b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts @@ -1,5 +1,5 @@ /* eslint-disable class-methods-use-this */ -import axios, { AxiosError, AxiosResponse, isAxiosError } from "axios"; +import axios, { AxiosError, AxiosResponse, isAxiosError } from "axios" import { dateTimeFormat, Issue, @@ -12,6 +12,7 @@ import { User, } from "../../../types" import { + JiraEpic, JiraIssue, JiraIssueType, JiraPriority, @@ -20,7 +21,7 @@ import { } from "../../../types/jira" import { IProvider } from "../base-provider" import { getAccessToken, refreshTokens } from "./getAccessToken" -import {JiraCloudUser} from "./cloud-types"; +import { JiraCloudUser } from "./cloud-types" export class JiraCloudProvider implements IProvider { public accessToken: string | undefined @@ -43,13 +44,14 @@ export class JiraCloudProvider implements IProvider { }, }) - const recreateAxiosError = (originalError: AxiosError, message: string) => new AxiosError( - message, - originalError.code, - originalError.config, - originalError.request, - originalError.response - ) + const recreateAxiosError = (originalError: AxiosError, message: string) => + new AxiosError( + message, + originalError.code, + originalError.config, + originalError.request, + originalError.response + ) instance.interceptors.response.use( (response) => response, @@ -57,13 +59,38 @@ export class JiraCloudProvider implements IProvider { if (isAxiosError(error) && error.response) { const statusCode = error.response.status if (statusCode === 400) { - return Promise.reject(recreateAxiosError(error, `Invalid request: ${JSON.stringify(error.response.data)}`)) - } if (statusCode === 401) { - return Promise.reject(recreateAxiosError(error, `User not authenticated: ${JSON.stringify(error.response.data)}`)) - } if (error.response.status === 403) { - return Promise.reject(recreateAxiosError(error, `User does not have a valid licence: ${JSON.stringify(error.response.data)}`)) - } if (error.response.status === 429) { - return Promise.reject(recreateAxiosError(error, `Rate limit exceeded: ${JSON.stringify(error.response.data)}`)) + return Promise.reject( + recreateAxiosError( + error, + `Invalid request: ${JSON.stringify(error.response.data)}` + ) + ) + } + if (statusCode === 401) { + return Promise.reject( + recreateAxiosError( + error, + `User not authenticated: ${JSON.stringify(error.response.data)}` + ) + ) + } + if (error.response.status === 403) { + return Promise.reject( + recreateAxiosError( + error, + `User does not have a valid licence: ${JSON.stringify( + error.response.data + )}` + ) + ) + } + if (error.response.status === 429) { + return Promise.reject( + recreateAxiosError( + error, + `Rate limit exceeded: ${JSON.stringify(error.response.data)}` + ) + ) } } @@ -75,11 +102,11 @@ export class JiraCloudProvider implements IProvider { } private getRestApiClient(version: number) { - return this.constructRestBasedClient('api', version.toString()); + return this.constructRestBasedClient("api", version.toString()) } private getAgileRestApiClient(version: string) { - return this.constructRestBasedClient('agile', version); + return this.constructRestBasedClient("agile", version) } offsetDate(date: Date) { @@ -168,7 +195,7 @@ export class JiraCloudProvider implements IProvider { async mapCustomFields(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .get('/field') + .get("/field") .then(async (response) => { response.data.forEach((field: { name: string; id: string }) => { this.customFields.set(field.name, field.id) @@ -185,7 +212,9 @@ export class JiraCloudProvider implements IProvider { async getProjects(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .get('/project/search?expand=description,lead,issueTypes,url,projectKeys,permissions,insight') + .get( + "/project/search?expand=description,lead,issueTypes,url,projectKeys,permissions,insight" + ) .then(async (response) => { const projects = response.data.values.map((project: JiraProject) => ({ key: project.key, @@ -200,7 +229,9 @@ export class JiraCloudProvider implements IProvider { let specificError = error if (error.response) { if (error.response.status === 404) { - specificError = new Error(`No projects matching the search criteria were found: ${error.response.data}`) + specificError = new Error( + `No projects matching the search criteria were found: ${error.response.data}` + ) } } @@ -227,7 +258,9 @@ export class JiraCloudProvider implements IProvider { } } - reject(new Error(`Error in fetching the issue types: ${specificError}`)) + reject( + new Error(`Error in fetching the issue types: ${specificError}`) + ) }) }) } @@ -235,7 +268,7 @@ export class JiraCloudProvider implements IProvider { async getIssueTypesWithFieldsMap(): Promise<{ [key: string]: string[] }> { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .get('/issue/createmeta?expand=projects.issuetypes.fields') + .get("/issue/createmeta?expand=projects.issuetypes.fields") .then(async (response) => { const issueTypeToFieldsMap: { [key: string]: string[] } = {} response.data.projects.forEach( @@ -283,13 +316,16 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get(`/user/assignable/search?project=${projectIdOrKey}`) .then(async (response) => { - const users = response.data.map((cloudUser: JiraCloudUser) => ({ - id: cloudUser.accountId, - name: cloudUser.name, - avatarUrls: cloudUser.avatarUrls, - displayName: cloudUser.displayName, - emailAddress: cloudUser.emailAddress, - } as User)) + const users = response.data.map( + (cloudUser: JiraCloudUser) => + ({ + id: cloudUser.accountId, + name: cloudUser.name, + avatarUrls: cloudUser.avatarUrls, + displayName: cloudUser.displayName, + emailAddress: cloudUser.emailAddress, + } as User) + ) resolve(users as User[]) }) @@ -297,11 +333,17 @@ export class JiraCloudProvider implements IProvider { let specificError = error if (error.response) { if (error.response.status === 404) { - specificError = new Error(`Project, issue, or transition were not found: ${error.response.data}`) + specificError = new Error( + `Project, issue, or transition were not found: ${error.response.data}` + ) } } - reject(new Error(`Error in fetching the assignable users for the project ${projectIdOrKey}: ${specificError}`)) + reject( + new Error( + `Error in fetching the assignable users for the project ${projectIdOrKey}: ${specificError}` + ) + ) }) }) } @@ -309,7 +351,7 @@ export class JiraCloudProvider implements IProvider { async getCurrentUser(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .get('/myself') + .get("/myself") .then(async (response: AxiosResponse) => { resolve({ id: response.data.accountId, @@ -342,14 +384,16 @@ export class JiraCloudProvider implements IProvider { } } - reject(new Error(`Error in fetching the issue reporter: ${specificError}`)) + reject( + new Error(`Error in fetching the issue reporter: ${specificError}`) + ) }) }) } async getBoardIds(project: string): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient('1.0') + this.getAgileRestApiClient("1.0") .get(`/board?projectKeyOrId=${project}`) .then(async (response) => { const boardIds: number[] = response.data.values.map( @@ -365,13 +409,11 @@ export class JiraCloudProvider implements IProvider { async getSprints(boardId: number): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient('1.0') + this.getAgileRestApiClient("1.0") .get(`/board/${boardId}/sprint`) .then(async (response) => { const sprints: Sprint[] = response.data.values - .filter( - (element: { state: string }) => element.state !== "closed" - ) + .filter((element: { state: string }) => element.state !== "closed") .map((element: JiraSprint) => { const sDate = new Date(element.startDate) const startDate = Number.isNaN(sDate.getTime()) @@ -414,20 +456,32 @@ export class JiraCloudProvider implements IProvider { resolve(this.fetchIssues(response)) }) .catch((error) => { - reject(new Error(`Error fetching issues by project: ${this.handleFetchIssuesError(error)}`)) + reject( + new Error( + `Error fetching issues by project: ${this.handleFetchIssuesError( + error + )}` + ) + ) }) }) } async getIssuesBySprint(sprintId: number): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient('1.0') + this.getAgileRestApiClient("1.0") .get(`/sprint/${sprintId}/issue`) .then(async (response) => { resolve(this.fetchIssues(response)) }) .catch((error) => { - reject(new Error(`Error fetching issues by sprint: ${this.handleFetchIssuesError(error)}`)) + reject( + new Error( + `Error fetching issues by sprint: ${this.handleFetchIssuesError( + error + )}` + ) + ) }) }) } @@ -437,13 +491,19 @@ export class JiraCloudProvider implements IProvider { boardId: number ): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient('1.0') + this.getAgileRestApiClient("1.0") .get(`/board/${boardId}/backlog?jql=project=${project}&maxResults=500`) .then(async (response) => { resolve(this.fetchIssues(response)) }) .catch((error) => { - reject(new Error(`Error fetching issues by project: ${this.handleFetchIssuesError(error)}`)) + reject( + new Error( + `Error fetching issues by project: ${this.handleFetchIssuesError( + error + )}` + ) + ) }) }) } @@ -458,7 +518,9 @@ export class JiraCloudProvider implements IProvider { creator: element.fields.creator.displayName, status: element.fields.status.name, type: element.fields.issuetype.name, - storyPointsEstimate: await this.getIssueStoryPointsEstimate(element.key), + storyPointsEstimate: await this.getIssueStoryPointsEstimate( + element.key + ), epic: element.fields.parent?.fields.summary, labels: element.fields.labels, assignee: { @@ -482,7 +544,7 @@ export class JiraCloudProvider implements IProvider { handleFetchIssuesError(error: AxiosError): Error { if (!error.response) { - return error; + return error } if (error.response.status === 404) { @@ -491,7 +553,7 @@ export class JiraCloudProvider implements IProvider { ) } - return error; + return error } async moveIssueToSprintAndRank( @@ -502,17 +564,16 @@ export class JiraCloudProvider implements IProvider { ): Promise { return new Promise((resolve, reject) => { const rankCustomField = this.customFields.get("Rank") - this.getAgileRestApiClient('1.0') - .post( - `/sprint/${sprint}/issue`, - { - rankCustomFieldId: rankCustomField!.match(/_(\d+)/)![1], - issues: [issue], - ...(rankAfter ? { rankAfterIssue: rankAfter } : {}), - ...(rankBefore ? { rankBeforeIssue: rankBefore } : {}), - } - ) - .then(async () => { resolve() }) + this.getAgileRestApiClient("1.0") + .post(`/sprint/${sprint}/issue`, { + rankCustomFieldId: rankCustomField!.match(/_(\d+)/)![1], + issues: [issue], + ...(rankAfter ? { rankAfterIssue: rankAfter } : {}), + ...(rankBefore ? { rankBeforeIssue: rankBefore } : {}), + }) + .then(async () => { + resolve() + }) .catch((error) => { let specificError = error if (error.response) { @@ -527,19 +588,22 @@ export class JiraCloudProvider implements IProvider { } } - reject(new Error(`Error in moving this issue to the Sprint with id ${sprint}: ${specificError}`)) + reject( + new Error( + `Error in moving this issue to the Sprint with id ${sprint}: ${specificError}` + ) + ) }) }) } async moveIssueToBacklog(issue: string): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient('1.0') - .post( - '/backlog/issue', - { issues: [issue] } - ) - .then(async () => { resolve() }) + this.getAgileRestApiClient("1.0") + .post("/backlog/issue", { issues: [issue] }) + .then(async () => { + resolve() + }) .catch((error) => { let specificError = error if (error.response) { @@ -554,7 +618,11 @@ export class JiraCloudProvider implements IProvider { } } - reject(new Error(`Error in moving this issue to the backlog: ${specificError}`)) + reject( + new Error( + `Error in moving this issue to the backlog: ${specificError}` + ) + ) }) }) } @@ -582,8 +650,8 @@ export class JiraCloudProvider implements IProvider { body.rankAfterIssue = rankAfter } - this.getAgileRestApiClient('1.0') - .put('/issue/rank', body) + this.getAgileRestApiClient("1.0") + .put("/issue/rank", body) .then(async (response) => { if (response.status === 204) { resolve() @@ -603,7 +671,11 @@ export class JiraCloudProvider implements IProvider { } } - reject(new Error(`Error in ranking this issue in the backlog: ${specificError}`)) + reject( + new Error( + `Error in ranking this issue in the backlog: ${specificError}` + ) + ) }) }) } @@ -627,7 +699,11 @@ export class JiraCloudProvider implements IProvider { } } - reject(new Error(`Error in getting the story points for issue: ${issue}: ${specificError}`)) + reject( + new Error( + `Error in getting the story points for issue: ${issue}: ${specificError}` + ) + ) }) }) } @@ -653,61 +729,58 @@ export class JiraCloudProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .post( - `/issue`, - { - fields: { - summary, - parent: { key: epic }, - issuetype: { id: type }, - project: { - id: projectId, - }, - reporter: { - id: reporter.id, - }, - ...(priority.id && { priority }), - ...(assignee && { - assignee: { - id: assignee.id, - } - }), - description: { - type: "doc", - version: 1, - content: [ - { - type: "paragraph", - content: [ - { - text: description, - type: "text", - }, - ], - }, - ], + .post(`/issue`, { + fields: { + summary, + parent: { key: epic }, + issuetype: { id: type }, + project: { + id: projectId, + }, + reporter: { + id: reporter.id, + }, + ...(priority.id && { priority }), + ...(assignee && { + assignee: { + id: assignee.id, }, - labels, - ...(offsetStartDate && { - [this.customFields.get("Start date")!]: offsetStartDate, - }), - ...(offsetDueDate && { - [this.customFields.get("Due date")!]: offsetDueDate, + }), + description: { + type: "doc", + version: 1, + content: [ + { + type: "paragraph", + content: [ + { + text: description, + type: "text", + }, + ], + }, + ], + }, + labels, + ...(offsetStartDate && { + [this.customFields.get("Start date")!]: offsetStartDate, + }), + ...(offsetDueDate && { + [this.customFields.get("Due date")!]: offsetDueDate, + }), + ...(sprint && + sprint.id && { + [this.customFields.get("Sprint")!]: +sprint.id, }), - ...(sprint && - sprint.id && { - [this.customFields.get("Sprint")!]: +sprint.id, - }), - ...(storyPointsEstimate && { - [this.customFields.get("Story point estimate")!]: + ...(storyPointsEstimate && { + [this.customFields.get("Story point estimate")!]: storyPointsEstimate, - }), - // ...(files && { - // [this.customFields.get("Attachment")!]: files, - // }), - }, - } - ) + }), + // ...(files && { + // [this.customFields.get("Attachment")!]: files, + // }), + }, + }) .then(async (response) => { const createdIssue = response.data resolve(JSON.stringify(createdIssue.key)) @@ -717,7 +790,9 @@ export class JiraCloudProvider implements IProvider { let specificError = error if (error.response) { if (error.response.status === 404) { - specificError = new Error("The user does not have the necessary permissions") + specificError = new Error( + "The user does not have the necessary permissions" + ) } } @@ -749,67 +824,66 @@ export class JiraCloudProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .put( - `/issue/${issueIdOrKey}`, - { - fields: { - ...(summary && { - summary, - }), - ...(epic && { - parent: { key: epic }, - }), - ...(type && { - issuetype: { id: type }, - }), - ...(projectId && { - project: { - id: projectId, - }, - }), - ...(reporter && { reporter }), - ...(priority && priority.id && { priority }), - ...(assignee && - assignee.id && { - assignee, - }), - ...(description && { - description: { - type: "doc", - version: 1, - content: [ - { - type: "paragraph", - content: [ - { - text: description, - type: "text", - }, - ], - }, - ], - }, - }), - ...(labels && { - labels, - }), - ...(offsetStartDate && { - [this.customFields.get("Start date")!]: offsetStartDate, - }), - ...(offsetDueDate && { - [this.customFields.get("Due date")!]: offsetDueDate, - }), - ...(sprint && { - [this.customFields.get("Sprint")!]: sprint.id, + .put(`/issue/${issueIdOrKey}`, { + fields: { + ...(summary && { + summary, + }), + ...(epic && { + parent: { key: epic }, + }), + ...(type && { + issuetype: { id: type }, + }), + ...(projectId && { + project: { + id: projectId, + }, + }), + ...(reporter && { reporter }), + ...(priority && priority.id && { priority }), + ...(assignee && + assignee.id && { + assignee, }), - ...(storyPointsEstimate !== undefined && { - [this.customFields.get("Story point estimate")!]: + ...(description && { + description: { + type: "doc", + version: 1, + content: [ + { + type: "paragraph", + content: [ + { + text: description, + type: "text", + }, + ], + }, + ], + }, + }), + ...(labels && { + labels, + }), + ...(offsetStartDate && { + [this.customFields.get("Start date")!]: offsetStartDate, + }), + ...(offsetDueDate && { + [this.customFields.get("Due date")!]: offsetDueDate, + }), + ...(sprint && { + [this.customFields.get("Sprint")!]: sprint.id, + }), + ...(storyPointsEstimate !== undefined && { + [this.customFields.get("Story point estimate")!]: storyPointsEstimate, - }), - }, - } - ) - .then(async () => { resolve() }) + }), + }, + }) + .then(async () => { + resolve() + }) .catch((error) => { let specificError = error if (error.response) { @@ -828,29 +902,30 @@ export class JiraCloudProvider implements IProvider { async setTransition(issueKey: string, status: string): Promise { const transitions = new Map() const transitionResponse = await this.getRestApiClient(3).get( - `/issue/${issueKey}/transitions`, + `/issue/${issueKey}/transitions` ) - const {data} = transitionResponse + const { data } = transitionResponse data.transitions.forEach((field: { name: string; id: string }) => { transitions.set(field.name, field.id) }) const transitionId = +transitions.get(status)! - this.getRestApiClient(3).post( - `/issue/${issueKey}/transitions`, - { transition: { id: transitionId } } - ) + this.getRestApiClient(3).post(`/issue/${issueKey}/transitions`, { + transition: { id: transitionId }, + }) } async getEpicsByProject(projectIdOrKey: string): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .get(`search?jql=issuetype = Epic AND project = ${projectIdOrKey}`) + .get( + `search?jql=issuetype = Epic AND project = ${projectIdOrKey}&fields=*all` + ) .then(async (response) => { const epics: Promise = Promise.all( - response.data.issues.map(async (element: JiraIssue) => ({ + response.data.issues.map(async (element: JiraEpic) => ({ issueKey: element.key, summary: element.fields.summary, labels: element.fields.labels, @@ -861,7 +936,15 @@ export class JiraCloudProvider implements IProvider { subtasks: element.fields.subtasks, created: element.fields.created, updated: element.fields.updated, - comment: element.fields.comment ?? { + comment: { + comments: element.fields.comment.comments.map((coelem) => ({ + id: coelem.id, + body: coelem.body.content[0].content[0].text, + author: coelem.author, + created: coelem.created, + updated: coelem.updated, + })), + } ?? { comments: [], }, projectId: element.fields.project.id, @@ -881,7 +964,11 @@ export class JiraCloudProvider implements IProvider { } } - reject(new Error(`Error in fetching the epics for the project ${projectIdOrKey}: ${specificError}`)) + reject( + new Error( + `Error in fetching the epics for the project ${projectIdOrKey}: ${specificError}` + ) + ) }) }) } @@ -889,7 +976,7 @@ export class JiraCloudProvider implements IProvider { async getLabels(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .get('/label') + .get("/label") .then(async (response) => { resolve(response.data.values) }) @@ -904,7 +991,7 @@ export class JiraCloudProvider implements IProvider { // and GET /rest/api/3/priority/search is experimental return new Promise((resolve, reject) => { this.getRestApiClient(3) - .get('/priority/search') + .get("/priority/search") .then(async (response) => { const priorityData: JiraPriority = response.data resolve(priorityData.values) @@ -921,36 +1008,41 @@ export class JiraCloudProvider implements IProvider { ): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .post( - `/issue/${issueIdOrKey}/comment`, - { - body: { - content: [ - { - content: [ - { - text: commentText.replace(/\n/g, " "), - type: "text" - } - ], - type: "paragraph" - } - ], - type: "doc", - version: 1 - } - } - ) - .then(async () => { resolve() }) + .post(`/issue/${issueIdOrKey}/comment`, { + body: { + content: [ + { + content: [ + { + text: commentText.replace(/\n/g, " "), + type: "text", + }, + ], + type: "paragraph", + }, + ], + type: "doc", + version: 1, + }, + }) + .then(async () => { + resolve() + }) .catch((error) => { let specificError = error if (error.response) { if (error.response.status === 404) { - specificError = new Error("The issue was not found or the user does not have the necessary permissions") + specificError = new Error( + "The issue was not found or the user does not have the necessary permissions" + ) } } - reject(new Error(`Error adding a comment to the issue ${issueIdOrKey}: ${specificError}`)) + reject( + new Error( + `Error adding a comment to the issue ${issueIdOrKey}: ${specificError}` + ) + ) }) }) } @@ -962,38 +1054,45 @@ export class JiraCloudProvider implements IProvider { ): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .put( - `/issue/${issueIdOrKey}/comment/${commentId}`, - { - body: { - content: [ - { - content: [ - { - text: commentText.replace(/\n/g, " "), - type: "text" - } - ], - type: "paragraph" - } - ], - type: "doc", - version: 1 - } - } - ) - .then(async () => { resolve() }) + .put(`/issue/${issueIdOrKey}/comment/${commentId}`, { + body: { + content: [ + { + content: [ + { + text: commentText.replace(/\n/g, " "), + type: "text", + }, + ], + type: "paragraph", + }, + ], + type: "doc", + version: 1, + }, + }) + .then(async () => { + resolve() + }) .catch((error) => { let specificError = error if (error.response) { if (error.response.status === 400) { - specificError = new Error("The user does not have permission to edit the comment or the request is invalid") + specificError = new Error( + "The user does not have permission to edit the comment or the request is invalid" + ) } else if (error.response.status === 404) { - specificError = new Error("The issue was not found or the user does not have the necessary permissions") + specificError = new Error( + "The issue was not found or the user does not have the necessary permissions" + ) } } - reject(new Error(`Error editing the comment in issue ${issueIdOrKey}: ${specificError}`)) + reject( + new Error( + `Error editing the comment in issue ${issueIdOrKey}: ${specificError}` + ) + ) }) }) } @@ -1002,20 +1101,32 @@ export class JiraCloudProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(3) .delete(`/issue/${issueIdOrKey}/comment/${commentId}`) - .then(async () => { resolve() }) + .then(async () => { + resolve() + }) .catch((error) => { let specificError = error if (error.response) { if (error.response.status === 400) { - specificError = new Error("The user does not have permission to delete the comment") + specificError = new Error( + "The user does not have permission to delete the comment" + ) } else if (error.response.status === 404) { - specificError = new Error("The issue was not found or the user does not have the necessary permissions") + specificError = new Error( + "The issue was not found or the user does not have the necessary permissions" + ) } else if (error.response.status === 405) { - specificError = new Error("An anonymous call has been made to the operation") + specificError = new Error( + "An anonymous call has been made to the operation" + ) } } - reject(new Error(`Error deleting the comment in issue ${issueIdOrKey}: ${specificError}`)) + reject( + new Error( + `Error deleting the comment in issue ${issueIdOrKey}: ${specificError}` + ) + ) }) }) } @@ -1024,22 +1135,36 @@ export class JiraCloudProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(2) .delete(`/issue/${issueIdOrKey}`) - .then(async () => { resolve() }) + .then(async () => { + resolve() + }) .catch((error) => { let specificError = error if (error.response) { if (error.response.status === 400) { - specificError = new Error("The issue has subtasks and deleteSubtasks is not set to true") + specificError = new Error( + "The issue has subtasks and deleteSubtasks is not set to true" + ) } else if (error.response.status === 403) { - specificError = new Error("The user does not have permission to delete the issue") + specificError = new Error( + "The user does not have permission to delete the issue" + ) } else if (error.response.status === 404) { - specificError = new Error("The issue was not found or the user does not have the necessary permissions") + specificError = new Error( + "The issue was not found or the user does not have the necessary permissions" + ) } else if (error.response.status === 405) { - specificError = new Error("An anonymous call has been made to the operation") + specificError = new Error( + "An anonymous call has been made to the operation" + ) } } - reject(new Error(`Error deleting the subtask ${issueIdOrKey}: ${specificError}`)) + reject( + new Error( + `Error deleting the subtask ${issueIdOrKey}: ${specificError}` + ) + ) }) }) } @@ -1052,23 +1177,20 @@ export class JiraCloudProvider implements IProvider { ): Promise<{ id: string; key: string }> { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .post( - '/issue', - { - fields: { - summary: subtaskSummary, - issuetype: { - id: subtaskIssueTypeId, - }, - parent: { - key: parentIssueKey, - }, - project: { - id: projectId, - }, + .post("/issue", { + fields: { + summary: subtaskSummary, + issuetype: { + id: subtaskIssueTypeId, }, - } - ) + parent: { + key: parentIssueKey, + }, + project: { + id: projectId, + }, + }, + }) .then(async (response) => { const createdSubtask: { id: string; key: string } = response.data resolve(createdSubtask) @@ -1083,9 +1205,9 @@ export class JiraCloudProvider implements IProvider { return new Promise((resolve, reject) => { if (this.accessToken !== undefined) { // IMPROVE expose API client instead of resource - const {defaults} = this.getRestApiClient(3) + const { defaults } = this.getRestApiClient(3) const result: Resource = { - baseUrl: defaults.baseURL ?? '', + baseUrl: defaults.baseURL ?? "", authorization: defaults.headers.Authorization as string, } resolve(result) @@ -1106,29 +1228,32 @@ export class JiraCloudProvider implements IProvider { const offsetEndDate = this.offsetDate(endDate) return new Promise((resolve, reject) => { - this.getAgileRestApiClient('1.0') - .post( - '/sprint', - { - name, - originBoardId, - ...(offsetStartDate && { - startDate: offsetStartDate, - }), - ...(offsetEndDate && { - endDate: offsetEndDate, - }), - ...(goal && { goal }), - } - ) - .then(async () => { resolve() }) + this.getAgileRestApiClient("1.0") + .post("/sprint", { + name, + originBoardId, + ...(offsetStartDate && { + startDate: offsetStartDate, + }), + ...(offsetEndDate && { + endDate: offsetEndDate, + }), + ...(goal && { goal }), + }) + .then(async () => { + resolve() + }) .catch((error) => { let specificError = error if (error.response) { if (error.response.status === 403) { - specificError = new Error("The user does not have the necessary permissions") + specificError = new Error( + "The user does not have the necessary permissions" + ) } else if (error.response.status === 404) { - specificError = new Error("The Board does not exist or the user does not have the necessary permissions to view it") + specificError = new Error( + "The Board does not exist or the user does not have the necessary permissions to view it" + ) } } diff --git a/electron/providers/jira-server-provider/JiraServerProvider.ts b/electron/providers/jira-server-provider/JiraServerProvider.ts index 402c4668..47c78e74 100644 --- a/electron/providers/jira-server-provider/JiraServerProvider.ts +++ b/electron/providers/jira-server-provider/JiraServerProvider.ts @@ -1,5 +1,5 @@ /* eslint-disable class-methods-use-this */ -import axios, {AxiosError, AxiosResponse, isAxiosError} from "axios"; +import axios, { AxiosError, AxiosResponse, isAxiosError } from "axios" import { dateTimeFormat, Issue, @@ -11,9 +11,15 @@ import { SprintCreate, User, } from "../../../types" -import {JiraIssue, JiraIssueType, JiraProject, JiraSprint,} from "../../../types/jira" -import {IProvider} from "../base-provider" -import {JiraServerInfo, JiraServerUser} from "./server-types"; +import { + JiraEpic, + JiraIssue, + JiraIssueType, + JiraProject, + JiraSprint, +} from "../../../types/jira" +import { IProvider } from "../base-provider" +import { JiraServerInfo, JiraServerUser } from "./server-types" export class JiraServerProvider implements IProvider { private loginOptions = { @@ -28,44 +34,58 @@ export class JiraServerProvider implements IProvider { private reversedCustomFields = new Map() - private executeVersioned(functionsByVersionMatcher: { [versionMatcher: string]: (...args: never[]) => R }, ...args: never[]) { + private executeVersioned( + functionsByVersionMatcher: { + [versionMatcher: string]: (...args: never[]) => R + }, + ...args: never[] + ) { if (!this.serverInfo) { - throw new Error('Server info not set!') + throw new Error("Server info not set!") } const matches = (matcher: string): boolean => { let match = true - matcher.split('.').forEach((matcherPart, index) => { - match = match && ( - matcherPart === '*' - || matcherPart === this.serverInfo!.versionNumbers[index].toString() - ) + matcher.split(".").forEach((matcherPart, index) => { + match = + match && + (matcherPart === "*" || + matcherPart === this.serverInfo!.versionNumbers[index].toString()) }) return match } - const isAMoreSpecificThanB = (matcherA: string, matcherB: string): boolean => { - const matcherBParts = matcherB.split('.') - let isMoreSpecific = false; - matcherA.split('.').forEach((matcherAPart, index) => { - if (matcherBParts[index] === '*' && matcherAPart !== '*') { - isMoreSpecific = true; + const isAMoreSpecificThanB = ( + matcherA: string, + matcherB: string + ): boolean => { + const matcherBParts = matcherB.split(".") + let isMoreSpecific = false + matcherA.split(".").forEach((matcherAPart, index) => { + if (matcherBParts[index] === "*" && matcherAPart !== "*") { + isMoreSpecific = true } }) - return isMoreSpecific; + return isMoreSpecific } let selectedMatcher: string | undefined Object.keys(functionsByVersionMatcher).forEach((matcher) => { - if (matches(matcher) && (selectedMatcher === undefined || isAMoreSpecificThanB(matcher, selectedMatcher))) { + if ( + matches(matcher) && + (selectedMatcher === undefined || + isAMoreSpecificThanB(matcher, selectedMatcher)) + ) { selectedMatcher = matcher } }) if (!selectedMatcher) { - throw new Error(`No version matcher found for version: ${this.serverInfo.version}`) + throw new Error( + `No version matcher found for version: ${this.serverInfo.version}` + ) } return functionsByVersionMatcher[selectedMatcher](...args) @@ -87,47 +107,73 @@ export class JiraServerProvider implements IProvider { }, }) - const recreateAxiosError = (originalError: AxiosError, message: string) => new AxiosError( + const recreateAxiosError = (originalError: AxiosError, message: string) => + new AxiosError( message, originalError.code, originalError.config, originalError.request, originalError.response - ) + ) instance.interceptors.response.use( - (response) => response, - (error) => { - if (isAxiosError(error) && error.response) { - const statusCode = error.response.status - if (statusCode === 400) { - return Promise.reject(recreateAxiosError(error, `Invalid request: ${JSON.stringify(error.response.data)}`)) - } if (statusCode === 401) { - return Promise.reject(recreateAxiosError(error, `User not authenticated: ${JSON.stringify(error.response.data)}`)) - } if (error.response.status === 403) { - return Promise.reject(recreateAxiosError(error, `User does not have a valid licence: ${JSON.stringify(error.response.data)}`)) - } if (error.response.status === 429) { - return Promise.reject(recreateAxiosError(error, `Rate limit exceeded: ${JSON.stringify(error.response.data)}`)) - } + (response) => response, + (error) => { + if (isAxiosError(error) && error.response) { + const statusCode = error.response.status + if (statusCode === 400) { + return Promise.reject( + recreateAxiosError( + error, + `Invalid request: ${JSON.stringify(error.response.data)}` + ) + ) + } + if (statusCode === 401) { + return Promise.reject( + recreateAxiosError( + error, + `User not authenticated: ${JSON.stringify(error.response.data)}` + ) + ) + } + if (error.response.status === 403) { + return Promise.reject( + recreateAxiosError( + error, + `User does not have a valid licence: ${JSON.stringify( + error.response.data + )}` + ) + ) + } + if (error.response.status === 429) { + return Promise.reject( + recreateAxiosError( + error, + `Rate limit exceeded: ${JSON.stringify(error.response.data)}` + ) + ) } - - return Promise.reject(error) } + + return Promise.reject(error) + } ) return instance } - private getRestApiClient(version: string|number) { - return this.constructRestBasedClient('api', version.toString()); + private getRestApiClient(version: string | number) { + return this.constructRestBasedClient("api", version.toString()) } private getAuthRestApiClient(version: number) { - return this.constructRestBasedClient('auth', version.toString()); + return this.constructRestBasedClient("auth", version.toString()) } private getAgileRestApiClient(version: string) { - return this.constructRestBasedClient('agile', version); + return this.constructRestBasedClient("agile", version) } async login({ @@ -151,13 +197,15 @@ export class JiraServerProvider implements IProvider { async getServerInfo(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(2) - .get('/serverInfo') + .get("/serverInfo") .then((response: AxiosResponse) => { this.serverInfo = response.data if (this.serverInfo.versionNumbers[0] < 7) { - reject(new Error( - `Your Jira server version is unsupported. Minimum major version: 7. Your version: ${this.serverInfo.versionNumbers[0]}`, - )) + reject( + new Error( + `Your Jira server version is unsupported. Minimum major version: 7. Your version: ${this.serverInfo.versionNumbers[0]}` + ) + ) } resolve() @@ -171,13 +219,16 @@ export class JiraServerProvider implements IProvider { async isLoggedIn(): Promise { return new Promise((resolve, reject) => { this.getAuthRestApiClient(1) - .get('/session') - .then(() => { resolve() }) + .get("/session") + .then(() => { + resolve() + }) .catch((error) => { if (isAxiosError(error) && error.response) { if (error.response.status === 401) { return Promise.reject(new Error("Wrong Username or Password")) - } if (error.response.status === 404) { + } + if (error.response.status === 404) { return Promise.reject(new Error("Wrong URL")) } } @@ -193,8 +244,10 @@ export class JiraServerProvider implements IProvider { async logout(): Promise { return new Promise((resolve, reject) => { this.getAuthRestApiClient(1) - .delete('/session') - .then(() => { resolve() }) + .delete("/session") + .then(() => { + resolve() + }) .catch((error) => { reject(new Error(`Error in logging out: ${error}`)) }) @@ -204,7 +257,7 @@ export class JiraServerProvider implements IProvider { async mapCustomFields(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(2) - .get('/field') + .get("/field") .then((response) => { response.data.forEach((field: { name: string; id: string }) => { this.customFields.set(field.name, field.id) @@ -221,7 +274,7 @@ export class JiraServerProvider implements IProvider { async getProjects(): Promise { return new Promise((resolve) => { this.getRestApiClient(2) - .get('/project?expand=lead,description') + .get("/project?expand=lead,description") .then((response) => { const projects = response.data.map((project: JiraProject) => ({ key: project.key, @@ -243,13 +296,15 @@ export class JiraServerProvider implements IProvider { const issueTypes: JiraIssueType[] = response.data resolve(issueTypes as IssueType[]) }) - .catch((error) => reject(new Error(`Error in fetching the issue types: ${error}`))) + .catch((error) => + reject(new Error(`Error in fetching the issue types: ${error}`)) + ) }) } async getBoardIds(project: string): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient('1.0') + this.getAgileRestApiClient("1.0") .get(`/board?projectKeyOrId=${project}`) .then(async (response) => { const boardIds: number[] = response.data.values.map( @@ -257,13 +312,15 @@ export class JiraServerProvider implements IProvider { ) resolve(boardIds) }) - .catch((error) => reject(new Error(`Error in fetching the boards: ${error}`))) + .catch((error) => + reject(new Error(`Error in fetching the boards: ${error}`)) + ) }) } async getSprints(boardId: number): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient('1.0') + this.getAgileRestApiClient("1.0") .get(`/board/${boardId}/sprint`) .then(async (response) => { const sprints: Sprint[] = response.data.values @@ -287,16 +344,20 @@ export class JiraServerProvider implements IProvider { }) resolve(sprints) }) - .catch((error) => reject(new Error(`Error in fetching the boards: ${error}`))) + .catch((error) => + reject(new Error(`Error in fetching the boards: ${error}`)) + ) }) } async getIssuesByProject(project: string, boardId: number): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient('1.0') + this.getAgileRestApiClient("1.0") .get(`/board/${boardId}/issue?jql=project=${project}&maxResults=10000`) .then((response) => resolve(this.fetchIssues(response))) - .catch((error) => reject(new Error(`Error in fetching issues: ${error}`))) + .catch((error) => + reject(new Error(`Error in fetching issues: ${error}`)) + ) }) } @@ -306,10 +367,14 @@ export class JiraServerProvider implements IProvider { boardId: number ): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient('1.0') - .get(`/board/${boardId}/sprint/${sprintId}/issue?jql=project=${project}`) + this.getAgileRestApiClient("1.0") + .get( + `/board/${boardId}/sprint/${sprintId}/issue?jql=project=${project}` + ) .then((response) => resolve(this.fetchIssues(response))) - .catch((error) => reject(new Error(`Error in fetching issues: ${error}`))) + .catch((error) => + reject(new Error(`Error in fetching issues: ${error}`)) + ) }) } @@ -318,10 +383,14 @@ export class JiraServerProvider implements IProvider { boardId: number ): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient('1.0') - .get(`/board/${boardId}/backlog?jql=sprint is EMPTY AND project=${project}`) + this.getAgileRestApiClient("1.0") + .get( + `/board/${boardId}/backlog?jql=sprint is EMPTY AND project=${project}` + ) .then((response) => resolve(this.fetchIssues(response))) - .catch((error) => reject(new Error(`Error in fetching issues: ${error}`))) + .catch((error) => + reject(new Error(`Error in fetching issues: ${error}`)) + ) }) } @@ -335,7 +404,9 @@ export class JiraServerProvider implements IProvider { creator: element.fields.creator.displayName, status: element.fields.status.name, type: element.fields.issuetype.name, - storyPointsEstimate: await this.getIssueStoryPointsEstimate(element.key), + storyPointsEstimate: await this.getIssueStoryPointsEstimate( + element.key + ), epic: element.fields.parent?.fields.summary, labels: element.fields.labels, assignee: { @@ -363,33 +434,33 @@ export class JiraServerProvider implements IProvider { ): Promise { return new Promise((resolve, reject) => { const rankCustomField = this.customFields.get("Rank") - this.getAgileRestApiClient('1.0') - .post( - `/sprint/${sprint}/issue`, - { - rankCustomFieldId: rankCustomField!.match(/_(\d+)/)![1], - issues: [issue], - ...(rankAfter && { rankAfterIssue: rankAfter }), - ...(rankBefore && { rankBeforeIssue: rankBefore }), - } - ) + this.getAgileRestApiClient("1.0") + .post(`/sprint/${sprint}/issue`, { + rankCustomFieldId: rankCustomField!.match(/_(\d+)/)![1], + issues: [issue], + ...(rankAfter && { rankAfterIssue: rankAfter }), + ...(rankBefore && { rankBeforeIssue: rankBefore }), + }) .then(() => resolve()) .catch((error) => { - reject(new Error(`Error in moving this issue to the Sprint with id ${sprint}: ${error}`)) + reject( + new Error( + `Error in moving this issue to the Sprint with id ${sprint}: ${error}` + ) + ) }) }) } async moveIssueToBacklog(issue: string): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient('1.0') - .post( - '/backlog/issue', - { issues: [issue] } - ) + this.getAgileRestApiClient("1.0") + .post("/backlog/issue", { issues: [issue] }) .then(() => resolve()) .catch((error) => - reject(new Error(`Error in moving this issue to the Backlog: ${error}`)) + reject( + new Error(`Error in moving this issue to the Backlog: ${error}`) + ) ) }) } @@ -415,11 +486,13 @@ export class JiraServerProvider implements IProvider { } else if (rankAfter) { body.rankAfterIssue = rankAfter } - this.getAgileRestApiClient('1.0') - .put('/issue/rank', body) + this.getAgileRestApiClient("1.0") + .put("/issue/rank", body) .then(() => resolve()) .catch((error) => - reject(new Error(`Error in moving this issue to the Backlog: ${error}`)) + reject( + new Error(`Error in moving this issue to the Backlog: ${error}`) + ) ) }) } @@ -435,7 +508,11 @@ export class JiraServerProvider implements IProvider { resolve(points) }) .catch((error) => - reject(new Error(`Error in getting the story points for issue: ${issue}: ${error}`)) + reject( + new Error( + `Error in getting the story points for issue: ${issue}: ${error}` + ) + ) ) }) } @@ -445,26 +522,35 @@ export class JiraServerProvider implements IProvider { this.getRestApiClient(2) .get(`/user/assignable/search?project=${projectIdOrKey}`) .then(async (response: AxiosResponse) => { - const users: User[] = response.data.map((user) => ({ - id: user.key, - name: user.name, - displayName: user.displayName, - avatarUrls: user.avatarUrls, - emailAddress: user.emailAddress, - } as User)) + const users: User[] = response.data.map( + (user) => + ({ + id: user.key, + name: user.name, + displayName: user.displayName, + avatarUrls: user.avatarUrls, + emailAddress: user.emailAddress, + } as User) + ) resolve(users) }) .catch((error) => { if (error.response) { if (error.response.status === 404) { - return Promise.reject(Error(`Project was not found: ${error.response.data}`)) + return Promise.reject( + Error(`Project was not found: ${error.response.data}`) + ) } } return Promise.reject(error) }) .catch((error) => { - reject(new Error(`Error in fetching the assignable users for the project ${projectIdOrKey}: ${error}`)) + reject( + new Error( + `Error in fetching the assignable users for the project ${projectIdOrKey}: ${error}` + ) + ) }) }) } @@ -472,15 +558,19 @@ export class JiraServerProvider implements IProvider { getCurrentUser(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(2) - .get('/myself') - .then(async (response: AxiosResponse) => resolve({ - id: response.data.key, - name: response.data.name, - displayName: response.data.displayName, - avatarUrls: response.data.avatarUrls, - emailAddress: response.data.emailAddress, - } as User)) - .catch((error) => reject(new Error(`Error in the current user: ${error}`))) + .get("/myself") + .then(async (response: AxiosResponse) => + resolve({ + id: response.data.key, + name: response.data.name, + displayName: response.data.displayName, + avatarUrls: response.data.avatarUrls, + emailAddress: response.data.emailAddress, + } as User) + ) + .catch((error) => + reject(new Error(`Error in the current user: ${error}`)) + ) }) } /* eslint-disable @typescript-eslint/no-unused-vars */ @@ -505,81 +595,84 @@ export class JiraServerProvider implements IProvider { const offsetDueDate = this.offsetDate(dueDate) return new Promise((resolve, reject) => { - this.getIssueTypesByProject(projectId) - .then((issueTypes) => { - const relevantIssueType = issueTypes.find((issueType) => issueType.id === type) - - this.getRestApiClient(2) - .post( - `/issue`, - { - fields: { - summary, - parent: { key: epic }, - issuetype: { id: type }, - project: { - id: projectId, - }, - reporter: { - name: reporter.name, - }, - ...(priority.id && { priority }), - ...(assignee && { - assignee: { - name: assignee.name, - }, - }), - description, - labels, - ...(offsetStartDate && { - [this.customFields.get("Start date")!]: offsetStartDate, - }), - ...(offsetDueDate && { - [this.customFields.get("Due date")!]: offsetDueDate, - }), - ...(sprint && - sprint.id && { - [this.customFields.get("Sprint")!]: +sprint.id, - }), - ...(storyPointsEstimate && { - [this.customFields.get("Story point estimate")!]: - storyPointsEstimate, - }), - ...(relevantIssueType && relevantIssueType.name === 'Epic' && { - [this.customFields.get("Epic Name")!]: summary - }), - // ...(files && { - // [this.customFields.get("Attachment")!]: files, - // }), + this.getIssueTypesByProject(projectId).then((issueTypes) => { + const relevantIssueType = issueTypes.find( + (issueType) => issueType.id === type + ) + + this.getRestApiClient(2) + .post(`/issue`, { + fields: { + summary, + parent: { key: epic }, + issuetype: { id: type }, + project: { + id: projectId, + }, + reporter: { + name: reporter.name, + }, + ...(priority.id && { priority }), + ...(assignee && { + assignee: { + name: assignee.name, }, + }), + description, + labels, + ...(offsetStartDate && { + [this.customFields.get("Start date")!]: offsetStartDate, + }), + ...(offsetDueDate && { + [this.customFields.get("Due date")!]: offsetDueDate, + }), + ...(sprint && + sprint.id && { + [this.customFields.get("Sprint")!]: +sprint.id, + }), + ...(storyPointsEstimate && { + [this.customFields.get("Story point estimate")!]: + storyPointsEstimate, + }), + ...(relevantIssueType && + relevantIssueType.name === "Epic" && { + [this.customFields.get("Epic Name")!]: summary, + }), + // ...(files && { + // [this.customFields.get("Attachment")!]: files, + // }), + }, + }) + .then(async (response) => { + const createdIssue = response.data + resolve(JSON.stringify(createdIssue.key)) + await this.setTransition(createdIssue.id, status) + }) + .catch((error) => { + let specificError = error + if (error.response) { + if (error.response.status === 404) { + specificError = new Error( + "The user does not have the necessary permissions" + ) } - ) - .then(async (response) => { - const createdIssue = response.data - resolve(JSON.stringify(createdIssue.key)) - await this.setTransition(createdIssue.id, status) - }) - .catch((error) => { - let specificError = error - if (error.response) { - if (error.response.status === 404) { - specificError = new Error("The user does not have the necessary permissions") - } - } + } - reject(new Error(`Error creating issue: ${specificError}`)) - }) - }) + reject(new Error(`Error creating issue: ${specificError}`)) + }) + }) }) } getEpicsByProject(projectIdOrKey: string): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(2) - .get(`search?jql=issuetype = Epic AND project = ${projectIdOrKey}`) + .get( + `search?jql=issuetype = Epic AND project = ${projectIdOrKey}&fields=*all}` + ) .then(async (response) => { const epics: Promise = Promise.all( - response.data.issues.map(async (element: JiraIssue) => ({ + response.data.issues.map(async (element: JiraEpic) => ({ issueKey: element.key, summary: element.fields.summary, labels: element.fields.labels, @@ -587,6 +680,23 @@ export class JiraServerProvider implements IProvider { displayName: element.fields.assignee?.displayName, avatarUrls: element.fields.assignee?.avatarUrls, }, + subtasks: element.fields.subtasks, + created: element.fields.created, + updated: element.fields.updated, + comment: { + comments: element.fields.comment.comments.map((coelem) => ({ + id: coelem.id, + body: coelem.body.content[0].content[0].text, + author: coelem.author, + created: coelem.created, + updated: coelem.updated, + })), + } ?? { + comments: [], + }, + projectId: element.fields.project.id, + sprint: element.fields.sprint, + attachments: element.fields.attachment, })) ) resolve(epics) @@ -601,20 +711,26 @@ export class JiraServerProvider implements IProvider { } } - reject(new Error(`Error in fetching the epics for the project ${projectIdOrKey}: ${specificError}`)) + reject( + new Error( + `Error in fetching the epics for the project ${projectIdOrKey}: ${specificError}` + ) + ) }) }) } getIssuesBySprint(sprintId: number): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient('1.0') + this.getAgileRestApiClient("1.0") .get(`/sprint/${sprintId}/issue`) .then(async (response) => { resolve(this.fetchIssues(response)) }) .catch((error) => { - reject(new Error(`Error fetching issues by sprint ${sprintId}: ${error}`)) + reject( + new Error(`Error fetching issues by sprint ${sprintId}: ${error}`) + ) }) }) } @@ -622,12 +738,14 @@ export class JiraServerProvider implements IProvider { getLabels(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(2) - .get('/jql/autocompletedata/suggestions?fieldName=labels') + .get("/jql/autocompletedata/suggestions?fieldName=labels") .then((response: AxiosResponse<{ results: { value: string }[] }>) => { resolve(response.data.results.map((result) => result.value)) }) .catch((error) => - reject(new Error(`Error in fetching labels: ${JSON.stringify(error)}`)) + reject( + new Error(`Error in fetching labels: ${JSON.stringify(error)}`) + ) ) }) } @@ -635,7 +753,7 @@ export class JiraServerProvider implements IProvider { getPriorities(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(2) - .get('/priority') + .get("/priority") .then((response) => { const priorityData: Priority[] = response.data resolve(priorityData) @@ -648,15 +766,15 @@ export class JiraServerProvider implements IProvider { getIssueTypesWithFieldsMap(): Promise<{ [key: string]: string[] }> { return this.executeVersioned({ - '7.*': this.getIssueTypesWithFieldsMap_7.bind(this), - '*': this.getIssueTypesWithFieldsMap_8and9.bind(this) + "7.*": this.getIssueTypesWithFieldsMap_7.bind(this), + "*": this.getIssueTypesWithFieldsMap_8and9.bind(this), }) } getIssueTypesWithFieldsMap_7(): Promise<{ [key: string]: string[] }> { return new Promise((resolve) => { this.getRestApiClient(2) - .get('/issue/createmeta?expand=projects.issuetypes.fields') + .get("/issue/createmeta?expand=projects.issuetypes.fields") .then(async (response) => { const issueTypeToFieldsMap: { [key: string]: string[] } = {} response.data.projects.forEach( @@ -683,39 +801,51 @@ export class JiraServerProvider implements IProvider { getIssueTypesWithFieldsMap_8and9(): Promise<{ [key: string]: string[] }> { return new Promise((resolve) => { // IMPROVE: This is barely scalable - this.getProjects() - .then(async (projects) => { - const issueTypeToFieldsMap: { [key: string]: string[] } = {} - await Promise.all(projects.map((project) => + this.getProjects().then(async (projects) => { + const issueTypeToFieldsMap: { [key: string]: string[] } = {} + await Promise.all( + projects.map((project) => // IMPROVE: This call currently only supports 50 issue types this.getRestApiClient(2) .get(`/issue/createmeta/${project.id}/issuetypes`) .then(async (response) => { - await Promise.all(response.data.values.map((issueType: { id: string }) => - // IMPROVE: This call currently only supports 50 issue types - this.getRestApiClient(2) - .get(`/issue/createmeta/${project.id}/issuetypes/${issueType.id}`) - .then((issueTypesResponse) => { - issueTypeToFieldsMap[issueType.id] = issueTypesResponse.data.values.map( - (issueTypeField: { fieldId: string }) => this.reversedCustomFields.get(issueTypeField.fieldId)! + await Promise.all( + response.data.values.map((issueType: { id: string }) => + // IMPROVE: This call currently only supports 50 issue types + this.getRestApiClient(2) + .get( + `/issue/createmeta/${project.id}/issuetypes/${issueType.id}` ) - }) - )) + .then((issueTypesResponse) => { + issueTypeToFieldsMap[issueType.id] = + issueTypesResponse.data.values.map( + (issueTypeField: { fieldId: string }) => + this.reversedCustomFields.get( + issueTypeField.fieldId + )! + ) + }) + ) + ) }) - )) + ) + ) - return resolve(issueTypeToFieldsMap) - }) + return resolve(issueTypeToFieldsMap) + }) }) } getResource(): Promise { return new Promise((resolve, reject) => { - if (this.loginOptions.username !== undefined && this.loginOptions.password) { + if ( + this.loginOptions.username !== undefined && + this.loginOptions.password + ) { // IMPROVE expose API client instead of resource - const {defaults} = this.getRestApiClient(2) + const { defaults } = this.getRestApiClient(2) const result: Resource = { - baseUrl: defaults.baseURL ?? '', + baseUrl: defaults.baseURL ?? "", authorization: defaults.headers.Authorization as string, } resolve(result) @@ -736,29 +866,32 @@ export class JiraServerProvider implements IProvider { const offsetEndDate = this.offsetDate(endDate) return new Promise((resolve, reject) => { - this.getAgileRestApiClient('1.0') - .post( - '/sprint', - { - name, - originBoardId, - ...(offsetStartDate && { - startDate: offsetStartDate, - }), - ...(offsetEndDate && { - endDate: offsetEndDate, - }), - ...(goal && { goal }), - } - ) - .then(async () => { resolve() }) + this.getAgileRestApiClient("1.0") + .post("/sprint", { + name, + originBoardId, + ...(offsetStartDate && { + startDate: offsetStartDate, + }), + ...(offsetEndDate && { + endDate: offsetEndDate, + }), + ...(goal && { goal }), + }) + .then(async () => { + resolve() + }) .catch((error) => { let specificError = error if (error.response) { if (error.response.status === 403) { - specificError = new Error("The user does not have the necessary permissions") + specificError = new Error( + "The user does not have the necessary permissions" + ) } else if (error.response.status === 404) { - specificError = new Error("The Board does not exist or the user does not have the necessary permissions to view it") + specificError = new Error( + "The Board does not exist or the user does not have the necessary permissions to view it" + ) } } @@ -771,20 +904,32 @@ export class JiraServerProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(2) .delete(`/issue/${issueIdOrKey}?deleteSubtasks`) - .then(async () => { resolve() }) + .then(async () => { + resolve() + }) .catch((error) => { let specificError = error if (error.response) { if (error.response.status === 403) { - specificError = new Error("The user does not have permission to delete the issue") + specificError = new Error( + "The user does not have permission to delete the issue" + ) } else if (error.response.status === 404) { - specificError = new Error("The issue was not found or the user does not have the necessary permissions") + specificError = new Error( + "The issue was not found or the user does not have the necessary permissions" + ) } else if (error.response.status === 405) { - specificError = new Error("An anonymous call has been made to the operation") + specificError = new Error( + "An anonymous call has been made to the operation" + ) } } - reject(new Error(`Error deleting the issue ${issueIdOrKey}: ${specificError}`)) + reject( + new Error( + `Error deleting the issue ${issueIdOrKey}: ${specificError}` + ) + ) }) }) } @@ -797,30 +942,27 @@ export class JiraServerProvider implements IProvider { ): Promise<{ id: string; key: string }> { return new Promise((resolve, reject) => { this.getRestApiClient(2) - .post( - '/issue', - { - fields: { - summary: subtaskSummary, - issuetype: { - id: subtaskIssueTypeId, - }, - parent: { - key: parentIssueKey, - }, - project: { - id: projectId, - }, - }, - } - ) - .then(async (response) => { - const createdSubtask: { id: string; key: string } = response.data - resolve(createdSubtask) - }) - .catch((error) => { - reject(new Error(`Error creating subtask: ${error}`)) - }) + .post("/issue", { + fields: { + summary: subtaskSummary, + issuetype: { + id: subtaskIssueTypeId, + }, + parent: { + key: parentIssueKey, + }, + project: { + id: projectId, + }, + }, + }) + .then(async (response) => { + const createdSubtask: { id: string; key: string } = response.data + resolve(createdSubtask) + }) + .catch((error) => { + reject(new Error(`Error creating subtask: ${error}`)) + }) }) } @@ -847,54 +989,53 @@ export class JiraServerProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(2) - .put( - `/issue/${issueIdOrKey}`, - { - fields: { - ...(summary && { - summary, - }), - ...(epic && { - parent: { key: epic }, - }), - ...(type && { - issuetype: { id: type }, - }), - ...(projectId && { - project: { - id: projectId, - }, - }), - ...(reporter && { - reporter, - }), - ...(priority && priority.id && { priority }), - ...(assignee && { - assignee, - }), - ...(description && { - description - }), - ...(labels && { - labels, - }), - ...(offsetStartDate && { - [this.customFields.get("Start date")!]: offsetStartDate, - }), - ...(offsetDueDate && { - [this.customFields.get("Due date")!]: offsetDueDate, - }), - ...(sprint && { - [this.customFields.get("Sprint")!]: sprint.id, - }), - ...(storyPointsEstimate !== undefined && { - [this.customFields.get("Story point estimate")!]: + .put(`/issue/${issueIdOrKey}`, { + fields: { + ...(summary && { + summary, + }), + ...(epic && { + parent: { key: epic }, + }), + ...(type && { + issuetype: { id: type }, + }), + ...(projectId && { + project: { + id: projectId, + }, + }), + ...(reporter && { + reporter, + }), + ...(priority && priority.id && { priority }), + ...(assignee && { + assignee, + }), + ...(description && { + description, + }), + ...(labels && { + labels, + }), + ...(offsetStartDate && { + [this.customFields.get("Start date")!]: offsetStartDate, + }), + ...(offsetDueDate && { + [this.customFields.get("Due date")!]: offsetDueDate, + }), + ...(sprint && { + [this.customFields.get("Sprint")!]: sprint.id, + }), + ...(storyPointsEstimate !== undefined && { + [this.customFields.get("Story point estimate")!]: storyPointsEstimate, - }), - }, - } - ) - .then(async () => { resolve() }) + }), + }, + }) + .then(async () => { + resolve() + }) .catch((error) => { let specificError = error if (error.response) { @@ -916,15 +1057,15 @@ export class JiraServerProvider implements IProvider { .get(`/issue/${issueIdOrKey}/transitions`) .then((response) => { const transitions = new Map() - response.data.transitions.forEach((field: { name: string; id: string }) => { - transitions.set(field.name, field.id) - }) + response.data.transitions.forEach( + (field: { name: string; id: string }) => { + transitions.set(field.name, field.id) + } + ) const transitionId = +transitions.get(targetStatus)! - return this - .getRestApiClient(2) - .post( + return this.getRestApiClient(2).post( `/issue/${issueIdOrKey}/transitions`, { transition: { id: transitionId } } ) @@ -969,21 +1110,26 @@ export class JiraServerProvider implements IProvider { } } - reject(new Error(`Error in fetching the issue reporter: ${specificError}`)) + reject( + new Error(`Error in fetching the issue reporter: ${specificError}`) + ) }) }) } addCommentToIssue(issueIdOrKey: string, commentText: string): Promise { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { this.getRestApiClient(2) - .post( - `/issue/${issueIdOrKey}/comment`, - { body: commentText.replace(/\n/g, " ") } - ) + .post(`/issue/${issueIdOrKey}/comment`, { + body: commentText.replace(/\n/g, " "), + }) .then(() => resolve()) .catch((error) => { - reject(new Error(`Error adding a comment to the issue ${issueIdOrKey}: ${error}`)) + reject( + new Error( + `Error adding a comment to the issue ${issueIdOrKey}: ${error}` + ) + ) }) }) } @@ -993,27 +1139,41 @@ export class JiraServerProvider implements IProvider { commentId: string, commentText: string ): Promise { - return new Promise((resolve, reject) =>{ + return new Promise((resolve, reject) => { // main part this.getRestApiClient(2) - .put( - `/issue/${issueIdOrKey}/comment/${commentId}`, - { body: commentText.replace(/\n/g, " ") } - ) - .then(() => { resolve() }) + .put(`/issue/${issueIdOrKey}/comment/${commentId}`, { + body: commentText.replace(/\n/g, " "), + }) + .then(() => { + resolve() + }) .catch((error) => { if (error.response) { if (error.response.status === 400) { - return Promise.reject(new Error("The user does not have permission to edit the comment or the request is invalid")) - } if (error.response.status === 404) { - return Promise.reject(new Error("The issue was not found or the user does not have the necessary permissions")) + return Promise.reject( + new Error( + "The user does not have permission to edit the comment or the request is invalid" + ) + ) + } + if (error.response.status === 404) { + return Promise.reject( + new Error( + "The issue was not found or the user does not have the necessary permissions" + ) + ) } } return Promise.reject(error) }) .catch((error) => { - reject(Error(`Error editing the comment in issue ${issueIdOrKey}: ${error}`)) + reject( + Error( + `Error editing the comment in issue ${issueIdOrKey}: ${error}` + ) + ) }) }) } @@ -1022,20 +1182,37 @@ export class JiraServerProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(2) .delete(`/issue/${issueIdOrKey}/comment/${commentId}`) - .then(() => { resolve() }) + .then(() => { + resolve() + }) .catch((error) => { if (error.response) { if (error.response.status === 404) { - return Promise.reject(new Error("The issue was not found or the user does not have the necessary permissions")) - } if (error.response.status === 405) { - return Promise.reject(new Error("An anonymous call has been made to the operation")) + return Promise.reject( + new Error( + "The issue was not found or the user does not have the necessary permissions" + ) + ) + } + if (error.response.status === 405) { + return Promise.reject( + new Error("An anonymous call has been made to the operation") + ) } } - return Promise.reject(new Error(`Error deleting the comment in issue ${issueIdOrKey}: ${error}`)) + return Promise.reject( + new Error( + `Error deleting the comment in issue ${issueIdOrKey}: ${error}` + ) + ) }) .catch((error) => { - reject(Error(`Error deleting the comment in issue ${issueIdOrKey}: ${error}`)) + reject( + Error( + `Error deleting the comment in issue ${issueIdOrKey}: ${error}` + ) + ) }) }) } diff --git a/types/jira.ts b/types/jira.ts index 88e6249a..91ed8fb3 100644 --- a/types/jira.ts +++ b/types/jira.ts @@ -17,8 +17,8 @@ export interface JiraSprint { state: string name: string } - -export interface JiraIssue { +// EpicIssue structure differs from normal Issue structure +export interface JiraEpic { key: string fields: { description: string @@ -56,6 +56,76 @@ export interface JiraIssue { project: { id: string } created: string updated: string + comment: { + comments: [ + { + id: string + author: { + accountId: string + avatarUrls: { + "48x48": string + "24x24": string + "16x16": string + "32x32": string + } + displayName: string + } + body: { + type: string + version: number + content: [ + { + type: string + content: [ + { + type: string + text: string + } + ] + } + ] + } + created: string + updated: string + } + ] + } + sprint?: JiraSprint + attachment?: Attachment[] + } +} +export interface JiraIssue { + key: string + fields: { + description: string + summary: string + creator: { name: string; displayName: string } + status: { name: string } + issuetype: { name: string } + customfield_10107: number + parent: { id: string; fields: { summary: string } } + epic: { name: string } + labels: string[] + assignee: { + displayName: string + avatarUrls: { + "16x16": string + "24x24": string + "36x36": string + "48x48": string + } + } + [rankCustomField: string]: string | unknown + subtasks: { + id: string + key: string + fields: { + summary: string + } + }[] + project: { id: string } + created: string + updated: string comment: { comments: [ { From 5cc757b334ecadc0a703ac8cd1fccc50f402273f Mon Sep 17 00:00:00 2001 From: ayman <118556179+aymka@users.noreply.github.com> Date: Mon, 11 Dec 2023 18:46:15 +0100 Subject: [PATCH 03/12] commentSection implemented - edit/delete/add may take long(needs fixing) --- types/jira.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/types/jira.ts b/types/jira.ts index 91ed8fb3..45fe5eac 100644 --- a/types/jira.ts +++ b/types/jira.ts @@ -26,12 +26,6 @@ export interface JiraEpic { creator: { name: string; displayName: string } status: { name: string } issuetype: { name: string } - // TODO: improve this, let's try not to: - // -hardcode customfields - // -not use | unknown if possible. - // the problem is: change the LHS name of these props in the fields definition - // based on the mapped fields (this.customFields), - // it might change based on the jira instance customfield_10107: number parent: { id: string; fields: { summary: string } } epic: { name: string } From 875e2f62bb87c1a31cce57ed6926655c56727ba0 Mon Sep 17 00:00:00 2001 From: ayman <118556179+aymka@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:16:53 +0100 Subject: [PATCH 04/12] commentSection ServerSide works fine now - but still slow loading time --- .../jira-server-provider/JiraServerProvider.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/electron/providers/jira-server-provider/JiraServerProvider.ts b/electron/providers/jira-server-provider/JiraServerProvider.ts index 47c78e74..9c01c49b 100644 --- a/electron/providers/jira-server-provider/JiraServerProvider.ts +++ b/electron/providers/jira-server-provider/JiraServerProvider.ts @@ -668,7 +668,7 @@ export class JiraServerProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(2) .get( - `search?jql=issuetype = Epic AND project = ${projectIdOrKey}&fields=*all}` + `search?jql=issuetype = Epic AND project = ${projectIdOrKey}&fields=*all` ) .then(async (response) => { const epics: Promise = Promise.all( @@ -680,23 +680,11 @@ export class JiraServerProvider implements IProvider { displayName: element.fields.assignee?.displayName, avatarUrls: element.fields.assignee?.avatarUrls, }, - subtasks: element.fields.subtasks, created: element.fields.created, updated: element.fields.updated, - comment: { - comments: element.fields.comment.comments.map((coelem) => ({ - id: coelem.id, - body: coelem.body.content[0].content[0].text, - author: coelem.author, - created: coelem.created, - updated: coelem.updated, - })), - } ?? { + comment: element.fields.comment ?? { comments: [], }, - projectId: element.fields.project.id, - sprint: element.fields.sprint, - attachments: element.fields.attachment, })) ) resolve(epics) From 55dc908c3c21577e2b6056001784c2386ee30053 Mon Sep 17 00:00:00 2001 From: Benedict Teutsch Date: Wed, 13 Dec 2023 16:23:02 +0100 Subject: [PATCH 05/12] added epic query invalidation for comment editing in epic detail view --- src/components/DetailView/Components/CommentSection/queries.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/DetailView/Components/CommentSection/queries.ts b/src/components/DetailView/Components/CommentSection/queries.ts index c827b7ed..73778a6f 100644 --- a/src/components/DetailView/Components/CommentSection/queries.ts +++ b/src/components/DetailView/Components/CommentSection/queries.ts @@ -27,6 +27,7 @@ export const addCommentMutation = (queryClient: QueryClient) => color: "green", }) queryClient.invalidateQueries({ queryKey: ["issues"] }) + queryClient.invalidateQueries({queryKey: ["epics"]}) }, }) @@ -53,6 +54,7 @@ export const editCommentMutation = (queryClient: QueryClient) => color: "green", }) queryClient.invalidateQueries({ queryKey: ["issues"] }) + queryClient.invalidateQueries({queryKey: ["epics"]}) }, }) @@ -77,5 +79,6 @@ export const deleteCommentMutation = (queryClient: QueryClient) => color: "green", }) queryClient.invalidateQueries({ queryKey: ["issues"] }) + queryClient.invalidateQueries({queryKey: ["epics"]}) }, }) From 10b6b3aa56bd09a75b62072a2cf3bbf0933f06b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Mon, 18 Dec 2023 18:38:34 +0100 Subject: [PATCH 06/12] Revert "commentSection ServerSide works fine now - but still slow loading time" This reverts commit 875e2f62bb87c1a31cce57ed6926655c56727ba0. --- .../jira-server-provider/JiraServerProvider.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/electron/providers/jira-server-provider/JiraServerProvider.ts b/electron/providers/jira-server-provider/JiraServerProvider.ts index 9c01c49b..47c78e74 100644 --- a/electron/providers/jira-server-provider/JiraServerProvider.ts +++ b/electron/providers/jira-server-provider/JiraServerProvider.ts @@ -668,7 +668,7 @@ export class JiraServerProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(2) .get( - `search?jql=issuetype = Epic AND project = ${projectIdOrKey}&fields=*all` + `search?jql=issuetype = Epic AND project = ${projectIdOrKey}&fields=*all}` ) .then(async (response) => { const epics: Promise = Promise.all( @@ -680,11 +680,23 @@ export class JiraServerProvider implements IProvider { displayName: element.fields.assignee?.displayName, avatarUrls: element.fields.assignee?.avatarUrls, }, + subtasks: element.fields.subtasks, created: element.fields.created, updated: element.fields.updated, - comment: element.fields.comment ?? { + comment: { + comments: element.fields.comment.comments.map((coelem) => ({ + id: coelem.id, + body: coelem.body.content[0].content[0].text, + author: coelem.author, + created: coelem.created, + updated: coelem.updated, + })), + } ?? { comments: [], }, + projectId: element.fields.project.id, + sprint: element.fields.sprint, + attachments: element.fields.attachment, })) ) resolve(epics) From 55173de065194a43203e19b06da9d48b8843060e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Mon, 18 Dec 2023 18:38:39 +0100 Subject: [PATCH 07/12] Revert "commentSection implemented - edit/delete/add may take long(needs fixing)" This reverts commit fdd357544a3d40cdfd2441925cad17195c33003e. --- .../jira-cloud-provider/JiraCloudProvider.ts | 701 ++++++--------- .../JiraServerProvider.ts | 817 +++++++----------- types/jira.ts | 72 +- 3 files changed, 609 insertions(+), 981 deletions(-) diff --git a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts index 638a6dfc..e837ec4a 100644 --- a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts +++ b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts @@ -1,5 +1,5 @@ /* eslint-disable class-methods-use-this */ -import axios, { AxiosError, AxiosResponse, isAxiosError } from "axios" +import axios, { AxiosError, AxiosResponse, isAxiosError } from "axios"; import { dateTimeFormat, Issue, @@ -12,7 +12,6 @@ import { User, } from "../../../types" import { - JiraEpic, JiraIssue, JiraIssueType, JiraPriority, @@ -21,7 +20,7 @@ import { } from "../../../types/jira" import { IProvider } from "../base-provider" import { getAccessToken, refreshTokens } from "./getAccessToken" -import { JiraCloudUser } from "./cloud-types" +import {JiraCloudUser} from "./cloud-types"; export class JiraCloudProvider implements IProvider { public accessToken: string | undefined @@ -44,14 +43,13 @@ export class JiraCloudProvider implements IProvider { }, }) - const recreateAxiosError = (originalError: AxiosError, message: string) => - new AxiosError( - message, - originalError.code, - originalError.config, - originalError.request, - originalError.response - ) + const recreateAxiosError = (originalError: AxiosError, message: string) => new AxiosError( + message, + originalError.code, + originalError.config, + originalError.request, + originalError.response + ) instance.interceptors.response.use( (response) => response, @@ -59,38 +57,13 @@ export class JiraCloudProvider implements IProvider { if (isAxiosError(error) && error.response) { const statusCode = error.response.status if (statusCode === 400) { - return Promise.reject( - recreateAxiosError( - error, - `Invalid request: ${JSON.stringify(error.response.data)}` - ) - ) - } - if (statusCode === 401) { - return Promise.reject( - recreateAxiosError( - error, - `User not authenticated: ${JSON.stringify(error.response.data)}` - ) - ) - } - if (error.response.status === 403) { - return Promise.reject( - recreateAxiosError( - error, - `User does not have a valid licence: ${JSON.stringify( - error.response.data - )}` - ) - ) - } - if (error.response.status === 429) { - return Promise.reject( - recreateAxiosError( - error, - `Rate limit exceeded: ${JSON.stringify(error.response.data)}` - ) - ) + return Promise.reject(recreateAxiosError(error, `Invalid request: ${JSON.stringify(error.response.data)}`)) + } if (statusCode === 401) { + return Promise.reject(recreateAxiosError(error, `User not authenticated: ${JSON.stringify(error.response.data)}`)) + } if (error.response.status === 403) { + return Promise.reject(recreateAxiosError(error, `User does not have a valid licence: ${JSON.stringify(error.response.data)}`)) + } if (error.response.status === 429) { + return Promise.reject(recreateAxiosError(error, `Rate limit exceeded: ${JSON.stringify(error.response.data)}`)) } } @@ -102,11 +75,11 @@ export class JiraCloudProvider implements IProvider { } private getRestApiClient(version: number) { - return this.constructRestBasedClient("api", version.toString()) + return this.constructRestBasedClient('api', version.toString()); } private getAgileRestApiClient(version: string) { - return this.constructRestBasedClient("agile", version) + return this.constructRestBasedClient('agile', version); } offsetDate(date: Date) { @@ -195,7 +168,7 @@ export class JiraCloudProvider implements IProvider { async mapCustomFields(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .get("/field") + .get('/field') .then(async (response) => { response.data.forEach((field: { name: string; id: string }) => { this.customFields.set(field.name, field.id) @@ -212,9 +185,7 @@ export class JiraCloudProvider implements IProvider { async getProjects(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .get( - "/project/search?expand=description,lead,issueTypes,url,projectKeys,permissions,insight" - ) + .get('/project/search?expand=description,lead,issueTypes,url,projectKeys,permissions,insight') .then(async (response) => { const projects = response.data.values.map((project: JiraProject) => ({ key: project.key, @@ -229,9 +200,7 @@ export class JiraCloudProvider implements IProvider { let specificError = error if (error.response) { if (error.response.status === 404) { - specificError = new Error( - `No projects matching the search criteria were found: ${error.response.data}` - ) + specificError = new Error(`No projects matching the search criteria were found: ${error.response.data}`) } } @@ -258,9 +227,7 @@ export class JiraCloudProvider implements IProvider { } } - reject( - new Error(`Error in fetching the issue types: ${specificError}`) - ) + reject(new Error(`Error in fetching the issue types: ${specificError}`)) }) }) } @@ -268,7 +235,7 @@ export class JiraCloudProvider implements IProvider { async getIssueTypesWithFieldsMap(): Promise<{ [key: string]: string[] }> { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .get("/issue/createmeta?expand=projects.issuetypes.fields") + .get('/issue/createmeta?expand=projects.issuetypes.fields') .then(async (response) => { const issueTypeToFieldsMap: { [key: string]: string[] } = {} response.data.projects.forEach( @@ -316,16 +283,13 @@ export class JiraCloudProvider implements IProvider { this.getRestApiClient(3) .get(`/user/assignable/search?project=${projectIdOrKey}`) .then(async (response) => { - const users = response.data.map( - (cloudUser: JiraCloudUser) => - ({ - id: cloudUser.accountId, - name: cloudUser.name, - avatarUrls: cloudUser.avatarUrls, - displayName: cloudUser.displayName, - emailAddress: cloudUser.emailAddress, - } as User) - ) + const users = response.data.map((cloudUser: JiraCloudUser) => ({ + id: cloudUser.accountId, + name: cloudUser.name, + avatarUrls: cloudUser.avatarUrls, + displayName: cloudUser.displayName, + emailAddress: cloudUser.emailAddress, + } as User)) resolve(users as User[]) }) @@ -333,17 +297,11 @@ export class JiraCloudProvider implements IProvider { let specificError = error if (error.response) { if (error.response.status === 404) { - specificError = new Error( - `Project, issue, or transition were not found: ${error.response.data}` - ) + specificError = new Error(`Project, issue, or transition were not found: ${error.response.data}`) } } - reject( - new Error( - `Error in fetching the assignable users for the project ${projectIdOrKey}: ${specificError}` - ) - ) + reject(new Error(`Error in fetching the assignable users for the project ${projectIdOrKey}: ${specificError}`)) }) }) } @@ -351,7 +309,7 @@ export class JiraCloudProvider implements IProvider { async getCurrentUser(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .get("/myself") + .get('/myself') .then(async (response: AxiosResponse) => { resolve({ id: response.data.accountId, @@ -384,16 +342,14 @@ export class JiraCloudProvider implements IProvider { } } - reject( - new Error(`Error in fetching the issue reporter: ${specificError}`) - ) + reject(new Error(`Error in fetching the issue reporter: ${specificError}`)) }) }) } async getBoardIds(project: string): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient("1.0") + this.getAgileRestApiClient('1.0') .get(`/board?projectKeyOrId=${project}`) .then(async (response) => { const boardIds: number[] = response.data.values.map( @@ -409,11 +365,13 @@ export class JiraCloudProvider implements IProvider { async getSprints(boardId: number): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient("1.0") + this.getAgileRestApiClient('1.0') .get(`/board/${boardId}/sprint`) .then(async (response) => { const sprints: Sprint[] = response.data.values - .filter((element: { state: string }) => element.state !== "closed") + .filter( + (element: { state: string }) => element.state !== "closed" + ) .map((element: JiraSprint) => { const sDate = new Date(element.startDate) const startDate = Number.isNaN(sDate.getTime()) @@ -456,32 +414,20 @@ export class JiraCloudProvider implements IProvider { resolve(this.fetchIssues(response)) }) .catch((error) => { - reject( - new Error( - `Error fetching issues by project: ${this.handleFetchIssuesError( - error - )}` - ) - ) + reject(new Error(`Error fetching issues by project: ${this.handleFetchIssuesError(error)}`)) }) }) } async getIssuesBySprint(sprintId: number): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient("1.0") + this.getAgileRestApiClient('1.0') .get(`/sprint/${sprintId}/issue`) .then(async (response) => { resolve(this.fetchIssues(response)) }) .catch((error) => { - reject( - new Error( - `Error fetching issues by sprint: ${this.handleFetchIssuesError( - error - )}` - ) - ) + reject(new Error(`Error fetching issues by sprint: ${this.handleFetchIssuesError(error)}`)) }) }) } @@ -491,19 +437,13 @@ export class JiraCloudProvider implements IProvider { boardId: number ): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient("1.0") + this.getAgileRestApiClient('1.0') .get(`/board/${boardId}/backlog?jql=project=${project}&maxResults=500`) .then(async (response) => { resolve(this.fetchIssues(response)) }) .catch((error) => { - reject( - new Error( - `Error fetching issues by project: ${this.handleFetchIssuesError( - error - )}` - ) - ) + reject(new Error(`Error fetching issues by project: ${this.handleFetchIssuesError(error)}`)) }) }) } @@ -518,9 +458,7 @@ export class JiraCloudProvider implements IProvider { creator: element.fields.creator.displayName, status: element.fields.status.name, type: element.fields.issuetype.name, - storyPointsEstimate: await this.getIssueStoryPointsEstimate( - element.key - ), + storyPointsEstimate: await this.getIssueStoryPointsEstimate(element.key), epic: element.fields.parent?.fields.summary, labels: element.fields.labels, assignee: { @@ -544,7 +482,7 @@ export class JiraCloudProvider implements IProvider { handleFetchIssuesError(error: AxiosError): Error { if (!error.response) { - return error + return error; } if (error.response.status === 404) { @@ -553,7 +491,7 @@ export class JiraCloudProvider implements IProvider { ) } - return error + return error; } async moveIssueToSprintAndRank( @@ -564,16 +502,17 @@ export class JiraCloudProvider implements IProvider { ): Promise { return new Promise((resolve, reject) => { const rankCustomField = this.customFields.get("Rank") - this.getAgileRestApiClient("1.0") - .post(`/sprint/${sprint}/issue`, { - rankCustomFieldId: rankCustomField!.match(/_(\d+)/)![1], - issues: [issue], - ...(rankAfter ? { rankAfterIssue: rankAfter } : {}), - ...(rankBefore ? { rankBeforeIssue: rankBefore } : {}), - }) - .then(async () => { - resolve() - }) + this.getAgileRestApiClient('1.0') + .post( + `/sprint/${sprint}/issue`, + { + rankCustomFieldId: rankCustomField!.match(/_(\d+)/)![1], + issues: [issue], + ...(rankAfter ? { rankAfterIssue: rankAfter } : {}), + ...(rankBefore ? { rankBeforeIssue: rankBefore } : {}), + } + ) + .then(async () => { resolve() }) .catch((error) => { let specificError = error if (error.response) { @@ -588,22 +527,19 @@ export class JiraCloudProvider implements IProvider { } } - reject( - new Error( - `Error in moving this issue to the Sprint with id ${sprint}: ${specificError}` - ) - ) + reject(new Error(`Error in moving this issue to the Sprint with id ${sprint}: ${specificError}`)) }) }) } async moveIssueToBacklog(issue: string): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient("1.0") - .post("/backlog/issue", { issues: [issue] }) - .then(async () => { - resolve() - }) + this.getAgileRestApiClient('1.0') + .post( + '/backlog/issue', + { issues: [issue] } + ) + .then(async () => { resolve() }) .catch((error) => { let specificError = error if (error.response) { @@ -618,11 +554,7 @@ export class JiraCloudProvider implements IProvider { } } - reject( - new Error( - `Error in moving this issue to the backlog: ${specificError}` - ) - ) + reject(new Error(`Error in moving this issue to the backlog: ${specificError}`)) }) }) } @@ -650,8 +582,8 @@ export class JiraCloudProvider implements IProvider { body.rankAfterIssue = rankAfter } - this.getAgileRestApiClient("1.0") - .put("/issue/rank", body) + this.getAgileRestApiClient('1.0') + .put('/issue/rank', body) .then(async (response) => { if (response.status === 204) { resolve() @@ -671,11 +603,7 @@ export class JiraCloudProvider implements IProvider { } } - reject( - new Error( - `Error in ranking this issue in the backlog: ${specificError}` - ) - ) + reject(new Error(`Error in ranking this issue in the backlog: ${specificError}`)) }) }) } @@ -699,11 +627,7 @@ export class JiraCloudProvider implements IProvider { } } - reject( - new Error( - `Error in getting the story points for issue: ${issue}: ${specificError}` - ) - ) + reject(new Error(`Error in getting the story points for issue: ${issue}: ${specificError}`)) }) }) } @@ -729,58 +653,61 @@ export class JiraCloudProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .post(`/issue`, { - fields: { - summary, - parent: { key: epic }, - issuetype: { id: type }, - project: { - id: projectId, - }, - reporter: { - id: reporter.id, - }, - ...(priority.id && { priority }), - ...(assignee && { - assignee: { - id: assignee.id, + .post( + `/issue`, + { + fields: { + summary, + parent: { key: epic }, + issuetype: { id: type }, + project: { + id: projectId, }, - }), - description: { - type: "doc", - version: 1, - content: [ - { - type: "paragraph", - content: [ - { - text: description, - type: "text", - }, - ], - }, - ], - }, - labels, - ...(offsetStartDate && { - [this.customFields.get("Start date")!]: offsetStartDate, - }), - ...(offsetDueDate && { - [this.customFields.get("Due date")!]: offsetDueDate, - }), - ...(sprint && - sprint.id && { - [this.customFields.get("Sprint")!]: +sprint.id, + reporter: { + id: reporter.id, + }, + ...(priority.id && { priority }), + ...(assignee && { + assignee: { + id: assignee.id, + } + }), + description: { + type: "doc", + version: 1, + content: [ + { + type: "paragraph", + content: [ + { + text: description, + type: "text", + }, + ], + }, + ], + }, + labels, + ...(offsetStartDate && { + [this.customFields.get("Start date")!]: offsetStartDate, }), - ...(storyPointsEstimate && { - [this.customFields.get("Story point estimate")!]: + ...(offsetDueDate && { + [this.customFields.get("Due date")!]: offsetDueDate, + }), + ...(sprint && + sprint.id && { + [this.customFields.get("Sprint")!]: +sprint.id, + }), + ...(storyPointsEstimate && { + [this.customFields.get("Story point estimate")!]: storyPointsEstimate, - }), - // ...(files && { - // [this.customFields.get("Attachment")!]: files, - // }), - }, - }) + }), + // ...(files && { + // [this.customFields.get("Attachment")!]: files, + // }), + }, + } + ) .then(async (response) => { const createdIssue = response.data resolve(JSON.stringify(createdIssue.key)) @@ -790,9 +717,7 @@ export class JiraCloudProvider implements IProvider { let specificError = error if (error.response) { if (error.response.status === 404) { - specificError = new Error( - "The user does not have the necessary permissions" - ) + specificError = new Error("The user does not have the necessary permissions") } } @@ -824,66 +749,67 @@ export class JiraCloudProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .put(`/issue/${issueIdOrKey}`, { - fields: { - ...(summary && { - summary, - }), - ...(epic && { - parent: { key: epic }, - }), - ...(type && { - issuetype: { id: type }, - }), - ...(projectId && { - project: { - id: projectId, - }, - }), - ...(reporter && { reporter }), - ...(priority && priority.id && { priority }), - ...(assignee && - assignee.id && { - assignee, + .put( + `/issue/${issueIdOrKey}`, + { + fields: { + ...(summary && { + summary, }), - ...(description && { - description: { - type: "doc", - version: 1, - content: [ - { - type: "paragraph", - content: [ - { - text: description, - type: "text", - }, - ], - }, - ], - }, - }), - ...(labels && { - labels, - }), - ...(offsetStartDate && { - [this.customFields.get("Start date")!]: offsetStartDate, - }), - ...(offsetDueDate && { - [this.customFields.get("Due date")!]: offsetDueDate, - }), - ...(sprint && { - [this.customFields.get("Sprint")!]: sprint.id, - }), - ...(storyPointsEstimate !== undefined && { - [this.customFields.get("Story point estimate")!]: + ...(epic && { + parent: { key: epic }, + }), + ...(type && { + issuetype: { id: type }, + }), + ...(projectId && { + project: { + id: projectId, + }, + }), + ...(reporter && { reporter }), + ...(priority && priority.id && { priority }), + ...(assignee && + assignee.id && { + assignee, + }), + ...(description && { + description: { + type: "doc", + version: 1, + content: [ + { + type: "paragraph", + content: [ + { + text: description, + type: "text", + }, + ], + }, + ], + }, + }), + ...(labels && { + labels, + }), + ...(offsetStartDate && { + [this.customFields.get("Start date")!]: offsetStartDate, + }), + ...(offsetDueDate && { + [this.customFields.get("Due date")!]: offsetDueDate, + }), + ...(sprint && { + [this.customFields.get("Sprint")!]: sprint.id, + }), + ...(storyPointsEstimate !== undefined && { + [this.customFields.get("Story point estimate")!]: storyPointsEstimate, - }), - }, - }) - .then(async () => { - resolve() - }) + }), + }, + } + ) + .then(async () => { resolve() }) .catch((error) => { let specificError = error if (error.response) { @@ -902,30 +828,29 @@ export class JiraCloudProvider implements IProvider { async setTransition(issueKey: string, status: string): Promise { const transitions = new Map() const transitionResponse = await this.getRestApiClient(3).get( - `/issue/${issueKey}/transitions` + `/issue/${issueKey}/transitions`, ) - const { data } = transitionResponse + const {data} = transitionResponse data.transitions.forEach((field: { name: string; id: string }) => { transitions.set(field.name, field.id) }) const transitionId = +transitions.get(status)! - this.getRestApiClient(3).post(`/issue/${issueKey}/transitions`, { - transition: { id: transitionId }, - }) + this.getRestApiClient(3).post( + `/issue/${issueKey}/transitions`, + { transition: { id: transitionId } } + ) } async getEpicsByProject(projectIdOrKey: string): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .get( - `search?jql=issuetype = Epic AND project = ${projectIdOrKey}&fields=*all` - ) + .get(`search?jql=issuetype = Epic AND project = ${projectIdOrKey}`) .then(async (response) => { const epics: Promise = Promise.all( - response.data.issues.map(async (element: JiraEpic) => ({ + response.data.issues.map(async (element: JiraIssue) => ({ issueKey: element.key, summary: element.fields.summary, labels: element.fields.labels, @@ -936,15 +861,7 @@ export class JiraCloudProvider implements IProvider { subtasks: element.fields.subtasks, created: element.fields.created, updated: element.fields.updated, - comment: { - comments: element.fields.comment.comments.map((coelem) => ({ - id: coelem.id, - body: coelem.body.content[0].content[0].text, - author: coelem.author, - created: coelem.created, - updated: coelem.updated, - })), - } ?? { + comment: element.fields.comment ?? { comments: [], }, projectId: element.fields.project.id, @@ -964,11 +881,7 @@ export class JiraCloudProvider implements IProvider { } } - reject( - new Error( - `Error in fetching the epics for the project ${projectIdOrKey}: ${specificError}` - ) - ) + reject(new Error(`Error in fetching the epics for the project ${projectIdOrKey}: ${specificError}`)) }) }) } @@ -976,7 +889,7 @@ export class JiraCloudProvider implements IProvider { async getLabels(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .get("/label") + .get('/label') .then(async (response) => { resolve(response.data.values) }) @@ -991,7 +904,7 @@ export class JiraCloudProvider implements IProvider { // and GET /rest/api/3/priority/search is experimental return new Promise((resolve, reject) => { this.getRestApiClient(3) - .get("/priority/search") + .get('/priority/search') .then(async (response) => { const priorityData: JiraPriority = response.data resolve(priorityData.values) @@ -1008,41 +921,36 @@ export class JiraCloudProvider implements IProvider { ): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .post(`/issue/${issueIdOrKey}/comment`, { - body: { - content: [ - { - content: [ - { - text: commentText.replace(/\n/g, " "), - type: "text", - }, - ], - type: "paragraph", - }, - ], - type: "doc", - version: 1, - }, - }) - .then(async () => { - resolve() - }) + .post( + `/issue/${issueIdOrKey}/comment`, + { + body: { + content: [ + { + content: [ + { + text: commentText.replace(/\n/g, " "), + type: "text" + } + ], + type: "paragraph" + } + ], + type: "doc", + version: 1 + } + } + ) + .then(async () => { resolve() }) .catch((error) => { let specificError = error if (error.response) { if (error.response.status === 404) { - specificError = new Error( - "The issue was not found or the user does not have the necessary permissions" - ) + specificError = new Error("The issue was not found or the user does not have the necessary permissions") } } - reject( - new Error( - `Error adding a comment to the issue ${issueIdOrKey}: ${specificError}` - ) - ) + reject(new Error(`Error adding a comment to the issue ${issueIdOrKey}: ${specificError}`)) }) }) } @@ -1054,45 +962,38 @@ export class JiraCloudProvider implements IProvider { ): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .put(`/issue/${issueIdOrKey}/comment/${commentId}`, { - body: { - content: [ - { - content: [ - { - text: commentText.replace(/\n/g, " "), - type: "text", - }, - ], - type: "paragraph", - }, - ], - type: "doc", - version: 1, - }, - }) - .then(async () => { - resolve() - }) + .put( + `/issue/${issueIdOrKey}/comment/${commentId}`, + { + body: { + content: [ + { + content: [ + { + text: commentText.replace(/\n/g, " "), + type: "text" + } + ], + type: "paragraph" + } + ], + type: "doc", + version: 1 + } + } + ) + .then(async () => { resolve() }) .catch((error) => { let specificError = error if (error.response) { if (error.response.status === 400) { - specificError = new Error( - "The user does not have permission to edit the comment or the request is invalid" - ) + specificError = new Error("The user does not have permission to edit the comment or the request is invalid") } else if (error.response.status === 404) { - specificError = new Error( - "The issue was not found or the user does not have the necessary permissions" - ) + specificError = new Error("The issue was not found or the user does not have the necessary permissions") } } - reject( - new Error( - `Error editing the comment in issue ${issueIdOrKey}: ${specificError}` - ) - ) + reject(new Error(`Error editing the comment in issue ${issueIdOrKey}: ${specificError}`)) }) }) } @@ -1101,32 +1002,20 @@ export class JiraCloudProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(3) .delete(`/issue/${issueIdOrKey}/comment/${commentId}`) - .then(async () => { - resolve() - }) + .then(async () => { resolve() }) .catch((error) => { let specificError = error if (error.response) { if (error.response.status === 400) { - specificError = new Error( - "The user does not have permission to delete the comment" - ) + specificError = new Error("The user does not have permission to delete the comment") } else if (error.response.status === 404) { - specificError = new Error( - "The issue was not found or the user does not have the necessary permissions" - ) + specificError = new Error("The issue was not found or the user does not have the necessary permissions") } else if (error.response.status === 405) { - specificError = new Error( - "An anonymous call has been made to the operation" - ) + specificError = new Error("An anonymous call has been made to the operation") } } - reject( - new Error( - `Error deleting the comment in issue ${issueIdOrKey}: ${specificError}` - ) - ) + reject(new Error(`Error deleting the comment in issue ${issueIdOrKey}: ${specificError}`)) }) }) } @@ -1135,36 +1024,22 @@ export class JiraCloudProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(2) .delete(`/issue/${issueIdOrKey}`) - .then(async () => { - resolve() - }) + .then(async () => { resolve() }) .catch((error) => { let specificError = error if (error.response) { if (error.response.status === 400) { - specificError = new Error( - "The issue has subtasks and deleteSubtasks is not set to true" - ) + specificError = new Error("The issue has subtasks and deleteSubtasks is not set to true") } else if (error.response.status === 403) { - specificError = new Error( - "The user does not have permission to delete the issue" - ) + specificError = new Error("The user does not have permission to delete the issue") } else if (error.response.status === 404) { - specificError = new Error( - "The issue was not found or the user does not have the necessary permissions" - ) + specificError = new Error("The issue was not found or the user does not have the necessary permissions") } else if (error.response.status === 405) { - specificError = new Error( - "An anonymous call has been made to the operation" - ) + specificError = new Error("An anonymous call has been made to the operation") } } - reject( - new Error( - `Error deleting the subtask ${issueIdOrKey}: ${specificError}` - ) - ) + reject(new Error(`Error deleting the subtask ${issueIdOrKey}: ${specificError}`)) }) }) } @@ -1177,20 +1052,23 @@ export class JiraCloudProvider implements IProvider { ): Promise<{ id: string; key: string }> { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .post("/issue", { - fields: { - summary: subtaskSummary, - issuetype: { - id: subtaskIssueTypeId, - }, - parent: { - key: parentIssueKey, - }, - project: { - id: projectId, + .post( + '/issue', + { + fields: { + summary: subtaskSummary, + issuetype: { + id: subtaskIssueTypeId, + }, + parent: { + key: parentIssueKey, + }, + project: { + id: projectId, + }, }, - }, - }) + } + ) .then(async (response) => { const createdSubtask: { id: string; key: string } = response.data resolve(createdSubtask) @@ -1205,9 +1083,9 @@ export class JiraCloudProvider implements IProvider { return new Promise((resolve, reject) => { if (this.accessToken !== undefined) { // IMPROVE expose API client instead of resource - const { defaults } = this.getRestApiClient(3) + const {defaults} = this.getRestApiClient(3) const result: Resource = { - baseUrl: defaults.baseURL ?? "", + baseUrl: defaults.baseURL ?? '', authorization: defaults.headers.Authorization as string, } resolve(result) @@ -1228,32 +1106,29 @@ export class JiraCloudProvider implements IProvider { const offsetEndDate = this.offsetDate(endDate) return new Promise((resolve, reject) => { - this.getAgileRestApiClient("1.0") - .post("/sprint", { - name, - originBoardId, - ...(offsetStartDate && { - startDate: offsetStartDate, - }), - ...(offsetEndDate && { - endDate: offsetEndDate, - }), - ...(goal && { goal }), - }) - .then(async () => { - resolve() - }) + this.getAgileRestApiClient('1.0') + .post( + '/sprint', + { + name, + originBoardId, + ...(offsetStartDate && { + startDate: offsetStartDate, + }), + ...(offsetEndDate && { + endDate: offsetEndDate, + }), + ...(goal && { goal }), + } + ) + .then(async () => { resolve() }) .catch((error) => { let specificError = error if (error.response) { if (error.response.status === 403) { - specificError = new Error( - "The user does not have the necessary permissions" - ) + specificError = new Error("The user does not have the necessary permissions") } else if (error.response.status === 404) { - specificError = new Error( - "The Board does not exist or the user does not have the necessary permissions to view it" - ) + specificError = new Error("The Board does not exist or the user does not have the necessary permissions to view it") } } diff --git a/electron/providers/jira-server-provider/JiraServerProvider.ts b/electron/providers/jira-server-provider/JiraServerProvider.ts index 47c78e74..402c4668 100644 --- a/electron/providers/jira-server-provider/JiraServerProvider.ts +++ b/electron/providers/jira-server-provider/JiraServerProvider.ts @@ -1,5 +1,5 @@ /* eslint-disable class-methods-use-this */ -import axios, { AxiosError, AxiosResponse, isAxiosError } from "axios" +import axios, {AxiosError, AxiosResponse, isAxiosError} from "axios"; import { dateTimeFormat, Issue, @@ -11,15 +11,9 @@ import { SprintCreate, User, } from "../../../types" -import { - JiraEpic, - JiraIssue, - JiraIssueType, - JiraProject, - JiraSprint, -} from "../../../types/jira" -import { IProvider } from "../base-provider" -import { JiraServerInfo, JiraServerUser } from "./server-types" +import {JiraIssue, JiraIssueType, JiraProject, JiraSprint,} from "../../../types/jira" +import {IProvider} from "../base-provider" +import {JiraServerInfo, JiraServerUser} from "./server-types"; export class JiraServerProvider implements IProvider { private loginOptions = { @@ -34,58 +28,44 @@ export class JiraServerProvider implements IProvider { private reversedCustomFields = new Map() - private executeVersioned( - functionsByVersionMatcher: { - [versionMatcher: string]: (...args: never[]) => R - }, - ...args: never[] - ) { + private executeVersioned(functionsByVersionMatcher: { [versionMatcher: string]: (...args: never[]) => R }, ...args: never[]) { if (!this.serverInfo) { - throw new Error("Server info not set!") + throw new Error('Server info not set!') } const matches = (matcher: string): boolean => { let match = true - matcher.split(".").forEach((matcherPart, index) => { - match = - match && - (matcherPart === "*" || - matcherPart === this.serverInfo!.versionNumbers[index].toString()) + matcher.split('.').forEach((matcherPart, index) => { + match = match && ( + matcherPart === '*' + || matcherPart === this.serverInfo!.versionNumbers[index].toString() + ) }) return match } - const isAMoreSpecificThanB = ( - matcherA: string, - matcherB: string - ): boolean => { - const matcherBParts = matcherB.split(".") - let isMoreSpecific = false - matcherA.split(".").forEach((matcherAPart, index) => { - if (matcherBParts[index] === "*" && matcherAPart !== "*") { - isMoreSpecific = true + const isAMoreSpecificThanB = (matcherA: string, matcherB: string): boolean => { + const matcherBParts = matcherB.split('.') + let isMoreSpecific = false; + matcherA.split('.').forEach((matcherAPart, index) => { + if (matcherBParts[index] === '*' && matcherAPart !== '*') { + isMoreSpecific = true; } }) - return isMoreSpecific + return isMoreSpecific; } let selectedMatcher: string | undefined Object.keys(functionsByVersionMatcher).forEach((matcher) => { - if ( - matches(matcher) && - (selectedMatcher === undefined || - isAMoreSpecificThanB(matcher, selectedMatcher)) - ) { + if (matches(matcher) && (selectedMatcher === undefined || isAMoreSpecificThanB(matcher, selectedMatcher))) { selectedMatcher = matcher } }) if (!selectedMatcher) { - throw new Error( - `No version matcher found for version: ${this.serverInfo.version}` - ) + throw new Error(`No version matcher found for version: ${this.serverInfo.version}`) } return functionsByVersionMatcher[selectedMatcher](...args) @@ -107,73 +87,47 @@ export class JiraServerProvider implements IProvider { }, }) - const recreateAxiosError = (originalError: AxiosError, message: string) => - new AxiosError( + const recreateAxiosError = (originalError: AxiosError, message: string) => new AxiosError( message, originalError.code, originalError.config, originalError.request, originalError.response - ) + ) instance.interceptors.response.use( - (response) => response, - (error) => { - if (isAxiosError(error) && error.response) { - const statusCode = error.response.status - if (statusCode === 400) { - return Promise.reject( - recreateAxiosError( - error, - `Invalid request: ${JSON.stringify(error.response.data)}` - ) - ) - } - if (statusCode === 401) { - return Promise.reject( - recreateAxiosError( - error, - `User not authenticated: ${JSON.stringify(error.response.data)}` - ) - ) - } - if (error.response.status === 403) { - return Promise.reject( - recreateAxiosError( - error, - `User does not have a valid licence: ${JSON.stringify( - error.response.data - )}` - ) - ) - } - if (error.response.status === 429) { - return Promise.reject( - recreateAxiosError( - error, - `Rate limit exceeded: ${JSON.stringify(error.response.data)}` - ) - ) + (response) => response, + (error) => { + if (isAxiosError(error) && error.response) { + const statusCode = error.response.status + if (statusCode === 400) { + return Promise.reject(recreateAxiosError(error, `Invalid request: ${JSON.stringify(error.response.data)}`)) + } if (statusCode === 401) { + return Promise.reject(recreateAxiosError(error, `User not authenticated: ${JSON.stringify(error.response.data)}`)) + } if (error.response.status === 403) { + return Promise.reject(recreateAxiosError(error, `User does not have a valid licence: ${JSON.stringify(error.response.data)}`)) + } if (error.response.status === 429) { + return Promise.reject(recreateAxiosError(error, `Rate limit exceeded: ${JSON.stringify(error.response.data)}`)) + } } - } - return Promise.reject(error) - } + return Promise.reject(error) + } ) return instance } - private getRestApiClient(version: string | number) { - return this.constructRestBasedClient("api", version.toString()) + private getRestApiClient(version: string|number) { + return this.constructRestBasedClient('api', version.toString()); } private getAuthRestApiClient(version: number) { - return this.constructRestBasedClient("auth", version.toString()) + return this.constructRestBasedClient('auth', version.toString()); } private getAgileRestApiClient(version: string) { - return this.constructRestBasedClient("agile", version) + return this.constructRestBasedClient('agile', version); } async login({ @@ -197,15 +151,13 @@ export class JiraServerProvider implements IProvider { async getServerInfo(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(2) - .get("/serverInfo") + .get('/serverInfo') .then((response: AxiosResponse) => { this.serverInfo = response.data if (this.serverInfo.versionNumbers[0] < 7) { - reject( - new Error( - `Your Jira server version is unsupported. Minimum major version: 7. Your version: ${this.serverInfo.versionNumbers[0]}` - ) - ) + reject(new Error( + `Your Jira server version is unsupported. Minimum major version: 7. Your version: ${this.serverInfo.versionNumbers[0]}`, + )) } resolve() @@ -219,16 +171,13 @@ export class JiraServerProvider implements IProvider { async isLoggedIn(): Promise { return new Promise((resolve, reject) => { this.getAuthRestApiClient(1) - .get("/session") - .then(() => { - resolve() - }) + .get('/session') + .then(() => { resolve() }) .catch((error) => { if (isAxiosError(error) && error.response) { if (error.response.status === 401) { return Promise.reject(new Error("Wrong Username or Password")) - } - if (error.response.status === 404) { + } if (error.response.status === 404) { return Promise.reject(new Error("Wrong URL")) } } @@ -244,10 +193,8 @@ export class JiraServerProvider implements IProvider { async logout(): Promise { return new Promise((resolve, reject) => { this.getAuthRestApiClient(1) - .delete("/session") - .then(() => { - resolve() - }) + .delete('/session') + .then(() => { resolve() }) .catch((error) => { reject(new Error(`Error in logging out: ${error}`)) }) @@ -257,7 +204,7 @@ export class JiraServerProvider implements IProvider { async mapCustomFields(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(2) - .get("/field") + .get('/field') .then((response) => { response.data.forEach((field: { name: string; id: string }) => { this.customFields.set(field.name, field.id) @@ -274,7 +221,7 @@ export class JiraServerProvider implements IProvider { async getProjects(): Promise { return new Promise((resolve) => { this.getRestApiClient(2) - .get("/project?expand=lead,description") + .get('/project?expand=lead,description') .then((response) => { const projects = response.data.map((project: JiraProject) => ({ key: project.key, @@ -296,15 +243,13 @@ export class JiraServerProvider implements IProvider { const issueTypes: JiraIssueType[] = response.data resolve(issueTypes as IssueType[]) }) - .catch((error) => - reject(new Error(`Error in fetching the issue types: ${error}`)) - ) + .catch((error) => reject(new Error(`Error in fetching the issue types: ${error}`))) }) } async getBoardIds(project: string): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient("1.0") + this.getAgileRestApiClient('1.0') .get(`/board?projectKeyOrId=${project}`) .then(async (response) => { const boardIds: number[] = response.data.values.map( @@ -312,15 +257,13 @@ export class JiraServerProvider implements IProvider { ) resolve(boardIds) }) - .catch((error) => - reject(new Error(`Error in fetching the boards: ${error}`)) - ) + .catch((error) => reject(new Error(`Error in fetching the boards: ${error}`))) }) } async getSprints(boardId: number): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient("1.0") + this.getAgileRestApiClient('1.0') .get(`/board/${boardId}/sprint`) .then(async (response) => { const sprints: Sprint[] = response.data.values @@ -344,20 +287,16 @@ export class JiraServerProvider implements IProvider { }) resolve(sprints) }) - .catch((error) => - reject(new Error(`Error in fetching the boards: ${error}`)) - ) + .catch((error) => reject(new Error(`Error in fetching the boards: ${error}`))) }) } async getIssuesByProject(project: string, boardId: number): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient("1.0") + this.getAgileRestApiClient('1.0') .get(`/board/${boardId}/issue?jql=project=${project}&maxResults=10000`) .then((response) => resolve(this.fetchIssues(response))) - .catch((error) => - reject(new Error(`Error in fetching issues: ${error}`)) - ) + .catch((error) => reject(new Error(`Error in fetching issues: ${error}`))) }) } @@ -367,14 +306,10 @@ export class JiraServerProvider implements IProvider { boardId: number ): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient("1.0") - .get( - `/board/${boardId}/sprint/${sprintId}/issue?jql=project=${project}` - ) + this.getAgileRestApiClient('1.0') + .get(`/board/${boardId}/sprint/${sprintId}/issue?jql=project=${project}`) .then((response) => resolve(this.fetchIssues(response))) - .catch((error) => - reject(new Error(`Error in fetching issues: ${error}`)) - ) + .catch((error) => reject(new Error(`Error in fetching issues: ${error}`))) }) } @@ -383,14 +318,10 @@ export class JiraServerProvider implements IProvider { boardId: number ): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient("1.0") - .get( - `/board/${boardId}/backlog?jql=sprint is EMPTY AND project=${project}` - ) + this.getAgileRestApiClient('1.0') + .get(`/board/${boardId}/backlog?jql=sprint is EMPTY AND project=${project}`) .then((response) => resolve(this.fetchIssues(response))) - .catch((error) => - reject(new Error(`Error in fetching issues: ${error}`)) - ) + .catch((error) => reject(new Error(`Error in fetching issues: ${error}`))) }) } @@ -404,9 +335,7 @@ export class JiraServerProvider implements IProvider { creator: element.fields.creator.displayName, status: element.fields.status.name, type: element.fields.issuetype.name, - storyPointsEstimate: await this.getIssueStoryPointsEstimate( - element.key - ), + storyPointsEstimate: await this.getIssueStoryPointsEstimate(element.key), epic: element.fields.parent?.fields.summary, labels: element.fields.labels, assignee: { @@ -434,33 +363,33 @@ export class JiraServerProvider implements IProvider { ): Promise { return new Promise((resolve, reject) => { const rankCustomField = this.customFields.get("Rank") - this.getAgileRestApiClient("1.0") - .post(`/sprint/${sprint}/issue`, { - rankCustomFieldId: rankCustomField!.match(/_(\d+)/)![1], - issues: [issue], - ...(rankAfter && { rankAfterIssue: rankAfter }), - ...(rankBefore && { rankBeforeIssue: rankBefore }), - }) + this.getAgileRestApiClient('1.0') + .post( + `/sprint/${sprint}/issue`, + { + rankCustomFieldId: rankCustomField!.match(/_(\d+)/)![1], + issues: [issue], + ...(rankAfter && { rankAfterIssue: rankAfter }), + ...(rankBefore && { rankBeforeIssue: rankBefore }), + } + ) .then(() => resolve()) .catch((error) => { - reject( - new Error( - `Error in moving this issue to the Sprint with id ${sprint}: ${error}` - ) - ) + reject(new Error(`Error in moving this issue to the Sprint with id ${sprint}: ${error}`)) }) }) } async moveIssueToBacklog(issue: string): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient("1.0") - .post("/backlog/issue", { issues: [issue] }) + this.getAgileRestApiClient('1.0') + .post( + '/backlog/issue', + { issues: [issue] } + ) .then(() => resolve()) .catch((error) => - reject( - new Error(`Error in moving this issue to the Backlog: ${error}`) - ) + reject(new Error(`Error in moving this issue to the Backlog: ${error}`)) ) }) } @@ -486,13 +415,11 @@ export class JiraServerProvider implements IProvider { } else if (rankAfter) { body.rankAfterIssue = rankAfter } - this.getAgileRestApiClient("1.0") - .put("/issue/rank", body) + this.getAgileRestApiClient('1.0') + .put('/issue/rank', body) .then(() => resolve()) .catch((error) => - reject( - new Error(`Error in moving this issue to the Backlog: ${error}`) - ) + reject(new Error(`Error in moving this issue to the Backlog: ${error}`)) ) }) } @@ -508,11 +435,7 @@ export class JiraServerProvider implements IProvider { resolve(points) }) .catch((error) => - reject( - new Error( - `Error in getting the story points for issue: ${issue}: ${error}` - ) - ) + reject(new Error(`Error in getting the story points for issue: ${issue}: ${error}`)) ) }) } @@ -522,35 +445,26 @@ export class JiraServerProvider implements IProvider { this.getRestApiClient(2) .get(`/user/assignable/search?project=${projectIdOrKey}`) .then(async (response: AxiosResponse) => { - const users: User[] = response.data.map( - (user) => - ({ - id: user.key, - name: user.name, - displayName: user.displayName, - avatarUrls: user.avatarUrls, - emailAddress: user.emailAddress, - } as User) - ) + const users: User[] = response.data.map((user) => ({ + id: user.key, + name: user.name, + displayName: user.displayName, + avatarUrls: user.avatarUrls, + emailAddress: user.emailAddress, + } as User)) resolve(users) }) .catch((error) => { if (error.response) { if (error.response.status === 404) { - return Promise.reject( - Error(`Project was not found: ${error.response.data}`) - ) + return Promise.reject(Error(`Project was not found: ${error.response.data}`)) } } return Promise.reject(error) }) .catch((error) => { - reject( - new Error( - `Error in fetching the assignable users for the project ${projectIdOrKey}: ${error}` - ) - ) + reject(new Error(`Error in fetching the assignable users for the project ${projectIdOrKey}: ${error}`)) }) }) } @@ -558,19 +472,15 @@ export class JiraServerProvider implements IProvider { getCurrentUser(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(2) - .get("/myself") - .then(async (response: AxiosResponse) => - resolve({ - id: response.data.key, - name: response.data.name, - displayName: response.data.displayName, - avatarUrls: response.data.avatarUrls, - emailAddress: response.data.emailAddress, - } as User) - ) - .catch((error) => - reject(new Error(`Error in the current user: ${error}`)) - ) + .get('/myself') + .then(async (response: AxiosResponse) => resolve({ + id: response.data.key, + name: response.data.name, + displayName: response.data.displayName, + avatarUrls: response.data.avatarUrls, + emailAddress: response.data.emailAddress, + } as User)) + .catch((error) => reject(new Error(`Error in the current user: ${error}`))) }) } /* eslint-disable @typescript-eslint/no-unused-vars */ @@ -595,84 +505,81 @@ export class JiraServerProvider implements IProvider { const offsetDueDate = this.offsetDate(dueDate) return new Promise((resolve, reject) => { - this.getIssueTypesByProject(projectId).then((issueTypes) => { - const relevantIssueType = issueTypes.find( - (issueType) => issueType.id === type - ) - - this.getRestApiClient(2) - .post(`/issue`, { - fields: { - summary, - parent: { key: epic }, - issuetype: { id: type }, - project: { - id: projectId, - }, - reporter: { - name: reporter.name, - }, - ...(priority.id && { priority }), - ...(assignee && { - assignee: { - name: assignee.name, + this.getIssueTypesByProject(projectId) + .then((issueTypes) => { + const relevantIssueType = issueTypes.find((issueType) => issueType.id === type) + + this.getRestApiClient(2) + .post( + `/issue`, + { + fields: { + summary, + parent: { key: epic }, + issuetype: { id: type }, + project: { + id: projectId, + }, + reporter: { + name: reporter.name, + }, + ...(priority.id && { priority }), + ...(assignee && { + assignee: { + name: assignee.name, + }, + }), + description, + labels, + ...(offsetStartDate && { + [this.customFields.get("Start date")!]: offsetStartDate, + }), + ...(offsetDueDate && { + [this.customFields.get("Due date")!]: offsetDueDate, + }), + ...(sprint && + sprint.id && { + [this.customFields.get("Sprint")!]: +sprint.id, + }), + ...(storyPointsEstimate && { + [this.customFields.get("Story point estimate")!]: + storyPointsEstimate, + }), + ...(relevantIssueType && relevantIssueType.name === 'Epic' && { + [this.customFields.get("Epic Name")!]: summary + }), + // ...(files && { + // [this.customFields.get("Attachment")!]: files, + // }), }, - }), - description, - labels, - ...(offsetStartDate && { - [this.customFields.get("Start date")!]: offsetStartDate, - }), - ...(offsetDueDate && { - [this.customFields.get("Due date")!]: offsetDueDate, - }), - ...(sprint && - sprint.id && { - [this.customFields.get("Sprint")!]: +sprint.id, - }), - ...(storyPointsEstimate && { - [this.customFields.get("Story point estimate")!]: - storyPointsEstimate, - }), - ...(relevantIssueType && - relevantIssueType.name === "Epic" && { - [this.customFields.get("Epic Name")!]: summary, - }), - // ...(files && { - // [this.customFields.get("Attachment")!]: files, - // }), - }, - }) - .then(async (response) => { - const createdIssue = response.data - resolve(JSON.stringify(createdIssue.key)) - await this.setTransition(createdIssue.id, status) - }) - .catch((error) => { - let specificError = error - if (error.response) { - if (error.response.status === 404) { - specificError = new Error( - "The user does not have the necessary permissions" - ) } - } + ) + .then(async (response) => { + const createdIssue = response.data + resolve(JSON.stringify(createdIssue.key)) + await this.setTransition(createdIssue.id, status) + }) + .catch((error) => { + let specificError = error + if (error.response) { + if (error.response.status === 404) { + specificError = new Error("The user does not have the necessary permissions") + } + } - reject(new Error(`Error creating issue: ${specificError}`)) - }) - }) + reject(new Error(`Error creating issue: ${specificError}`)) + }) + }) }) } getEpicsByProject(projectIdOrKey: string): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(2) - .get( - `search?jql=issuetype = Epic AND project = ${projectIdOrKey}&fields=*all}` - ) + .get(`search?jql=issuetype = Epic AND project = ${projectIdOrKey}`) .then(async (response) => { const epics: Promise = Promise.all( - response.data.issues.map(async (element: JiraEpic) => ({ + response.data.issues.map(async (element: JiraIssue) => ({ issueKey: element.key, summary: element.fields.summary, labels: element.fields.labels, @@ -680,23 +587,6 @@ export class JiraServerProvider implements IProvider { displayName: element.fields.assignee?.displayName, avatarUrls: element.fields.assignee?.avatarUrls, }, - subtasks: element.fields.subtasks, - created: element.fields.created, - updated: element.fields.updated, - comment: { - comments: element.fields.comment.comments.map((coelem) => ({ - id: coelem.id, - body: coelem.body.content[0].content[0].text, - author: coelem.author, - created: coelem.created, - updated: coelem.updated, - })), - } ?? { - comments: [], - }, - projectId: element.fields.project.id, - sprint: element.fields.sprint, - attachments: element.fields.attachment, })) ) resolve(epics) @@ -711,26 +601,20 @@ export class JiraServerProvider implements IProvider { } } - reject( - new Error( - `Error in fetching the epics for the project ${projectIdOrKey}: ${specificError}` - ) - ) + reject(new Error(`Error in fetching the epics for the project ${projectIdOrKey}: ${specificError}`)) }) }) } getIssuesBySprint(sprintId: number): Promise { return new Promise((resolve, reject) => { - this.getAgileRestApiClient("1.0") + this.getAgileRestApiClient('1.0') .get(`/sprint/${sprintId}/issue`) .then(async (response) => { resolve(this.fetchIssues(response)) }) .catch((error) => { - reject( - new Error(`Error fetching issues by sprint ${sprintId}: ${error}`) - ) + reject(new Error(`Error fetching issues by sprint ${sprintId}: ${error}`)) }) }) } @@ -738,14 +622,12 @@ export class JiraServerProvider implements IProvider { getLabels(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(2) - .get("/jql/autocompletedata/suggestions?fieldName=labels") + .get('/jql/autocompletedata/suggestions?fieldName=labels') .then((response: AxiosResponse<{ results: { value: string }[] }>) => { resolve(response.data.results.map((result) => result.value)) }) .catch((error) => - reject( - new Error(`Error in fetching labels: ${JSON.stringify(error)}`) - ) + reject(new Error(`Error in fetching labels: ${JSON.stringify(error)}`)) ) }) } @@ -753,7 +635,7 @@ export class JiraServerProvider implements IProvider { getPriorities(): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(2) - .get("/priority") + .get('/priority') .then((response) => { const priorityData: Priority[] = response.data resolve(priorityData) @@ -766,15 +648,15 @@ export class JiraServerProvider implements IProvider { getIssueTypesWithFieldsMap(): Promise<{ [key: string]: string[] }> { return this.executeVersioned({ - "7.*": this.getIssueTypesWithFieldsMap_7.bind(this), - "*": this.getIssueTypesWithFieldsMap_8and9.bind(this), + '7.*': this.getIssueTypesWithFieldsMap_7.bind(this), + '*': this.getIssueTypesWithFieldsMap_8and9.bind(this) }) } getIssueTypesWithFieldsMap_7(): Promise<{ [key: string]: string[] }> { return new Promise((resolve) => { this.getRestApiClient(2) - .get("/issue/createmeta?expand=projects.issuetypes.fields") + .get('/issue/createmeta?expand=projects.issuetypes.fields') .then(async (response) => { const issueTypeToFieldsMap: { [key: string]: string[] } = {} response.data.projects.forEach( @@ -801,51 +683,39 @@ export class JiraServerProvider implements IProvider { getIssueTypesWithFieldsMap_8and9(): Promise<{ [key: string]: string[] }> { return new Promise((resolve) => { // IMPROVE: This is barely scalable - this.getProjects().then(async (projects) => { - const issueTypeToFieldsMap: { [key: string]: string[] } = {} - await Promise.all( - projects.map((project) => + this.getProjects() + .then(async (projects) => { + const issueTypeToFieldsMap: { [key: string]: string[] } = {} + await Promise.all(projects.map((project) => // IMPROVE: This call currently only supports 50 issue types this.getRestApiClient(2) .get(`/issue/createmeta/${project.id}/issuetypes`) .then(async (response) => { - await Promise.all( - response.data.values.map((issueType: { id: string }) => - // IMPROVE: This call currently only supports 50 issue types - this.getRestApiClient(2) - .get( - `/issue/createmeta/${project.id}/issuetypes/${issueType.id}` + await Promise.all(response.data.values.map((issueType: { id: string }) => + // IMPROVE: This call currently only supports 50 issue types + this.getRestApiClient(2) + .get(`/issue/createmeta/${project.id}/issuetypes/${issueType.id}`) + .then((issueTypesResponse) => { + issueTypeToFieldsMap[issueType.id] = issueTypesResponse.data.values.map( + (issueTypeField: { fieldId: string }) => this.reversedCustomFields.get(issueTypeField.fieldId)! ) - .then((issueTypesResponse) => { - issueTypeToFieldsMap[issueType.id] = - issueTypesResponse.data.values.map( - (issueTypeField: { fieldId: string }) => - this.reversedCustomFields.get( - issueTypeField.fieldId - )! - ) - }) - ) - ) + }) + )) }) - ) - ) + )) - return resolve(issueTypeToFieldsMap) - }) + return resolve(issueTypeToFieldsMap) + }) }) } getResource(): Promise { return new Promise((resolve, reject) => { - if ( - this.loginOptions.username !== undefined && - this.loginOptions.password - ) { + if (this.loginOptions.username !== undefined && this.loginOptions.password) { // IMPROVE expose API client instead of resource - const { defaults } = this.getRestApiClient(2) + const {defaults} = this.getRestApiClient(2) const result: Resource = { - baseUrl: defaults.baseURL ?? "", + baseUrl: defaults.baseURL ?? '', authorization: defaults.headers.Authorization as string, } resolve(result) @@ -866,32 +736,29 @@ export class JiraServerProvider implements IProvider { const offsetEndDate = this.offsetDate(endDate) return new Promise((resolve, reject) => { - this.getAgileRestApiClient("1.0") - .post("/sprint", { - name, - originBoardId, - ...(offsetStartDate && { - startDate: offsetStartDate, - }), - ...(offsetEndDate && { - endDate: offsetEndDate, - }), - ...(goal && { goal }), - }) - .then(async () => { - resolve() - }) + this.getAgileRestApiClient('1.0') + .post( + '/sprint', + { + name, + originBoardId, + ...(offsetStartDate && { + startDate: offsetStartDate, + }), + ...(offsetEndDate && { + endDate: offsetEndDate, + }), + ...(goal && { goal }), + } + ) + .then(async () => { resolve() }) .catch((error) => { let specificError = error if (error.response) { if (error.response.status === 403) { - specificError = new Error( - "The user does not have the necessary permissions" - ) + specificError = new Error("The user does not have the necessary permissions") } else if (error.response.status === 404) { - specificError = new Error( - "The Board does not exist or the user does not have the necessary permissions to view it" - ) + specificError = new Error("The Board does not exist or the user does not have the necessary permissions to view it") } } @@ -904,32 +771,20 @@ export class JiraServerProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(2) .delete(`/issue/${issueIdOrKey}?deleteSubtasks`) - .then(async () => { - resolve() - }) + .then(async () => { resolve() }) .catch((error) => { let specificError = error if (error.response) { if (error.response.status === 403) { - specificError = new Error( - "The user does not have permission to delete the issue" - ) + specificError = new Error("The user does not have permission to delete the issue") } else if (error.response.status === 404) { - specificError = new Error( - "The issue was not found or the user does not have the necessary permissions" - ) + specificError = new Error("The issue was not found or the user does not have the necessary permissions") } else if (error.response.status === 405) { - specificError = new Error( - "An anonymous call has been made to the operation" - ) + specificError = new Error("An anonymous call has been made to the operation") } } - reject( - new Error( - `Error deleting the issue ${issueIdOrKey}: ${specificError}` - ) - ) + reject(new Error(`Error deleting the issue ${issueIdOrKey}: ${specificError}`)) }) }) } @@ -942,27 +797,30 @@ export class JiraServerProvider implements IProvider { ): Promise<{ id: string; key: string }> { return new Promise((resolve, reject) => { this.getRestApiClient(2) - .post("/issue", { - fields: { - summary: subtaskSummary, - issuetype: { - id: subtaskIssueTypeId, - }, - parent: { - key: parentIssueKey, - }, - project: { - id: projectId, - }, - }, - }) - .then(async (response) => { - const createdSubtask: { id: string; key: string } = response.data - resolve(createdSubtask) - }) - .catch((error) => { - reject(new Error(`Error creating subtask: ${error}`)) - }) + .post( + '/issue', + { + fields: { + summary: subtaskSummary, + issuetype: { + id: subtaskIssueTypeId, + }, + parent: { + key: parentIssueKey, + }, + project: { + id: projectId, + }, + }, + } + ) + .then(async (response) => { + const createdSubtask: { id: string; key: string } = response.data + resolve(createdSubtask) + }) + .catch((error) => { + reject(new Error(`Error creating subtask: ${error}`)) + }) }) } @@ -989,53 +847,54 @@ export class JiraServerProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(2) - .put(`/issue/${issueIdOrKey}`, { - fields: { - ...(summary && { - summary, - }), - ...(epic && { - parent: { key: epic }, - }), - ...(type && { - issuetype: { id: type }, - }), - ...(projectId && { - project: { - id: projectId, - }, - }), - ...(reporter && { - reporter, - }), - ...(priority && priority.id && { priority }), - ...(assignee && { - assignee, - }), - ...(description && { - description, - }), - ...(labels && { - labels, - }), - ...(offsetStartDate && { - [this.customFields.get("Start date")!]: offsetStartDate, - }), - ...(offsetDueDate && { - [this.customFields.get("Due date")!]: offsetDueDate, - }), - ...(sprint && { - [this.customFields.get("Sprint")!]: sprint.id, - }), - ...(storyPointsEstimate !== undefined && { - [this.customFields.get("Story point estimate")!]: + .put( + `/issue/${issueIdOrKey}`, + { + fields: { + ...(summary && { + summary, + }), + ...(epic && { + parent: { key: epic }, + }), + ...(type && { + issuetype: { id: type }, + }), + ...(projectId && { + project: { + id: projectId, + }, + }), + ...(reporter && { + reporter, + }), + ...(priority && priority.id && { priority }), + ...(assignee && { + assignee, + }), + ...(description && { + description + }), + ...(labels && { + labels, + }), + ...(offsetStartDate && { + [this.customFields.get("Start date")!]: offsetStartDate, + }), + ...(offsetDueDate && { + [this.customFields.get("Due date")!]: offsetDueDate, + }), + ...(sprint && { + [this.customFields.get("Sprint")!]: sprint.id, + }), + ...(storyPointsEstimate !== undefined && { + [this.customFields.get("Story point estimate")!]: storyPointsEstimate, - }), - }, - }) - .then(async () => { - resolve() - }) + }), + }, + } + ) + .then(async () => { resolve() }) .catch((error) => { let specificError = error if (error.response) { @@ -1057,15 +916,15 @@ export class JiraServerProvider implements IProvider { .get(`/issue/${issueIdOrKey}/transitions`) .then((response) => { const transitions = new Map() - response.data.transitions.forEach( - (field: { name: string; id: string }) => { - transitions.set(field.name, field.id) - } - ) + response.data.transitions.forEach((field: { name: string; id: string }) => { + transitions.set(field.name, field.id) + }) const transitionId = +transitions.get(targetStatus)! - return this.getRestApiClient(2).post( + return this + .getRestApiClient(2) + .post( `/issue/${issueIdOrKey}/transitions`, { transition: { id: transitionId } } ) @@ -1110,26 +969,21 @@ export class JiraServerProvider implements IProvider { } } - reject( - new Error(`Error in fetching the issue reporter: ${specificError}`) - ) + reject(new Error(`Error in fetching the issue reporter: ${specificError}`)) }) }) } addCommentToIssue(issueIdOrKey: string, commentText: string): Promise { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { this.getRestApiClient(2) - .post(`/issue/${issueIdOrKey}/comment`, { - body: commentText.replace(/\n/g, " "), - }) + .post( + `/issue/${issueIdOrKey}/comment`, + { body: commentText.replace(/\n/g, " ") } + ) .then(() => resolve()) .catch((error) => { - reject( - new Error( - `Error adding a comment to the issue ${issueIdOrKey}: ${error}` - ) - ) + reject(new Error(`Error adding a comment to the issue ${issueIdOrKey}: ${error}`)) }) }) } @@ -1139,41 +993,27 @@ export class JiraServerProvider implements IProvider { commentId: string, commentText: string ): Promise { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) =>{ // main part this.getRestApiClient(2) - .put(`/issue/${issueIdOrKey}/comment/${commentId}`, { - body: commentText.replace(/\n/g, " "), - }) - .then(() => { - resolve() - }) + .put( + `/issue/${issueIdOrKey}/comment/${commentId}`, + { body: commentText.replace(/\n/g, " ") } + ) + .then(() => { resolve() }) .catch((error) => { if (error.response) { if (error.response.status === 400) { - return Promise.reject( - new Error( - "The user does not have permission to edit the comment or the request is invalid" - ) - ) - } - if (error.response.status === 404) { - return Promise.reject( - new Error( - "The issue was not found or the user does not have the necessary permissions" - ) - ) + return Promise.reject(new Error("The user does not have permission to edit the comment or the request is invalid")) + } if (error.response.status === 404) { + return Promise.reject(new Error("The issue was not found or the user does not have the necessary permissions")) } } return Promise.reject(error) }) .catch((error) => { - reject( - Error( - `Error editing the comment in issue ${issueIdOrKey}: ${error}` - ) - ) + reject(Error(`Error editing the comment in issue ${issueIdOrKey}: ${error}`)) }) }) } @@ -1182,37 +1022,20 @@ export class JiraServerProvider implements IProvider { return new Promise((resolve, reject) => { this.getRestApiClient(2) .delete(`/issue/${issueIdOrKey}/comment/${commentId}`) - .then(() => { - resolve() - }) + .then(() => { resolve() }) .catch((error) => { if (error.response) { if (error.response.status === 404) { - return Promise.reject( - new Error( - "The issue was not found or the user does not have the necessary permissions" - ) - ) - } - if (error.response.status === 405) { - return Promise.reject( - new Error("An anonymous call has been made to the operation") - ) + return Promise.reject(new Error("The issue was not found or the user does not have the necessary permissions")) + } if (error.response.status === 405) { + return Promise.reject(new Error("An anonymous call has been made to the operation")) } } - return Promise.reject( - new Error( - `Error deleting the comment in issue ${issueIdOrKey}: ${error}` - ) - ) + return Promise.reject(new Error(`Error deleting the comment in issue ${issueIdOrKey}: ${error}`)) }) .catch((error) => { - reject( - Error( - `Error deleting the comment in issue ${issueIdOrKey}: ${error}` - ) - ) + reject(Error(`Error deleting the comment in issue ${issueIdOrKey}: ${error}`)) }) }) } diff --git a/types/jira.ts b/types/jira.ts index 45fe5eac..ab6d9466 100644 --- a/types/jira.ts +++ b/types/jira.ts @@ -17,77 +17,7 @@ export interface JiraSprint { state: string name: string } -// EpicIssue structure differs from normal Issue structure -export interface JiraEpic { - key: string - fields: { - description: string - summary: string - creator: { name: string; displayName: string } - status: { name: string } - issuetype: { name: string } - customfield_10107: number - parent: { id: string; fields: { summary: string } } - epic: { name: string } - labels: string[] - assignee: { - displayName: string - avatarUrls: { - "16x16": string - "24x24": string - "36x36": string - "48x48": string - } - } - [rankCustomField: string]: string | unknown - subtasks: { - id: string - key: string - fields: { - summary: string - } - }[] - project: { id: string } - created: string - updated: string - comment: { - comments: [ - { - id: string - author: { - accountId: string - avatarUrls: { - "48x48": string - "24x24": string - "16x16": string - "32x32": string - } - displayName: string - } - body: { - type: string - version: number - content: [ - { - type: string - content: [ - { - type: string - text: string - } - ] - } - ] - } - created: string - updated: string - } - ] - } - sprint?: JiraSprint - attachment?: Attachment[] - } -} + export interface JiraIssue { key: string fields: { From e12e83cfd315ca0f89cdbef89ab9b2d707488517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Mon, 18 Dec 2023 18:41:08 +0100 Subject: [PATCH 08/12] Add missing types --- types/jira.ts | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/types/jira.ts b/types/jira.ts index ab6d9466..8d0b8af6 100644 --- a/types/jira.ts +++ b/types/jira.ts @@ -1,4 +1,4 @@ -import { Attachment, Priority } from "." +import {Attachment, Priority} from "." export interface JiraProject { projectTypeKey: string @@ -18,6 +18,79 @@ export interface JiraSprint { name: string } +// EpicIssue structure differs from normal Issue structure +export interface JiraEpic { + key: string + fields: { + description: string + summary: string + creator: { name: string; displayName: string } + status: { name: string } + issuetype: { name: string } + // TODO: improve this, let's try not to hardcode customfields + customfield_10107: number + parent: { id: string; fields: { summary: string } } + epic: { name: string } + labels: string[] + assignee: { + displayName: string + avatarUrls: { + "16x16": string + "24x24": string + "36x36": string + "48x48": string + } + } + [rankCustomField: string]: string | unknown + subtasks: { + id: string + key: string + fields: { + summary: string + } + }[] + project: { id: string } + created: string + updated: string + comment: { + comments: [ + { + id: string + author: { + accountId: string + avatarUrls: { + "48x48": string + "24x24": string + "16x16": string + "32x32": string + } + displayName: string + } + body: { + type: string + version: number + content: [ + { + type: string + content: [ + { + type: string + text: string + } + ] + } + ] + } + created: string + updated: string + } + ] + } + sprint?: JiraSprint + attachment?: Attachment[] + } +} + export interface JiraIssue { key: string fields: { From 4ecdd3b41adf3ba027ad9af022bf59d3f3784470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Mon, 18 Dec 2023 18:41:52 +0100 Subject: [PATCH 09/12] Revert "commentSection implemented - edit/delete/add may take long(needs fixing)" This reverts commit 5cc757b3 --- types/jira.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/types/jira.ts b/types/jira.ts index 8d0b8af6..65b47da6 100644 --- a/types/jira.ts +++ b/types/jira.ts @@ -99,6 +99,12 @@ export interface JiraIssue { creator: { name: string; displayName: string } status: { name: string } issuetype: { name: string } + // TODO: improve this, let's try not to: + // -hardcode customfields + // -not use | unknown if possible. + // the problem is: change the LHS name of these props in the fields definition + // based on the mapped fields (this.customFields), + // it might change based on the jira instance customfield_10107: number parent: { id: string; fields: { summary: string } } epic: { name: string } From 8dac3350b85e5c11ea3b836a59e2a8bd250c90d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Mon, 18 Dec 2023 18:49:02 +0100 Subject: [PATCH 10/12] Reconstruct server and cloud implementations --- .../jira-cloud-provider/JiraCloudProvider.ts | 15 +++++++++--- .../JiraServerProvider.ts | 23 ++++++++++++++++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts index e837ec4a..471ab806 100644 --- a/electron/providers/jira-cloud-provider/JiraCloudProvider.ts +++ b/electron/providers/jira-cloud-provider/JiraCloudProvider.ts @@ -12,6 +12,7 @@ import { User, } from "../../../types" import { + JiraEpic, JiraIssue, JiraIssueType, JiraPriority, @@ -847,10 +848,10 @@ export class JiraCloudProvider implements IProvider { async getEpicsByProject(projectIdOrKey: string): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(3) - .get(`search?jql=issuetype = Epic AND project = ${projectIdOrKey}`) + .get(`search?jql=issuetype = Epic AND project = ${projectIdOrKey}&fields=*all`) .then(async (response) => { const epics: Promise = Promise.all( - response.data.issues.map(async (element: JiraIssue) => ({ + response.data.issues.map(async (element: JiraEpic) => ({ issueKey: element.key, summary: element.fields.summary, labels: element.fields.labels, @@ -861,7 +862,15 @@ export class JiraCloudProvider implements IProvider { subtasks: element.fields.subtasks, created: element.fields.created, updated: element.fields.updated, - comment: element.fields.comment ?? { + comment: { + comments: element.fields.comment.comments.map((commentElement) => ({ + id: commentElement.id, + body: commentElement.body.content[0].content[0].text, + author: commentElement.author, + created: commentElement.created, + updated: commentElement.updated, + })), + } ?? { comments: [], }, projectId: element.fields.project.id, diff --git a/electron/providers/jira-server-provider/JiraServerProvider.ts b/electron/providers/jira-server-provider/JiraServerProvider.ts index 402c4668..97c15dfc 100644 --- a/electron/providers/jira-server-provider/JiraServerProvider.ts +++ b/electron/providers/jira-server-provider/JiraServerProvider.ts @@ -11,7 +11,7 @@ import { SprintCreate, User, } from "../../../types" -import {JiraIssue, JiraIssueType, JiraProject, JiraSprint,} from "../../../types/jira" +import {JiraEpic, JiraIssue, JiraIssueType, JiraProject, JiraSprint,} from "../../../types/jira" import {IProvider} from "../base-provider" import {JiraServerInfo, JiraServerUser} from "./server-types"; @@ -576,10 +576,10 @@ export class JiraServerProvider implements IProvider { getEpicsByProject(projectIdOrKey: string): Promise { return new Promise((resolve, reject) => { this.getRestApiClient(2) - .get(`search?jql=issuetype = Epic AND project = ${projectIdOrKey}`) + .get(`search?jql=issuetype = Epic AND project = ${projectIdOrKey}&fields=*all`) .then(async (response) => { const epics: Promise = Promise.all( - response.data.issues.map(async (element: JiraIssue) => ({ + response.data.issues.map(async (element: JiraEpic) => ({ issueKey: element.key, summary: element.fields.summary, labels: element.fields.labels, @@ -587,6 +587,23 @@ export class JiraServerProvider implements IProvider { displayName: element.fields.assignee?.displayName, avatarUrls: element.fields.assignee?.avatarUrls, }, + subtasks: element.fields.subtasks, + created: element.fields.created, + updated: element.fields.updated, + comment: { + comments: element.fields.comment.comments.map((commentElement) => ({ + id: commentElement.id, + body: commentElement.body, + author: commentElement.author, + created: commentElement.created, + updated: commentElement.updated, + })), + } ?? { + comments: [], + }, + projectId: element.fields.project.id, + sprint: element.fields.sprint, + attachments: element.fields.attachment, })) ) resolve(epics) From 7f03873e6423c8d3a16faa90f507632acc7b6967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Wed, 20 Dec 2023 10:36:19 +0100 Subject: [PATCH 11/12] Simplify comment expression --- electron/providers/jira-server-provider/JiraServerProvider.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/electron/providers/jira-server-provider/JiraServerProvider.ts b/electron/providers/jira-server-provider/JiraServerProvider.ts index 97c15dfc..64362103 100644 --- a/electron/providers/jira-server-provider/JiraServerProvider.ts +++ b/electron/providers/jira-server-provider/JiraServerProvider.ts @@ -598,8 +598,6 @@ export class JiraServerProvider implements IProvider { created: commentElement.created, updated: commentElement.updated, })), - } ?? { - comments: [], }, projectId: element.fields.project.id, sprint: element.fields.sprint, From c178ad9ec7680cd9a19d3fe47043ab1f633c2797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20R=C3=BCsch?= Date: Wed, 20 Dec 2023 10:49:24 +0100 Subject: [PATCH 12/12] Fix accidental merge changes --- .../jira-server-provider/JiraServerProvider.ts | 2 -- src/components/EpicDetailView/EpicDetailView.tsx | 4 ++-- types/jira.ts | 11 ++++++++++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/electron/providers/jira-server-provider/JiraServerProvider.ts b/electron/providers/jira-server-provider/JiraServerProvider.ts index 65fa2ce4..71f82146 100644 --- a/electron/providers/jira-server-provider/JiraServerProvider.ts +++ b/electron/providers/jira-server-provider/JiraServerProvider.ts @@ -594,8 +594,6 @@ export class JiraServerProvider implements IProvider { avatarUrls: element.fields.assignee?.avatarUrls, }, subtasks: element.fields.subtasks, - created: element.fields.created, - updated: element.fields.updated, comment: { comments: element.fields.comment.comments.map((commentElement) => ({ id: commentElement.id, diff --git a/src/components/EpicDetailView/EpicDetailView.tsx b/src/components/EpicDetailView/EpicDetailView.tsx index 0fc06a8d..fec413a9 100644 --- a/src/components/EpicDetailView/EpicDetailView.tsx +++ b/src/components/EpicDetailView/EpicDetailView.tsx @@ -44,8 +44,8 @@ export function EpicDetailView({ created, updated, comment, - closeModal, - }: { + closeModal, +}: { issueKey: string summary: string labels: string[] diff --git a/types/jira.ts b/types/jira.ts index 46ce44c9..dc3f2724 100644 --- a/types/jira.ts +++ b/types/jira.ts @@ -22,7 +22,16 @@ export interface JiraSprint { export interface JiraEpic { key: string fields: { - description: string + description: { + type: string, + version: string, + content: string & { + content: { + type: string, + text: string + }[] + }[] + } summary: string creator: { name: string; displayName: string } status: { name: string }