From 94d8aa04f519e0c9c4a66901a2dd59cf832733bc Mon Sep 17 00:00:00 2001 From: Lee Chun Wei <47494777+chunweii@users.noreply.github.com> Date: Tue, 8 Mar 2022 14:38:51 +0800 Subject: [PATCH 01/13] Create issue templates (#899) As an Open Source Project, it becomes painful to see developers writing bug reports which do not clarify what the bug is about. (Which was one of the purposes of CATcher as well) Let's come up with a few issue templates for Bug Reports / Feature Requests such that issue reporters are guided to give good features / clarify their issues before submitting. --- .github/ISSUE_TEMPLATE/bug_report.md | 32 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/custom.md | 10 +++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/custom.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..95837e66d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: category.Bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. Windows 11] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 000000000..48d5f81fa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..d6ea6c8b4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: 'category.Feature' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 8f3d8ba0b905e80fb67b91fcad2097fe5f5cc1f4 Mon Sep 17 00:00:00 2001 From: "Lee Xiong Jie, Isaac" <68138671+luminousleek@users.noreply.github.com> Date: Thu, 10 Mar 2022 15:45:38 +0800 Subject: [PATCH 02/13] Abstract code to find comments that match a template to the Template class (#904) Abstract code to find comments that match a template to superclass There is some duplicated code across 4 templates that deal with finding a comment that conforms to the comment template. Let's abstract this code into the Template superclass and create a new parseFailure attribute inside Template. Let's also use this opportunity to standardise the variable names across the templates. Lastly, let's use parseFailure in Issue. --- src/app/core/models/issue.model.ts | 4 ++-- .../templates/team-accepted-template.model.ts | 6 +++--- .../templates/team-response-template.model.ts | 18 +++++++++--------- .../core/models/templates/template.model.ts | 13 +++++++++++++ .../tester-response-template.model.ts | 9 ++++----- .../tutor-moderation-todo-template.model.ts | 17 ++++++++++------- 6 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/app/core/models/issue.model.ts b/src/app/core/models/issue.model.ts index e906b888e..6811373dd 100644 --- a/src/app/core/models/issue.model.ts +++ b/src/app/core/models/issue.model.ts @@ -135,7 +135,7 @@ export class Issue { issue.teamAssigned = teamData; issue.assignees = githubIssue.assignees.map((assignee) => assignee.login); - issue.teamResponseError = template.parseError; + issue.teamResponseError = template.parseFailure; issue.issueComment = template.comment; issue.teamResponse = template.teamResponse && Issue.updateTeamResponse(template.teamResponse.content); issue.duplicateOf = template.duplicateOf && template.duplicateOf.issueNumber; @@ -150,7 +150,7 @@ export class Issue { const teamAcceptedTemplate = new TeamAcceptedTemplate(githubIssue.comments); issue.githubComments = githubIssue.comments; - issue.testerResponseError = testerResponseTemplate.parseError && !teamAcceptedTemplate.teamAccepted; + issue.testerResponseError = testerResponseTemplate.parseFailure && teamAcceptedTemplate.parseFailure; issue.teamAccepted = teamAcceptedTemplate.teamAccepted; issue.issueComment = testerResponseTemplate.comment; issue.teamResponse = testerResponseTemplate.teamResponse && Issue.updateTeamResponse(testerResponseTemplate.teamResponse.content); diff --git a/src/app/core/models/templates/team-accepted-template.model.ts b/src/app/core/models/templates/team-accepted-template.model.ts index 75d7b9bda..7f9a35e53 100644 --- a/src/app/core/models/templates/team-accepted-template.model.ts +++ b/src/app/core/models/templates/team-accepted-template.model.ts @@ -7,12 +7,12 @@ export const TeamAcceptedHeader = { teamAccepted: new Header(TeamAcceptedMessage export class TeamAcceptedTemplate extends Template { teamAccepted?: boolean; - constructor(githubIssueComments: GithubComment[]) { + constructor(githubComments: GithubComment[]) { super(Object.values(TeamAcceptedHeader)); - const teamAcceptedComment = githubIssueComments.find((comment) => this.test(comment.body)); + this.findConformingComment(githubComments); - if (teamAcceptedComment === undefined) { + if (this.parseFailure) { return; } diff --git a/src/app/core/models/templates/team-response-template.model.ts b/src/app/core/models/templates/team-response-template.model.ts index 30aaaaacc..0da036aca 100644 --- a/src/app/core/models/templates/team-response-template.model.ts +++ b/src/app/core/models/templates/team-response-template.model.ts @@ -13,23 +13,23 @@ export class TeamResponseTemplate extends Template { teamResponse: Section; duplicateOf: DuplicateOfSection; comment: IssueComment; - parseError: boolean; constructor(githubComments: GithubComment[]) { super(Object.values(TeamResponseHeaders)); - const comment = githubComments.find((githubComment: GithubComment) => this.test(githubComment.body)); - if (comment === undefined) { - this.parseError = true; + const templateConformingComment = this.findConformingComment(githubComments); + + if (this.parseFailure) { return; } + this.comment = { - ...comment, - description: comment.body, - createdAt: comment.created_at, - updatedAt: comment.updated_at + ...templateConformingComment, + description: templateConformingComment.body, + createdAt: templateConformingComment.created_at, + updatedAt: templateConformingComment.updated_at }; - const commentsContent: string = comment.body; + const commentsContent: string = templateConformingComment.body; this.teamResponse = this.parseTeamResponse(commentsContent); this.duplicateOf = this.parseDuplicateOf(commentsContent); } diff --git a/src/app/core/models/templates/template.model.ts b/src/app/core/models/templates/template.model.ts index 9da79a0fc..a460e4ce5 100644 --- a/src/app/core/models/templates/template.model.ts +++ b/src/app/core/models/templates/template.model.ts @@ -1,8 +1,10 @@ +import { GithubComment } from '../github/github-comment.model'; import { SectionalDependency } from './sections/section.model'; export abstract class Template { headers: Header[]; regex: RegExp; + parseFailure: boolean; protected constructor(headers: Header[]) { this.headers = headers; @@ -30,6 +32,17 @@ export abstract class Template { this.regex.lastIndex = 0; return numOfMatch >= this.headers.length; } + + /** + * Finds a comment that conforms to the template + */ + findConformingComment(githubComments: GithubComment[]): GithubComment { + const templateConformingComment = githubComments.find((githubComment) => this.test(githubComment.body)); + if (templateConformingComment === undefined) { + this.parseFailure = true; + } + return templateConformingComment; + } } export class Header { diff --git a/src/app/core/models/templates/tester-response-template.model.ts b/src/app/core/models/templates/tester-response-template.model.ts index 7b50326d8..6d2b37cde 100644 --- a/src/app/core/models/templates/tester-response-template.model.ts +++ b/src/app/core/models/templates/tester-response-template.model.ts @@ -16,14 +16,13 @@ export class TesterResponseTemplate extends Template { comment: IssueComment; teamChosenSeverity?: string; teamChosenType?: string; - parseError: boolean; - constructor(githubIssueComments: GithubComment[]) { + constructor(githubComments: GithubComment[]) { super(Object.values(TesterResponseHeaders)); - const templateConformingComment = githubIssueComments.find((comment) => this.test(comment.body)); - if (templateConformingComment === undefined) { - this.parseError = true; + const templateConformingComment = this.findConformingComment(githubComments); + + if (this.parseFailure) { return; } diff --git a/src/app/core/models/templates/tutor-moderation-todo-template.model.ts b/src/app/core/models/templates/tutor-moderation-todo-template.model.ts index 97ffc03d7..e3b0b5d21 100644 --- a/src/app/core/models/templates/tutor-moderation-todo-template.model.ts +++ b/src/app/core/models/templates/tutor-moderation-todo-template.model.ts @@ -14,14 +14,17 @@ export class TutorModerationTodoTemplate extends Template { constructor(githubComments: GithubComment[]) { super(Object.values(tutorModerationTodoHeaders)); - const templateConformingComment = githubComments.find((comment) => this.test(comment.body)); - if (templateConformingComment) { - this.comment = { - ...templateConformingComment, - description: templateConformingComment.body - }; - this.moderation = this.parseModeration(this.comment.description); + const templateConformingComment = this.findConformingComment(githubComments); + + if (this.parseFailure) { + return; } + + this.comment = { + ...templateConformingComment, + description: templateConformingComment.body + }; + this.moderation = this.parseModeration(this.comment.description); } parseModeration(toParse: string): ModerationSection { From 9f259d6e514f9b05550d900e58a7a87342123c71 Mon Sep 17 00:00:00 2001 From: "Lee Xiong Jie, Isaac" <68138671+luminousleek@users.noreply.github.com> Date: Wed, 16 Mar 2022 12:35:23 +0800 Subject: [PATCH 03/13] Add unit tests for Team Accepted Template (#895) Previously this template did not have unit tests. Let's add some tests to test the parsing logic for TeamAcceptedTemplate. --- .../team-accepted-template.model.spec.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/model/templates/team-accepted-template.model.spec.ts diff --git a/tests/model/templates/team-accepted-template.model.spec.ts b/tests/model/templates/team-accepted-template.model.spec.ts new file mode 100644 index 000000000..d87f08aad --- /dev/null +++ b/tests/model/templates/team-accepted-template.model.spec.ts @@ -0,0 +1,33 @@ +import { GithubComment } from '../../../src/app/core/models/github/github-comment.model'; +import { TeamAcceptedMessage, TeamAcceptedTemplate } from '../../../src/app/core/models/templates/team-accepted-template.model'; + +import { TEAM_RESPONSE_MULTIPLE_DISAGREEMENT } from '../../constants/githubcomment.constants'; + +const EMPTY_BODY_GITHUB_COMMENT = { + body: '' +} as GithubComment; + +const ACCEPTED_MESSAGE_GITHUB_COMMENT = { + body: TeamAcceptedMessage +} as GithubComment; + +const hasAcceptedComment = [EMPTY_BODY_GITHUB_COMMENT, ACCEPTED_MESSAGE_GITHUB_COMMENT]; +const noAcceptedComment = [EMPTY_BODY_GITHUB_COMMENT, TEAM_RESPONSE_MULTIPLE_DISAGREEMENT]; + +describe('TeamAcceptedTemplate class', () => { + it('parses team accepted message correctly', () => { + const template = new TeamAcceptedTemplate([ACCEPTED_MESSAGE_GITHUB_COMMENT]); + + expect(template.teamAccepted).toBe(true); + }); + it('finds team accepted comment correctly', () => { + const template = new TeamAcceptedTemplate(hasAcceptedComment); + + expect(template.teamAccepted).toBe(true); + }); + it('does not find team accepted comment', () => { + const template = new TeamAcceptedTemplate(noAcceptedComment); + + expect(template.teamAccepted).not.toBe(true); + }); +}); From 205e779ac0f08b4689759b33594387664b911d5a Mon Sep 17 00:00:00 2001 From: Lee Chun Wei <47494777+chunweii@users.noreply.github.com> Date: Tue, 22 Mar 2022 21:59:44 +0800 Subject: [PATCH 04/13] Improve Duplicate Issue Search in Team Response Phase (#894) Choosing an issue as the parent of a duplicate issue can be challenging. Users can receive close to a hundred issues in the Team Response Phase so they would need to scroll through everything to find the correct issue. Let's add a search bar in the selection dropdown menu for users to search for the parent issue by name or ID. Also, let's include the ID of the parent duplicate issue for issues that has already been duplicated, to ease the finding of the parent issue. --- package.json | 1 + .../duplicateOf/duplicate-of.component.html | 10 ++++- .../duplicateOf/duplicate-of.component.ts | 41 +++++++++++++++--- .../shared/issue/issue-components.module.ts | 3 +- .../new-team-response.component.html | 13 +++++- .../new-team-response.component.ts | 42 ++++++++++++++++--- .../new-team-response.module.ts | 11 ++++- 7 files changed, 106 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 45a5c3d46..26dcf8981 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "karma-spec-reporter": "0.0.32", "moment": "^2.24.0", "ngx-markdown": "^8.2.1", + "ngx-mat-select-search": "^1.8.0", "node-fetch": "^2.6.0", "rxjs": "6.5.3", "tslib": "^1.9.0", diff --git a/src/app/shared/issue/duplicateOf/duplicate-of.component.html b/src/app/shared/issue/duplicateOf/duplicate-of.component.html index 5b6a65f5a..c2484e143 100644 --- a/src/app/shared/issue/duplicateOf/duplicate-of.component.html +++ b/src/app/shared/issue/duplicateOf/duplicate-of.component.html @@ -19,18 +19,26 @@ + + + diff --git a/src/app/shared/issue/duplicateOf/duplicate-of.component.ts b/src/app/shared/issue/duplicateOf/duplicate-of.component.ts index df0fb12d8..0fdda92ee 100644 --- a/src/app/shared/issue/duplicateOf/duplicate-of.component.ts +++ b/src/app/shared/issue/duplicateOf/duplicate-of.component.ts @@ -1,12 +1,15 @@ -import { Component, EventEmitter, Input, OnInit, Output, ViewChild, ViewEncapsulation } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild, ViewEncapsulation } from '@angular/core'; +import { FormControl } from '@angular/forms'; import { MatCheckbox, MatSelect, MatSelectChange } from '@angular/material'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { Observable, ReplaySubject, Subject } from 'rxjs'; +import { first, map, takeUntil } from 'rxjs/operators'; import { Issue, SEVERITY_ORDER } from '../../../core/models/issue.model'; import { ErrorHandlingService } from '../../../core/services/error-handling.service'; import { IssueService } from '../../../core/services/issue.service'; import { PermissionService } from '../../../core/services/permission.service'; import { PhaseService } from '../../../core/services/phase.service'; +import { TABLE_COLUMNS } from '../../issue-tables/issue-tables-columns'; +import { applySearchFilter } from '../../issue-tables/search-filter'; @Component({ selector: 'app-duplicate-of-component', @@ -14,9 +17,11 @@ import { PhaseService } from '../../../core/services/phase.service'; styleUrls: ['./duplicate-of.component.css'], encapsulation: ViewEncapsulation.None }) -export class DuplicateOfComponent implements OnInit { +export class DuplicateOfComponent implements OnInit, OnDestroy { isEditing = false; duplicatedIssueList: Observable; + searchFilterCtrl: FormControl = new FormControl(); + filteredDuplicateIssueList: ReplaySubject = new ReplaySubject(1); @Input() issue: Issue; @@ -25,6 +30,9 @@ export class DuplicateOfComponent implements OnInit { @ViewChild(MatSelect, { static: true }) duplicateOfSelection: MatSelect; @ViewChild(MatCheckbox, { static: true }) duplicatedCheckbox: MatCheckbox; + // A subject that will emit a signal when this component is being destroyed + private _onDestroy = new Subject(); + // Max chars visible for a duplicate entry in duplicates dropdown list. readonly MAX_TITLE_LENGTH_FOR_DUPLICATE_ISSUE = 17; // Max chars visible for a non-duplicate entry in duplicates dropdown list. @@ -52,8 +60,22 @@ export class DuplicateOfComponent implements OnInit { return issue.title.length > maxTitleLength; } + ngOnDestroy(): void { + this._onDestroy.next(); // Emits the destroy signal + this._onDestroy.complete(); + } + ngOnInit() { this.duplicatedIssueList = this.getDupIssueList(); + // Populate the filtered list with all the issues first + this.duplicatedIssueList.pipe(first()).subscribe((issues) => this.filteredDuplicateIssueList.next(issues)); + this.searchFilterCtrl.valueChanges.pipe(takeUntil(this._onDestroy)).subscribe((_) => this.filterIssues()); + } + + private filterIssues(): void { + this.changeFilter(this.duplicatedIssueList, this.searchFilterCtrl.value).subscribe((issues) => + this.filteredDuplicateIssueList.next(issues) + ); } updateDuplicateStatus(event: MatSelectChange) { @@ -74,7 +96,7 @@ export class DuplicateOfComponent implements OnInit { if (SEVERITY_ORDER[this.issue.severity] > SEVERITY_ORDER[issue.severity]) { reason.push('Issue of lower priority'); } else if (issue.duplicated || !!issue.duplicateOf) { - reason.push('A duplicated issue'); + reason.push('Duplicate of #' + issue.duplicateOf); } } return reason.join(', '); @@ -118,6 +140,15 @@ export class DuplicateOfComponent implements OnInit { return clone; } + private changeFilter(issuesObservable: Observable, searchInputString): Observable { + return issuesObservable.pipe( + first(), + map((issues) => { + return applySearchFilter(searchInputString, [TABLE_COLUMNS.ID, TABLE_COLUMNS.TITLE], this.issueService, issues); + }) + ); + } + private getDupIssueList(): Observable { return this.issueService.issues$.pipe( map((issues) => { diff --git a/src/app/shared/issue/issue-components.module.ts b/src/app/shared/issue/issue-components.module.ts index 3c11fba21..9227f2cad 100644 --- a/src/app/shared/issue/issue-components.module.ts +++ b/src/app/shared/issue/issue-components.module.ts @@ -1,6 +1,7 @@ import { NgModule } from '@angular/core'; import { MatProgressBarModule } from '@angular/material'; import { MarkdownModule } from 'ngx-markdown'; +import { NgxMatSelectSearchModule } from 'ngx-mat-select-search'; import { CommentEditorModule } from '../comment-editor/comment-editor.module'; import { SharedModule } from '../shared.module'; import { AssigneeComponent } from './assignee/assignee.component'; @@ -13,7 +14,7 @@ import { TitleComponent } from './title/title.component'; import { UnsureCheckboxComponent } from './unsure-checkbox/unsure-checkbox.component'; @NgModule({ - imports: [SharedModule, CommentEditorModule, MatProgressBarModule, MarkdownModule.forChild()], + imports: [SharedModule, CommentEditorModule, MatProgressBarModule, NgxMatSelectSearchModule, MarkdownModule.forChild()], declarations: [ TitleComponent, DescriptionComponent, diff --git a/src/app/shared/view-issue/new-team-response/new-team-response.component.html b/src/app/shared/view-issue/new-team-response/new-team-response.component.html index 3fbfb905d..82bee3bfb 100644 --- a/src/app/shared/view-issue/new-team-response/new-team-response.component.html +++ b/src/app/shared/view-issue/new-team-response/new-team-response.component.html @@ -17,7 +17,18 @@ - + + + + #{{ issue.id }}: {{ issue.title }} ({{ getDisabledDupOptionErrorText(issue) }}) diff --git a/src/app/shared/view-issue/new-team-response/new-team-response.component.ts b/src/app/shared/view-issue/new-team-response/new-team-response.component.ts index b821ad1d1..be6325d34 100644 --- a/src/app/shared/view-issue/new-team-response/new-team-response.component.ts +++ b/src/app/shared/view-issue/new-team-response/new-team-response.component.ts @@ -1,9 +1,9 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, NgForm, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material'; import { MatCheckboxChange } from '@angular/material/checkbox'; -import { Observable, throwError } from 'rxjs'; -import { finalize, flatMap, map } from 'rxjs/operators'; +import { Observable, ReplaySubject, Subject, throwError } from 'rxjs'; +import { finalize, first, flatMap, map, takeUntil } from 'rxjs/operators'; import { IssueComment } from '../../../core/models/comment.model'; import { Conflict } from '../../../core/models/conflict/conflict.model'; import { Issue, SEVERITY_ORDER, STATUS } from '../../../core/models/issue.model'; @@ -11,6 +11,8 @@ import { ErrorHandlingService } from '../../../core/services/error-handling.serv import { IssueService } from '../../../core/services/issue.service'; import { LabelService } from '../../../core/services/label.service'; import { PhaseService } from '../../../core/services/phase.service'; +import { TABLE_COLUMNS } from '../../issue-tables/issue-tables-columns'; +import { applySearchFilter } from '../../issue-tables/search-filter'; import { SUBMIT_BUTTON_TEXT } from '../view-issue.component'; import { ConflictDialogComponent } from './conflict-dialog/conflict-dialog.component'; @@ -19,11 +21,13 @@ import { ConflictDialogComponent } from './conflict-dialog/conflict-dialog.compo templateUrl: './new-team-response.component.html', styleUrls: ['./new-team-response.component.css'] }) -export class NewTeamResponseComponent implements OnInit { +export class NewTeamResponseComponent implements OnInit, OnDestroy { newTeamResponseForm: FormGroup; teamMembers: string[]; duplicatedIssueList: Observable; conflict: Conflict; + searchFilterCtrl: FormControl = new FormControl(); + filteredDuplicateIssueList: ReplaySubject = new ReplaySubject(1); isFormPending = false; @@ -32,6 +36,9 @@ export class NewTeamResponseComponent implements OnInit { @Input() issue: Issue; @Output() issueUpdated = new EventEmitter(); + // A subject that will emit a signal when this component is being destroyed + private _onDestroy = new Subject(); + constructor( public issueService: IssueService, private formBuilder: FormBuilder, @@ -46,6 +53,9 @@ export class NewTeamResponseComponent implements OnInit { return member.loginId; }); this.duplicatedIssueList = this.getDupIssueList(); + // Populate the filtered list with all the issues first + this.duplicatedIssueList.pipe(first()).subscribe((issues) => this.filteredDuplicateIssueList.next(issues)); + this.searchFilterCtrl.valueChanges.pipe(takeUntil(this._onDestroy)).subscribe((_) => this.filterIssues()); this.newTeamResponseForm = this.formBuilder.group({ description: [''], severity: [this.issue.severity, Validators.required], @@ -69,6 +79,26 @@ export class NewTeamResponseComponent implements OnInit { this.submitButtonText = SUBMIT_BUTTON_TEXT.SUBMIT; } + private filterIssues(): void { + this.changeFilter(this.duplicatedIssueList, this.searchFilterCtrl.value).subscribe((issues) => + this.filteredDuplicateIssueList.next(issues) + ); + } + + private changeFilter(issuesObservable: Observable, searchInputString): Observable { + return issuesObservable.pipe( + first(), + map((issues) => { + return applySearchFilter(searchInputString, [TABLE_COLUMNS.ID, TABLE_COLUMNS.TITLE], this.issueService, issues); + }) + ); + } + + ngOnDestroy(): void { + this._onDestroy.next(); // Emits the destroy signal + this._onDestroy.complete(); + } + submitNewTeamResponse(form: NgForm) { if (this.newTeamResponseForm.invalid) { return; @@ -154,7 +184,7 @@ export class NewTeamResponseComponent implements OnInit { if (SEVERITY_ORDER[this.severity.value] > SEVERITY_ORDER[issue.severity]) { reason.push('Issue of lower priority'); } else if (issue.duplicated || !!issue.duplicateOf) { - reason.push('A duplicated issue'); + reason.push('Duplicate of #' + issue.duplicateOf); } } return reason.join(', '); diff --git a/src/app/shared/view-issue/new-team-response/new-team-response.module.ts b/src/app/shared/view-issue/new-team-response/new-team-response.module.ts index d9dd9bc5f..46cfc7922 100644 --- a/src/app/shared/view-issue/new-team-response/new-team-response.module.ts +++ b/src/app/shared/view-issue/new-team-response/new-team-response.module.ts @@ -1,6 +1,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { MarkdownModule } from 'ngx-markdown'; +import { NgxMatSelectSearchModule } from 'ngx-mat-select-search'; import { CommentEditorModule } from '../../comment-editor/comment-editor.module'; import { IssueComponentsModule } from '../../issue/issue-components.module'; import { LabelDropdownModule } from '../../label-dropdown/label-dropdown.module'; @@ -11,7 +12,15 @@ import { NewTeamResponseComponent } from './new-team-response.component'; @NgModule({ exports: [NewTeamResponseComponent, ConflictDialogComponent], declarations: [NewTeamResponseComponent, ConflictDialogComponent], - imports: [CommonModule, CommentEditorModule, SharedModule, IssueComponentsModule, LabelDropdownModule, MarkdownModule.forChild()], + imports: [ + CommonModule, + CommentEditorModule, + SharedModule, + IssueComponentsModule, + LabelDropdownModule, + MarkdownModule.forChild(), + NgxMatSelectSearchModule + ], entryComponents: [ConflictDialogComponent] }) export class NewTeamResponseModule {} From fd0d954dc73c084bc230307688aec02670b139b5 Mon Sep 17 00:00:00 2001 From: Ding YuChen Date: Thu, 24 Mar 2022 17:09:31 +0800 Subject: [PATCH 05/13] Remove lower priority check for duplicate issues (#913) Currently, CATcher doesn't allow assigning a duplicate if the duplicate has a higher priority than the original. The team needs to downgrade the priority of the duplicate to match the original before it can be assigned as a duplicate. Let's remove this function for an improved user experience. --- .../issue/duplicateOf/duplicate-of.component.ts | 14 +++----------- .../new-team-response.component.ts | 14 +++----------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/src/app/shared/issue/duplicateOf/duplicate-of.component.ts b/src/app/shared/issue/duplicateOf/duplicate-of.component.ts index 0fdda92ee..7f3e8153e 100644 --- a/src/app/shared/issue/duplicateOf/duplicate-of.component.ts +++ b/src/app/shared/issue/duplicateOf/duplicate-of.component.ts @@ -3,7 +3,7 @@ import { FormControl } from '@angular/forms'; import { MatCheckbox, MatSelect, MatSelectChange } from '@angular/material'; import { Observable, ReplaySubject, Subject } from 'rxjs'; import { first, map, takeUntil } from 'rxjs/operators'; -import { Issue, SEVERITY_ORDER } from '../../../core/models/issue.model'; +import { Issue } from '../../../core/models/issue.model'; import { ErrorHandlingService } from '../../../core/services/error-handling.service'; import { IssueService } from '../../../core/services/issue.service'; import { PermissionService } from '../../../core/services/permission.service'; @@ -87,19 +87,11 @@ export class DuplicateOfComponent implements OnInit, OnDestroy { } dupIssueOptionIsDisabled(issue: Issue): boolean { - return SEVERITY_ORDER[this.issue.severity] > SEVERITY_ORDER[issue.severity] || issue.duplicated || !!issue.duplicateOf; + return issue.duplicated || !!issue.duplicateOf; } getDisabledDupOptionErrorText(issue: Issue): string { - const reason = new Array(); - if (this.dupIssueOptionIsDisabled(issue)) { - if (SEVERITY_ORDER[this.issue.severity] > SEVERITY_ORDER[issue.severity]) { - reason.push('Issue of lower priority'); - } else if (issue.duplicated || !!issue.duplicateOf) { - reason.push('Duplicate of #' + issue.duplicateOf); - } - } - return reason.join(', '); + return this.dupIssueOptionIsDisabled(issue) ? 'A duplicated issue' : ''; } handleCheckboxChange(event) { diff --git a/src/app/shared/view-issue/new-team-response/new-team-response.component.ts b/src/app/shared/view-issue/new-team-response/new-team-response.component.ts index be6325d34..a3dce6fd9 100644 --- a/src/app/shared/view-issue/new-team-response/new-team-response.component.ts +++ b/src/app/shared/view-issue/new-team-response/new-team-response.component.ts @@ -6,7 +6,7 @@ import { Observable, ReplaySubject, Subject, throwError } from 'rxjs'; import { finalize, first, flatMap, map, takeUntil } from 'rxjs/operators'; import { IssueComment } from '../../../core/models/comment.model'; import { Conflict } from '../../../core/models/conflict/conflict.model'; -import { Issue, SEVERITY_ORDER, STATUS } from '../../../core/models/issue.model'; +import { Issue, STATUS } from '../../../core/models/issue.model'; import { ErrorHandlingService } from '../../../core/services/error-handling.service'; import { IssueService } from '../../../core/services/issue.service'; import { LabelService } from '../../../core/services/label.service'; @@ -175,19 +175,11 @@ export class NewTeamResponseComponent implements OnInit, OnDestroy { } dupIssueOptionIsDisabled(issue: Issue): boolean { - return SEVERITY_ORDER[this.severity.value] > SEVERITY_ORDER[issue.severity] || issue.duplicated || !!issue.duplicateOf; + return issue.duplicated || !!issue.duplicateOf; } getDisabledDupOptionErrorText(issue: Issue): string { - const reason = new Array(); - if (this.dupIssueOptionIsDisabled(issue)) { - if (SEVERITY_ORDER[this.severity.value] > SEVERITY_ORDER[issue.severity]) { - reason.push('Issue of lower priority'); - } else if (issue.duplicated || !!issue.duplicateOf) { - reason.push('Duplicate of #' + issue.duplicateOf); - } - } - return reason.join(', '); + return this.dupIssueOptionIsDisabled(issue) ? 'A duplicated issue' : ''; } handleChangeOfDuplicateCheckbox(event: MatCheckboxChange) { From bfa5aad9fdbf2797e8548110e8a9bdfdf9e7ece4 Mon Sep 17 00:00:00 2001 From: Gabriel Goh <77230723+gycgabriel@users.noreply.github.com> Date: Tue, 29 Mar 2022 13:43:09 +0800 Subject: [PATCH 06/13] Add a link to report CATcher problems (#907) There is no link to CATcher issue tracker for users to report CATcher problems. Let's add a link so users can easily reach this issue tracker to report CATcher problems. --- e2e/spec/login/login.e2e-spec.ts | 2 +- src/app/shared/layout/header.component.html | 5 ++++- src/app/shared/layout/header.component.ts | 6 ++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/e2e/spec/login/login.e2e-spec.ts b/e2e/spec/login/login.e2e-spec.ts index 37ec01563..1ee04652c 100644 --- a/e2e/spec/login/login.e2e-spec.ts +++ b/e2e/spec/login/login.e2e-spec.ts @@ -10,7 +10,7 @@ describe("CATcher's Login Page", () => { }); it('displays "CATcher" in header bar', async () => { - expect(await page.getTitle()).toEqual(`CATcher v${AppConfig.version}\nreceipt`); + expect(await page.getTitle()).toEqual(`CATcher v${AppConfig.version}\nreceipt\nmail`); }); it('allows users to authenticate themselves', async () => { diff --git a/src/app/shared/layout/header.component.html b/src/app/shared/layout/header.component.html index 5df7e3bf4..ab325eb4e 100644 --- a/src/app/shared/layout/header.component.html +++ b/src/app/shared/layout/header.component.html @@ -37,13 +37,16 @@ + + diff --git a/src/app/shared/action-toasters/undo-action/undo-action.component.ts b/src/app/shared/action-toasters/undo-action/undo-action.component.ts new file mode 100644 index 000000000..dbd34dc1c --- /dev/null +++ b/src/app/shared/action-toasters/undo-action/undo-action.component.ts @@ -0,0 +1,10 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_SNACK_BAR_DATA, MatSnackBarRef } from '@angular/material'; + +@Component({ + selector: 'app-undo-action', + templateUrl: './undo-action.component.html' +}) +export class UndoActionComponent { + constructor(public snackBarRef: MatSnackBarRef, @Inject(MAT_SNACK_BAR_DATA) public data: any) {} +} diff --git a/src/app/shared/issue-tables/issue-tables.component.ts b/src/app/shared/issue-tables/issue-tables.component.ts index 9383db144..653ff3eb3 100644 --- a/src/app/shared/issue-tables/issue-tables.component.ts +++ b/src/app/shared/issue-tables/issue-tables.component.ts @@ -1,5 +1,5 @@ import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core'; -import { MatPaginator, MatSort } from '@angular/material'; +import { MatPaginator, MatSnackBar, MatSort } from '@angular/material'; import { finalize } from 'rxjs/operators'; import { Issue, STATUS } from '../../core/models/issue.model'; import { DialogService } from '../../core/services/dialog.service'; @@ -11,6 +11,7 @@ import { LoggingService } from '../../core/services/logging.service'; import { PermissionService } from '../../core/services/permission.service'; import { PhaseService } from '../../core/services/phase.service'; import { UserService } from '../../core/services/user.service'; +import { UndoActionComponent } from '../../shared/action-toasters/undo-action/undo-action.component'; import { IssuesDataTable } from './IssuesDataTable'; export enum ACTION_BUTTONS { @@ -28,6 +29,8 @@ export enum ACTION_BUTTONS { styleUrls: ['./issue-tables.component.css'] }) export class IssueTablesComponent implements OnInit, AfterViewInit { + snackBarAutoCloseTime = 3000; + @Input() headers: string[]; @Input() actions: ACTION_BUTTONS[]; @Input() filters?: any = undefined; @@ -54,7 +57,8 @@ export class IssueTablesComponent implements OnInit, AfterViewInit { private phaseService: PhaseService, private errorHandlingService: ErrorHandlingService, private loggingService: LoggingService, - private dialogService: DialogService + private dialogService: DialogService, + private snackBar: MatSnackBar = null ) {} ngOnInit() { @@ -175,6 +179,28 @@ export class IssueTablesComponent implements OnInit, AfterViewInit { } ); event.stopPropagation(); + + let snackBarRef = null; + snackBarRef = this.snackBar.openFromComponent(UndoActionComponent, { + data: { message: `Deleted issue ${id}` }, + duration: this.snackBarAutoCloseTime + }); + snackBarRef.onAction().subscribe(() => { + this.undeleteIssue(id, event); + }); + } + + undeleteIssue(id: number, event: Event) { + this.loggingService.info(`IssueTablesComponent: Undeleting Issue ${id}`); + this.issueService.undeleteIssue(id).subscribe( + (reopenedIssue) => {}, + (error) => { + this.errorHandlingService.handleError(error); + } + ); + event.stopPropagation(); + + this.snackBar.open(`Restored issue ${id}`, '', { duration: this.snackBarAutoCloseTime }); } openDeleteDialog(id: number, event: Event) { diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index d6c8eceea..cfc65f82c 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -4,6 +4,7 @@ import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { FormDisableControlDirective } from '../core/directives/form-disable-control.directive'; +import { ActionToasterModule } from './action-toasters/action-toasters.module'; import { ErrorToasterModule } from './error-toasters/error-toaster.module'; import { MaterialModule } from './material.module'; @@ -18,7 +19,8 @@ import { MaterialModule } from './material.module'; HttpClientModule, RouterModule, MaterialModule, - ErrorToasterModule + ErrorToasterModule, + ActionToasterModule ] }) export class SharedModule {} From b8a5a34ecfebfcbeab4b831964b6bf834bd9ea88 Mon Sep 17 00:00:00 2001 From: Lee Chun Wei <47494777+chunweii@users.noreply.github.com> Date: Sat, 23 Apr 2022 18:14:37 +0800 Subject: [PATCH 13/13] Remove assignee check API (#933) In a previous PR, there was a buggy implementation of checking whether assignees are authorized to be assigned due to lack of pagination. It makes the code unnecessarily complex. Let's remove the check altogether and just display the "Validation Failed" message from GitHub to the user as it is. --- src/app/app.module.ts | 2 +- .../core/services/error-handling.service.ts | 2 ++ .../factories/factory.issue.service.ts | 6 ++-- src/app/core/services/github.service.ts | 22 --------------- src/app/core/services/issue.service.ts | 28 ++++++++----------- .../issue/assignee/assignee.component.ts | 2 +- .../issues-faulty.component.spec.ts | 2 +- .../issues-pending.component.spec.ts | 2 +- .../issues-responded.component.spec.ts | 2 +- .../shared/issue-tables/search-filter.spec.ts | 2 +- .../issue/assignee/assignee.component.spec.ts | 11 ++------ 11 files changed, 27 insertions(+), 54 deletions(-) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 59a5fde75..a90d87bf8 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -87,7 +87,7 @@ import { SharedModule } from './shared/shared.module'; { provide: IssueService, useFactory: IssueServiceFactory, - deps: [GithubService, UserService, PhaseService, ElectronService, DataService] + deps: [GithubService, UserService, PhaseService, ElectronService, DataService, LoggingService] }, { provide: ErrorHandler, diff --git a/src/app/core/services/error-handling.service.ts b/src/app/core/services/error-handling.service.ts index ea27b5669..b38eec07b 100644 --- a/src/app/core/services/error-handling.service.ts +++ b/src/app/core/services/error-handling.service.ts @@ -25,6 +25,8 @@ export class ErrorHandlingService implements ErrorHandler { this.handleHttpError(error, actionCallback); } else if (error.constructor.name === 'RequestError') { this.handleHttpError(error as RequestError, actionCallback); + } else if (typeof error === 'string') { + this.handleGeneralError(error); } else { this.handleGeneralError(error.message || JSON.stringify(error)); } diff --git a/src/app/core/services/factories/factory.issue.service.ts b/src/app/core/services/factories/factory.issue.service.ts index f167da027..ea67a2271 100644 --- a/src/app/core/services/factories/factory.issue.service.ts +++ b/src/app/core/services/factories/factory.issue.service.ts @@ -3,6 +3,7 @@ import { DataService } from '../data.service'; import { ElectronService } from '../electron.service'; import { GithubService } from '../github.service'; import { IssueService } from '../issue.service'; +import { LoggingService } from '../logging.service'; import { MockIssueService } from '../mocks/mock.issue.service'; import { PhaseService } from '../phase.service'; import { UserService } from '../user.service'; @@ -12,10 +13,11 @@ export function IssueServiceFactory( userService: UserService, phaseService: PhaseService, electronService: ElectronService, - dataService: DataService + dataService: DataService, + logger: LoggingService ) { if (AppConfig.test) { return new MockIssueService(githubService, phaseService, dataService); } - return new IssueService(githubService, userService, phaseService, electronService, dataService); + return new IssueService(githubService, userService, phaseService, electronService, dataService, logger); } diff --git a/src/app/core/services/github.service.ts b/src/app/core/services/github.service.ts index 0916fab54..ef390614f 100644 --- a/src/app/core/services/github.service.ts +++ b/src/app/core/services/github.service.ts @@ -259,28 +259,6 @@ export class GithubService { octokit.issues.updateLabel({ owner: ORG_NAME, repo: REPO, name: labelName, current_name: labelName, color: labelColor }); } - /** - * Checks if the given list of users are allowed to be assigned to an issue. - * @param assignees - GitHub usernames to be checked - */ - areUsersAssignable(assignees: string[]): Observable { - return from( - octokit.issues.listAssignees({ - owner: ORG_NAME, - repo: REPO - }) - ).pipe( - map(({ data }: { data: { login: string }[] }) => data.map(({ login }) => login)), - map((assignables: string[]) => - assignees.forEach((assignee) => { - if (!assignables.includes(assignee)) { - throw new Error(`Cannot assign ${assignee} to the issue. Please check if ${assignee} is authorized.`); - } - }) - ) - ); - } - closeIssue(id: number): Observable { return from(octokit.issues.update({ owner: ORG_NAME, repo: REPO, issue_number: id, state: 'closed' })).pipe( map((response: GithubResponse) => { diff --git a/src/app/core/services/issue.service.ts b/src/app/core/services/issue.service.ts index 7671d1df7..bdad5e1bf 100644 --- a/src/app/core/services/issue.service.ts +++ b/src/app/core/services/issue.service.ts @@ -14,6 +14,7 @@ import { appVersion } from './application.service'; import { DataService } from './data.service'; import { ElectronService } from './electron.service'; import { GithubService } from './github.service'; +import { LoggingService } from './logging.service'; import { PhaseService } from './phase.service'; import { UserService } from './user.service'; @@ -42,7 +43,8 @@ export class IssueService { private userService: UserService, private phaseService: PhaseService, private electronService: ElectronService, - private dataService: DataService + private dataService: DataService, + private logger: LoggingService ) { this.issues$ = new BehaviorSubject(new Array()); } @@ -134,11 +136,6 @@ export class IssueService { .pipe(map((response: GithubIssue) => this.createIssueModel(response))); } - updateIssueWithAssigneeCheck(issue: Issue): Observable { - const assignees = this.phaseService.currentPhase === Phase.phaseModeration ? [] : issue.assignees; - return this.githubService.areUsersAssignable(assignees).pipe(flatMap(() => this.updateIssue(issue))); - } - updateIssue(issue: Issue): Observable { const assignees = this.phaseService.currentPhase === Phase.phaseModeration ? [] : issue.assignees; return this.githubService @@ -147,6 +144,10 @@ export class IssueService { map((response: GithubIssue) => { response.comments = issue.githubComments; return this.createIssueModel(response); + }), + catchError((err) => { + this.logger.error(err); // Log full details of error first + return throwError(err.response.data.message); // More readable error message }) ); } @@ -190,16 +191,11 @@ export class IssueService { createTeamResponse(issue: Issue): Observable { const teamResponse = issue.createGithubTeamResponse(); - return this.githubService.areUsersAssignable(issue.assignees || []).pipe( - flatMap(() => - this.githubService.createIssueComment(issue.id, teamResponse).pipe( - flatMap((githubComment: GithubComment) => { - issue.githubComments = [githubComment, ...issue.githubComments.filter((c) => c.id !== githubComment.id)]; - return this.updateIssue(issue); - }) - ) - ), - catchError((err) => throwError(err)) + return this.githubService.createIssueComment(issue.id, teamResponse).pipe( + flatMap((githubComment: GithubComment) => { + issue.githubComments = [githubComment, ...issue.githubComments.filter((c) => c.id !== githubComment.id)]; + return this.updateIssue(issue); + }) ); } diff --git a/src/app/shared/issue/assignee/assignee.component.ts b/src/app/shared/issue/assignee/assignee.component.ts index 0e20fb74f..c56de4ecf 100644 --- a/src/app/shared/issue/assignee/assignee.component.ts +++ b/src/app/shared/issue/assignee/assignee.component.ts @@ -54,7 +54,7 @@ export class AssigneeComponent implements OnInit { const newIssue = this.issue.clone(this.phaseService.currentPhase); const oldAssignees = newIssue.assignees; newIssue.assignees = this.assignees; - this.issueService.updateIssueWithAssigneeCheck(newIssue).subscribe( + this.issueService.updateIssue(newIssue).subscribe( (updatedIssue: Issue) => { this.issueUpdated.emit(updatedIssue); // Update assignees of duplicate issues diff --git a/tests/app/phase-team-response/issues-faulty/issues-faulty.component.spec.ts b/tests/app/phase-team-response/issues-faulty/issues-faulty.component.spec.ts index 9664ca9d5..83381a1a2 100644 --- a/tests/app/phase-team-response/issues-faulty/issues-faulty.component.spec.ts +++ b/tests/app/phase-team-response/issues-faulty/issues-faulty.component.spec.ts @@ -17,7 +17,7 @@ describe('IssuesFaultyComponent', () => { const DUMMY_RESPONSE = 'dummy response'; beforeEach(() => { - issueService = new IssueService(null, null, null, null, null); + issueService = new IssueService(null, null, null, null, null, null); issueService.updateLocalStore(dummyIssue); issuesFaultyComponent = new IssuesFaultyComponent(issueService, userService, null); issuesFaultyComponent.ngOnInit(); diff --git a/tests/app/phase-team-response/issues-pending/issues-pending.component.spec.ts b/tests/app/phase-team-response/issues-pending/issues-pending.component.spec.ts index 22df4e78d..b99657927 100644 --- a/tests/app/phase-team-response/issues-pending/issues-pending.component.spec.ts +++ b/tests/app/phase-team-response/issues-pending/issues-pending.component.spec.ts @@ -11,7 +11,7 @@ describe('IssuesPendingComponent', () => { const dummyTeam: Team = TEAM_4; let dummyIssue: Issue; let issuesPendingComponent: IssuesPendingComponent; - const issueService: IssueService = new IssueService(null, null, null, null, null); + const issueService: IssueService = new IssueService(null, null, null, null, null, null); const userService: UserService = new UserService(null, null); userService.currentUser = USER_Q; const DUMMY_DUPLICATE_ISSUE_ID = 1; diff --git a/tests/app/phase-team-response/issues-responded/issues-responded.component.spec.ts b/tests/app/phase-team-response/issues-responded/issues-responded.component.spec.ts index dcda46eac..1b6d18802 100644 --- a/tests/app/phase-team-response/issues-responded/issues-responded.component.spec.ts +++ b/tests/app/phase-team-response/issues-responded/issues-responded.component.spec.ts @@ -12,7 +12,7 @@ describe('IssuesRespondedComponent', () => { const DUMMY_RESPONSE = 'dummy response'; let dummyIssue: Issue; - const issueService = new IssueService(null, null, null, null, null); + const issueService = new IssueService(null, null, null, null, null, null); const userService = new UserService(null, null); userService.currentUser = USER_Q; const issuesRespondedComponent = new IssuesRespondedComponent(issueService, userService); diff --git a/tests/app/shared/issue-tables/search-filter.spec.ts b/tests/app/shared/issue-tables/search-filter.spec.ts index 0a03446c7..e51fdbc39 100644 --- a/tests/app/shared/issue-tables/search-filter.spec.ts +++ b/tests/app/shared/issue-tables/search-filter.spec.ts @@ -45,7 +45,7 @@ describe('search-filter', () => { TABLE_COLUMNS.ASSIGNEE, TABLE_COLUMNS.DUPLICATED_ISSUES ]; - const issueService: IssueService = new IssueService(null, null, null, null, null); + const issueService: IssueService = new IssueService(null, null, null, null, null, null); beforeEach(() => { issueService.updateLocalStore(mediumSeverityIssueWithResponse); diff --git a/tests/app/shared/issue/assignee/assignee.component.spec.ts b/tests/app/shared/issue/assignee/assignee.component.spec.ts index 05437c6d9..7c89a20fc 100644 --- a/tests/app/shared/issue/assignee/assignee.component.spec.ts +++ b/tests/app/shared/issue/assignee/assignee.component.spec.ts @@ -51,12 +51,7 @@ describe('AssigneeComponent', () => { }); const phaseService: any = jasmine.createSpyObj('PhaseService', [], { currentPhase: Phase.phaseTeamResponse }); - const issueService: any = jasmine.createSpyObj('IssueService', [ - 'getDuplicateIssuesFor', - 'getLatestIssue', - 'updateIssue', - 'updateIssueWithAssigneeCheck' - ]); + const issueService: any = jasmine.createSpyObj('IssueService', ['getDuplicateIssuesFor', 'getLatestIssue', 'updateIssue']); const permissionsService: any = jasmine.createSpyObj('PermissionService', ['isIssueLabelsEditable']); beforeEach(async(() => { @@ -131,7 +126,7 @@ describe('AssigneeComponent', () => { const updatedDuplicateIssue = duplicateIssue.clone(phaseService.currentPhase); updatedDuplicateIssue.assignees = [testStudent.loginId]; - expect(issueService.updateIssueWithAssigneeCheck).toHaveBeenCalledWith(updatedIssue); + expect(issueService.updateIssue).toHaveBeenCalledWith(updatedIssue); expect(issueService.updateIssue).toHaveBeenCalledWith(updatedDuplicateIssue); }); @@ -149,7 +144,7 @@ describe('AssigneeComponent', () => { function dispatchClosedEvent() { const matSelectElement: HTMLElement = debugElement.query(By.css('.mat-select')).nativeElement; - issueService.updateIssueWithAssigneeCheck.and.callFake((updatedIssue: Issue) => of(updatedIssue)); + issueService.updateIssue.and.callFake((updatedIssue: Issue) => of(updatedIssue)); matSelectElement.dispatchEvent(new Event('closed')); fixture.detectChanges(); }