Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate issue functionality in server #66

Merged
Merged
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
723d35e
Add axios as a dependency
maximilianruesch Nov 11, 2023
b8bffe8
Add jira cloud API clients
maximilianruesch Nov 11, 2023
08f8328
Migrate trivial requests
maximilianruesch Nov 11, 2023
fe98be6
Migrate non-trivial issue functionality
maximilianruesch Nov 11, 2023
c2f73d2
Resolve issues with default axios handlers
maximilianruesch Nov 11, 2023
359f1bb
Universally handle 401 errors
maximilianruesch Nov 11, 2023
ec0abf4
Improve error handling
maximilianruesch Nov 12, 2023
e30fa9e
Universally handle 400 errors
maximilianruesch Nov 12, 2023
ead3438
Universally handle 429 errors
maximilianruesch Nov 12, 2023
cf84f14
Handle exposed resource through API client
maximilianruesch Nov 12, 2023
053ef38
Universally handle generic 403 errors
maximilianruesch Nov 12, 2023
65775f0
Add new Jira server API clients
maximilianruesch Nov 12, 2023
de7083f
Migrate trivial Jira server calls to new API clients
maximilianruesch Nov 12, 2023
f54a3e9
Migrate non-trivial issue fetching API requests
maximilianruesch Nov 12, 2023
dd9e0eb
Add implementation for current user
maximilianruesch Nov 12, 2023
2e6fce5
Add implementation for assignable users by project
maximilianruesch Nov 12, 2023
53369ef
Fix eslint
maximilianruesch Nov 12, 2023
caa0231
Fix eslint
maximilianruesch Nov 12, 2023
34c8304
Merge branch 'refactor/swap-to-axios' into refactor/migrate-jira-serv…
maximilianruesch Nov 12, 2023
e3f5ebb
Merge branch 'refactor/migrate-jira-server-to-axios' into user-story/…
maximilianruesch Nov 12, 2023
e4ad2c8
Fix eslint
maximilianruesch Nov 12, 2023
c1fe04d
Fix custom fields
maximilianruesch Nov 12, 2023
50c152d
Fix custom fields mapping
maximilianruesch Nov 12, 2023
6215abf
Merge branch 'refactor/migrate-jira-server-to-axios' into user-story/…
maximilianruesch Nov 12, 2023
97b242a
Merge branch 'user-story/migrate-user-functionality-in-server' into u…
maximilianruesch Nov 12, 2023
5015dc9
Add rudimentary support for issue creation meta
maximilianruesch Nov 12, 2023
0335a32
Fix issue types with fields map
maximilianruesch Nov 13, 2023
dcab85b
Implement getPriorities
maximilianruesch Nov 13, 2023
daae882
Add ADR for axios
maximilianruesch Nov 18, 2023
a55492e
Merge branch 'refactor/swap-to-axios' into refactor/migrate-jira-serv…
maximilianruesch Nov 18, 2023
8b44aa0
Merge branch 'refactor/migrate-jira-server-to-axios' into user-story/…
maximilianruesch Nov 18, 2023
166cd4e
Merge branch 'user-story/migrate-user-functionality-in-server' into u…
maximilianruesch Nov 18, 2023
300b1de
Implement get issues by sprint
maximilianruesch Nov 18, 2023
a189ecd
Fix fetch issues
maximilianruesch Nov 18, 2023
026288c
Fix get issue reporter
maximilianruesch Nov 18, 2023
159f39a
Implement get editable issue fields
maximilianruesch Nov 18, 2023
3236dd6
Implement edit issue
maximilianruesch Nov 18, 2023
e7ef312
Implement set transition
maximilianruesch Nov 18, 2023
aa76f5f
Implement delete issue
maximilianruesch Nov 18, 2023
77b179a
Fix get projects
maximilianruesch Nov 19, 2023
ccc43bb
Merge branch 'refactor/swap-to-axios' into refactor/migrate-jira-serv…
maximilianruesch Nov 19, 2023
10e64ef
Merge branch 'refactor/migrate-jira-server-to-axios' into user-story/…
maximilianruesch Nov 19, 2023
81d9de5
Merge branch 'user-story/migrate-user-functionality-in-server' into u…
maximilianruesch Nov 19, 2023
fa8dbe4
Merge branch 'main' into user-story/migrate-user-functionality-in-server
maximilianruesch Nov 21, 2023
5be874a
Merge branch 'user-story/migrate-user-functionality-in-server' into u…
maximilianruesch Nov 21, 2023
77b04c9
Fix create issue modal
maximilianruesch Nov 21, 2023
7b286f8
Merge branch 'user-story/migrate-user-functionality-in-server' into u…
maximilianruesch Nov 21, 2023
3f09c54
Rename variable in create issue modal
maximilianruesch Nov 21, 2023
a4353a6
Merge branch 'user-story/migrate-user-functionality-in-server' into u…
maximilianruesch Nov 21, 2023
ee3dcf4
Merge branch 'main' into user-story/migrate-issue-functionality-in-se…
maximilianruesch Nov 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 232 additions & 15 deletions electron/providers/jira-server-provider/JiraServerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export class JiraServerProvider implements IProvider {

private customFields = new Map<string, string>()

private reversedCustomFields = new Map<string, string>()

private getAuthHeader() {
return `Basic ${Buffer.from(
`${this.loginOptions.username}:${this.loginOptions.password}`
Expand Down Expand Up @@ -95,7 +97,7 @@ export class JiraServerProvider implements IProvider {
this.loginOptions.username = basicLoginOptions.username
this.loginOptions.password = basicLoginOptions.password

// await this.mapCustomFields()
await this.mapCustomFields()
return this.isLoggedIn()
}

Expand Down Expand Up @@ -139,6 +141,7 @@ export class JiraServerProvider implements IProvider {
.then((response) => {
response.data.forEach((field: { name: string; id: string }) => {
this.customFields.set(field.name, field.id)
this.reversedCustomFields.set(field.id, field.name)
})
resolve()
})
Expand Down Expand Up @@ -262,19 +265,25 @@ export class JiraServerProvider implements IProvider {
response.data.issues.map(async (element: JiraIssue) => ({
issueKey: element.key,
summary: element.fields.summary,
creator: element.fields.creator.name,
creator: element.fields.creator.displayName,
status: element.fields.status.name,
type: element.fields.issuetype.name,
storyPointsEstimate: await this.getIssueStoryPointsEstimate(
element.key
),
epic: element.fields.epic?.name,
storyPointsEstimate: await this.getIssueStoryPointsEstimate(element.key),
epic: element.fields.parent?.fields.summary,
labels: element.fields.labels,
assignee: {
displayName: element.fields.assignee?.displayName,
avatarUrls: element.fields.assignee?.avatarUrls,
},
rank: element.fields[rankCustomField!],
description: element.fields.description,
subtasks: element.fields.subtasks,
created: element.fields.created,
updated: element.fields.updated,
comment: element.fields.comment,
projectId: element.fields.project.id,
sprint: element.fields.sprint,
attachments: element.fields.attachment,
}))
)
}
Expand Down Expand Up @@ -405,19 +414,63 @@ export class JiraServerProvider implements IProvider {
}

getIssuesBySprint(sprintId: number): Promise<Issue[]> {
throw new Error("Method not implemented for Jira Server")
return new Promise((resolve, reject) => {
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}`))
})
})
}

