diff --git a/src/app/core/services/issue-comment.service.ts b/src/app/core/services/issue-comment.service.ts index 0c3951dd9..fdb65cc4e 100644 --- a/src/app/core/services/issue-comment.service.ts +++ b/src/app/core/services/issue-comment.service.ts @@ -52,6 +52,12 @@ export class IssueCommentService { `# Items for the Tester to Verify\n${this.getTesterResponsesString(testerResponses)}`; } + // Template url: https://github.com/CATcher-org/templates#dev-response-phase + createGithubTeamResponse(teamResponse: string, duplicateOf: number): string { + return `# Team\'s Response\n${teamResponse}\n ` + + `## Duplicate status (if any):\n${duplicateOf ? `Duplicate of #${duplicateOf}` : `--`}`; + } + // Template url: https://github.com/CATcher-org/templates#tutor-moderation createGithubTutorResponse(issueDisputes: IssueDispute[]): string { let tutorResponseString = '# Tutor Moderation\n\n'; diff --git a/src/app/core/services/issue.service.ts b/src/app/core/services/issue.service.ts index b831e7042..4cc05c6cd 100644 --- a/src/app/core/services/issue.service.ts +++ b/src/app/core/services/issue.service.ts @@ -106,8 +106,7 @@ export class IssueService { private createGithubIssueDescription(issue: Issue): string { switch (this.phaseService.currentPhase) { case Phase.phaseTeamResponse: - return `# Description\n${issue.description}\n# Team\'s Response\n${issue.teamResponse}\n ` + - `## State the duplicated issue here, if any\n${issue.duplicateOf ? `Duplicate of #${issue.duplicateOf}` : `--`}`; + return `# Description\n${issue.description}\n`; case Phase.phaseModeration: return `# Description\n${issue.description}\n# Team\'s Response\n${issue.teamResponse}\n ` + // `## State the duplicated issue here, if any\n${issue.duplicateOf ? `Duplicate of #${issue.duplicateOf}` : `--`}\n` + @@ -430,6 +429,14 @@ export class IssueService { return this.issueCommentService.getIssueComments(issueId, this.isIssueReloaded).pipe( map((issueComments: IssueComments) => { const issueComment = this.getIssueComment(issueComments); + let teamResponse = issueInJson['teamResponse']; + let duplicateOf = issueInJson['duplicateOf']; + if ( !!issueComment && this.phaseService.currentPhase === Phase.phaseTesterResponse) { + teamResponse = this.parseTeamResponseForTesterResponsePhase(issueComment.description); + } else if ( !!issueComment && this.phaseService.currentPhase === Phase.phaseTeamResponse) { + teamResponse = this.parseTeamResponseForTeamResponsePhase(issueComment.description); + duplicateOf = this.parseDuplicateOfForTeamResponsePhase(issueComment.description); + } const incompleteIssueDisputes: IssueDispute[] = issueInJson['issueDisputes']; this.isIssueReloaded = false; return { @@ -441,10 +448,9 @@ export class IssueService { description: issueInJson['body'], teamAssigned: this.getTeamAssignedToIssue(issueInJson), todoList: this.getToDoList(issueComment, issueInJson['issueDisputes']), - teamResponse: this.phaseService.currentPhase === Phase.phaseTesterResponse && !!issueComment ? - this.parseTeamResponse(issueComment.description) : issueInJson['teamResponse'], + teamResponse: teamResponse, tutorResponse: issueInJson['tutorResponse'], - duplicateOf: issueInJson['duplicateOf'], + duplicateOf: +duplicateOf, testerResponses: this.phaseService.currentPhase === Phase.phaseTesterResponse && !!issueComment ? this.parseTesterResponse(issueComment.description) : issueInJson['testerResponses'], issueComment: issueComment, @@ -481,13 +487,21 @@ export class IssueService { ); } + /** + * Searches for a comment in the issue that matches the required template. + * @return IssueComment - Latest Comment that matches the required template. + */ getIssueComment(issueComments: IssueComments): IssueComment { let regex = /# *Team\'?s Response[\n\r]*[\s\S]*# Items for the Tester to Verify/gi; if (this.phaseService.currentPhase === Phase.phaseModeration) { regex = /# Tutor Moderation[\n\r]*#{2} *:question: *.*[\n\r]*.*[\n\r]*[\s\S]*?(?=-{19})/gi; + } else if (this.phaseService.currentPhase === Phase.phaseTeamResponse) { + regex = /# Team's Response[\r\n]*[\S\s]*?[\r\n]*## Duplicate status \(if any\):[\r\n]*[\S\s]*/gi; } - for (const comment of issueComments.comments) { + // Re-Order the comments (Most Recent First) + const comments = issueComments.comments.reverse(); + for (const comment of comments) { const matched = regex.exec(comment.description); if (matched) { return comment; @@ -522,7 +536,7 @@ export class IssueService { } // Template url: https://github.com/CATcher-org/templates#teams-response-1 - parseTeamResponse(toParse: string): string { + parseTeamResponseForTesterResponsePhase(toParse: string): string { let teamResponse = ''; const regex = /# *Team\'?s Response[\n\r]*([\s\S]*)# Items for the Tester to Verify/gi; const matches = regex.exec(toParse); @@ -533,6 +547,28 @@ export class IssueService { return teamResponse; } + parseTeamResponseForTeamResponsePhase(toParse: string): string { + let teamResponse = ''; + const regex = /# Team's Response[\r\n]*([\S\s]*?)[\r\n]*## Duplicate status \(if any\):/gi; + const matches = regex.exec(toParse); + + if (matches && matches.length > this.MINIMUM_MATCHES) { + teamResponse = matches[1].trim(); + } + return teamResponse; + } + + parseDuplicateOfForTeamResponsePhase(toParse: string): string { + let duplicateOf = ''; + const regex = /## Duplicate status \(if any\):[\r\n]*Duplicate of #(.*)/gi; + const matches = regex.exec(toParse); + + if (matches && matches.length > this.MINIMUM_MATCHES) { + duplicateOf = matches[1].trim(); + } + return duplicateOf; + } + // Template url: https://github.com/CATcher-org/templates#disputes parseIssueDisputes(toParse: string): IssueDispute[] { let matches; diff --git a/src/app/shared/issue/duplicateOf/duplicate-of.component.ts b/src/app/shared/issue/duplicateOf/duplicate-of.component.ts index 2f0b3862c..a2e25d5c5 100644 --- a/src/app/shared/issue/duplicateOf/duplicate-of.component.ts +++ b/src/app/shared/issue/duplicateOf/duplicate-of.component.ts @@ -16,6 +16,8 @@ import { map } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { MatCheckbox, MatSelect } from '@angular/material'; import { PermissionService } from '../../../core/services/permission.service'; +import { IssueCommentService } from '../../../core/services/issue-comment.service'; +import { IssueComment } from '../../../core/models/comment.model'; @Component({ selector: 'app-duplicate-of-component', @@ -30,6 +32,7 @@ export class DuplicateOfComponent implements OnInit { @Input() issue: Issue; @Output() issueUpdated = new EventEmitter(); + @Output() commentUpdated = new EventEmitter(); @ViewChild(MatSelect) duplicateOfSelection: MatSelect; @ViewChild(MatCheckbox) duplicatedCheckbox: MatCheckbox; @@ -40,6 +43,7 @@ export class DuplicateOfComponent implements OnInit { readonly MAX_TITLE_LENGTH_FOR_NON_DUPLICATE_ISSUE = 37; constructor(public issueService: IssueService, + public issueCommentService: IssueCommentService, private errorHandlingService: ErrorHandlingService, public permissions: PermissionService) { } @@ -66,12 +70,22 @@ export class DuplicateOfComponent implements OnInit { } updateDuplicateStatus(event) { - this.issueService.updateIssue({ + const latestIssue = { ...this.issue, duplicated: !!event, duplicateOf: event ? event.value : null, - }).subscribe((updatedIssue) => { - this.issueUpdated.emit(updatedIssue); + }; + + latestIssue.issueComment.description = this.issueCommentService. + createGithubTeamResponse(latestIssue.teamResponse, latestIssue.duplicateOf); + + this.issueService.updateIssue(latestIssue).subscribe((updatedIssue) => { + this.issueCommentService.updateIssueComment(latestIssue.issueComment).subscribe((updatedComment) => { + updatedIssue.duplicateOf = latestIssue.duplicateOf; + updatedIssue.issueComment = updatedComment; + this.commentUpdated.emit(updatedComment); + this.issueUpdated.emit(updatedIssue); + }); }, (error) => { this.errorHandlingService.handleHttpError(error); }); diff --git a/src/app/shared/issue/response/response.component.ts b/src/app/shared/issue/response/response.component.ts index 991217bea..32370f9db 100644 --- a/src/app/shared/issue/response/response.component.ts +++ b/src/app/shared/issue/response/response.component.ts @@ -6,6 +6,8 @@ import {finalize} from 'rxjs/operators'; import {PermissionService} from '../../../core/services/permission.service'; import {Phase, PhaseService} from '../../../core/services/phase.service'; import {Issue} from '../../../core/models/issue.model'; +import {IssueCommentService} from '../../../core/services/issue-comment.service'; +import {IssueComment} from '../../../core/models/comment.model'; @Component({ selector: 'app-issue-response', @@ -36,9 +38,11 @@ export class ResponseComponent implements OnInit { @Input() isEditing: boolean; @Output() issueUpdated = new EventEmitter(); @Output() updateEditState = new EventEmitter(); + @Output() commentUpdated = new EventEmitter(); constructor(private issueService: IssueService, private formBuilder: FormBuilder, + private issueCommentService: IssueCommentService, private errorHandlingService: ErrorHandlingService, private permissions: PermissionService, private phaseService: PhaseService) { @@ -65,14 +69,32 @@ export class ResponseComponent implements OnInit { if (this.responseForm.invalid) { return; } - + const latestIssue = this.getUpdatedIssue(); this.isSavePending = true; - this.issueService.updateIssue(this.getUpdatedIssue()).pipe(finalize(() => { - this.updateEditState.emit(false); - this.isSavePending = false; - })).subscribe((updatedIssue: Issue) => { - this.issueUpdated.emit(updatedIssue); - form.resetForm(); + this.issueService.updateIssue(latestIssue).subscribe((updatedIssue: Issue) => { + + if (this.phaseService.currentPhase === Phase.phaseTeamResponse) { + // For Team Response phase, where the items are in the issue's comment + latestIssue.issueComment.description = this.issueCommentService. + createGithubTeamResponse(latestIssue.teamResponse, latestIssue.duplicateOf); + + this.issueCommentService.updateIssueComment(latestIssue.issueComment).subscribe( + (updatedComment) => { + this.commentUpdated.emit(updatedComment); + this.updateEditState.emit(false); + this.isSavePending = false; + updatedIssue.issueComment = updatedComment; + updatedIssue.teamResponse = this.issueService.parseTeamResponseForTeamResponsePhase(updatedComment.description); + updatedIssue.duplicateOf = +this.issueService.parseDuplicateOfForTeamResponsePhase(updatedComment.description); + this.issueUpdated.emit(updatedIssue); + form.resetForm(); + }, (error) => { + this.errorHandlingService.handleHttpError(error); + }); + } else { + this.issueUpdated.emit(updatedIssue); + form.resetForm(); + } }, (error) => { this.errorHandlingService.handleHttpError(error); }); diff --git a/src/app/shared/new-team-respond/new-team-response.component.ts b/src/app/shared/new-team-respond/new-team-response.component.ts index e5295417b..40ef9eb86 100644 --- a/src/app/shared/new-team-respond/new-team-response.component.ts +++ b/src/app/shared/new-team-respond/new-team-response.component.ts @@ -3,9 +3,11 @@ import { IssueService } from '../../core/services/issue.service'; import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms'; import { Issue, SEVERITY_ORDER, STATUS } from '../../core/models/issue.model'; import { ErrorHandlingService } from '../../core/services/error-handling.service'; -import { finalize, map } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { LabelService } from '../../core/services/label.service'; +import { IssueCommentService } from '../../core/services/issue-comment.service'; +import { IssueComment } from '../../core/models/comment.model'; @Component({ selector: 'app-new-team-response', @@ -20,9 +22,11 @@ export class NewTeamResponseComponent implements OnInit { isFormPending = false; @Input() issue: Issue; @Output() issueUpdated = new EventEmitter(); + @Output() updatedCommentEmitter = new EventEmitter(); constructor(private issueService: IssueService, private formBuilder: FormBuilder, + private issueCommentService: IssueCommentService, public labelService: LabelService, private errorHandlingService: ErrorHandlingService) { } @@ -51,7 +55,6 @@ export class NewTeamResponseComponent implements OnInit { this.duplicateOf.updateValueAndValidity(); this.responseTag.updateValueAndValidity(); }); - } submitNewTeamResponse(form: NgForm) { @@ -59,7 +62,27 @@ export class NewTeamResponseComponent implements OnInit { return; } this.isFormPending = true; - this.issueService.updateIssue({ + const latestIssue = this.getUpdatedIssue(); + + this.issueService.updateIssue(latestIssue) + .subscribe(() => { + + // New Team Response has no pre-existing comments hence new comment will be added. + const newCommentDescription = this.issueCommentService.createGithubTeamResponse(this.description.value, this.duplicateOf.value); + this.issueCommentService.createIssueComment(this.issue.id, newCommentDescription) + .subscribe((newComment: IssueComment) => { + this.updatedCommentEmitter.emit(newComment); + latestIssue.issueComment = newComment; + this.issueUpdated.emit(latestIssue); + form.resetForm(); + }); + }, (error) => { + this.errorHandlingService.handleHttpError(error); + }); + } + + getUpdatedIssue() { + return { ...this.issue, severity: this.severity.value, type: this.type.value, @@ -68,13 +91,8 @@ export class NewTeamResponseComponent implements OnInit { duplicated: this.duplicated.value, status: STATUS.Done, teamResponse: this.description.value, - duplicateOf: this.duplicateOf.value, - }).pipe(finalize(() => this.isFormPending = false)).subscribe((updatedIssue: Issue) => { - this.issueUpdated.emit(updatedIssue); - form.resetForm(); - }, (error) => { - this.errorHandlingService.handleHttpError(error); - }); + duplicateOf: this.duplicateOf.value + }; } dupIssueOptionIsDisabled(issue: Issue): boolean { diff --git a/src/app/shared/view-issue/view-issue.component.html b/src/app/shared/view-issue/view-issue.component.html index c7fb0681c..ca43ea55a 100644 --- a/src/app/shared/view-issue/view-issue.component.html +++ b/src/app/shared/view-issue/view-issue.component.html @@ -13,14 +13,14 @@ + [issue]="issue" [isEditing]="isTeamResponseEditing" (updateEditState)="updateTeamResponseEditState($event)" attributeName="teamResponse" + (issueUpdated)="updateIssue($event)" (commentUpdated)="updateComment($event)"> + (issueUpdated)="updateIssue($event)" (updatedCommentEmitter)= "updateComment($event)"> @@ -85,7 +85,7 @@
- +
diff --git a/src/app/shared/view-issue/view-issue.component.ts b/src/app/shared/view-issue/view-issue.component.ts index ad77ea509..dc6a04a3d 100644 --- a/src/app/shared/view-issue/view-issue.component.ts +++ b/src/app/shared/view-issue/view-issue.component.ts @@ -88,13 +88,13 @@ export class ViewIssueComponent implements OnInit, OnDestroy { updateIssue(newIssue: Issue) { this.issue = newIssue; - this.issueService.updateLocalStore(this.issue); + this.issueService.updateLocalStore(newIssue); } updateComment(newComment: IssueComment) { this.issue.issueComment = newComment; - this.issueService.updateLocalStore(this.issue); this.issueCommentService.updateLocalStore(newComment, this.issueId); + this.issueService.updateLocalStore(this.issue); } updateDescriptionEditState(updatedState: boolean) {