getLabels(): Promise<string[]> {
throw new Error("Method not implemented for Jira Server")
}

getPriorities(): Promise<Priority[]> {
throw new Error("Method not implemented for Jira Server")
return new Promise((resolve, reject) => {
this.getRestApiClient(2)
.get('/priority')
.then((response) => {
const priorityData: Priority[] = response.data
resolve(priorityData)
})
.catch((error) =>
reject(new Error(`Error in fetching priorities: ${error}`))
)
})
}

getIssueTypesWithFieldsMap(): Promise<{ [key: string]: string[] }> {
throw new Error("Method not implemented for Jira Server")
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) =>
// 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)!
)
})
))
})
))

return resolve(issueTypeToFieldsMap)
})
})
}

getResource(): Promise<Resource> {
Expand All @@ -429,7 +482,25 @@ export class JiraServerProvider implements IProvider {
}

deleteIssue(issueIdOrKey: string): Promise<void> {
throw new Error("Method not implemented for Jira Server")
return new Promise((resolve, reject) => {
this.getRestApiClient(2)
.delete(`/issue/${issueIdOrKey}?deleteSubtasks`)
.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")
} else if (error.response.status === 404) {
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")
}
}

reject(new Error(`Error deleting the issue ${issueIdOrKey}: ${specificError}`))
})
})
}

createSubtask(
Expand All @@ -441,20 +512,157 @@ export class JiraServerProvider implements IProvider {
throw new Error("Method not implemented for Jira Server")
}

editIssue(issue: Issue, issueIdOrKey: string): Promise<void> {
throw new Error("Method not implemented for Jira Server")
editIssue(
{
summary,
type,
projectId,
reporter,
assignee,
sprint,
storyPointsEstimate,
description,
epic,
startDate,
dueDate,
labels,
priority,
}: Issue,
issueIdOrKey: string
): Promise<void> {
const offsetStartDate = this.offsetDate(startDate)
const offsetDueDate = this.offsetDate(dueDate)

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: {
id: reporter,
},
}),
...(priority && priority.id && { priority }),
...(assignee &&
assignee.id && {
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() })
.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"
)
}
}

reject(new Error(`Error creating issue: ${specificError}`))
})
})
}

setTransition(issueIdOrKey: string, targetStatus: string): Promise<void> {
throw new Error("Method not implemented for Jira Server")
return new Promise((resolve, reject) => {
this.getRestApiClient(2)
.get(`/issue/${issueIdOrKey}/transitions`)
.then((response) => {
const transitions = new Map<string, string>()
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(
`/issue/${issueIdOrKey}/transitions`,
{ transition: { id: transitionId } }
)
})
.then(() => resolve())
.catch((error) => {
reject(new Error(`Error setting transition: ${error}`))
})
})
}

getEditableIssueFields(issueIdOrKey: string): Promise<string[]> {
throw new Error("Method not implemented for Jira Server")
return new Promise((resolve, reject) => {
this.getRestApiClient(2)
.get(`/issue/${issueIdOrKey}/editmeta`)
.then(async (response) => {
const fieldKeys = Object.keys(response.data.fields).map(
(fieldKey) => this.reversedCustomFields.get(fieldKey)!
)
resolve(fieldKeys)
})
.catch((error) =>
reject(new Error(`Error in fetching the issue types map: ${error}`))
)
})
}

getIssueReporter(issueIdOrKey: string): Promise<User> {
throw new Error("Method not implemented for Jira Server")
return new Promise((resolve, reject) => {
this.getRestApiClient(2)
.get(`/issue/${issueIdOrKey}?fields=reporter`)
.then(async (response) => {
resolve(response.data.fields.reporter as User)
})
.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 permission to view it: ${error.response.data}`
)
}
}

reject(new Error(`Error in fetching the issue reporter: ${specificError}`))
})
})
}

addCommentToIssue(issueIdOrKey: string, commentText: string): Promise<void> {
Expand All @@ -479,4 +687,13 @@ export class JiraServerProvider implements IProvider {
}): Promise<void> {
throw new Error("Method not implemented for Jira Server")
}

offsetDate(date: Date) {
if (!date) {
return date
}
const convertedDate = new Date(date)
const timezoneOffset = convertedDate.getTimezoneOffset()
return new Date(convertedDate.getTime() - timezoneOffset * 60 * 1000)
}
}