From 65db156048e1796c2566c36f6befadf9b8b92f97 Mon Sep 17 00:00:00 2001 From: Chidozie Ononiwu Date: Thu, 18 Jul 2024 14:25:12 -0700 Subject: [PATCH 1/7] APIView Conversation Page --- .../api-revision-options.component.html | 8 ++-- .../api-revision-options.component.scss | 2 - .../api-revision-options.component.ts | 2 +- .../conversation/conversation.component.html | 19 ++++++++ .../conversation/conversation.component.scss | 44 +++++++++++++++++++ .../conversation.component.spec.ts | 21 +++++++++ .../conversation/conversation.component.ts | 17 +++++++ .../review-page/review-page.component.html | 9 +++- .../review-page/review-page.component.scss | 6 ++- .../review-page/review-page.component.ts | 23 ++++++++-- .../revisions-list.component.ts | 4 +- .../review-page/review-page.module.ts | 3 +- .../APIView/ClientSPA/src/app/app.module.ts | 3 -- 13 files changed, 142 insertions(+), 19 deletions(-) create mode 100644 src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.html create mode 100644 src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.scss create mode 100644 src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.spec.ts create mode 100644 src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.ts diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.html b/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.html index 06876b0393a..d08019b1bb7 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.html +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.html @@ -40,7 +40,7 @@ {{ selectedActiveAPIRevision.prNo }} version: {{ selectedActiveAPIRevision.version }} - {{ selectedActiveAPIRevision.creatorBy }} + {{ selectedActiveAPIRevision.createdBy }} released: {{ selectedActiveAPIRevision.releasedOn | timeago }} @@ -50,7 +50,7 @@ {{ apiRevision.prNo }} version: {{ apiRevision.version }} - {{ apiRevision.creatorBy }} + {{ apiRevision.createdBy }} Latest GA Latest Approved Latest Main @@ -110,7 +110,7 @@ {{ selectedDiffAPIRevision.prNo }} version: {{ selectedDiffAPIRevision.version }} - {{ selectedDiffAPIRevision.creatorBy }} + {{ selectedDiffAPIRevision.createdBy }} released: {{ selectedDiffAPIRevision.releasedOn | timeago }} @@ -120,7 +120,7 @@ {{ apiRevision.prNo }} version: {{ apiRevision.version }} - {{ apiRevision.creatorBy }} + {{ apiRevision.createdBy }} Latest GA Latest Approved Latest Main diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.scss b/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.scss index c9cd6425a69..c65f3b534d9 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.scss +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.scss @@ -76,6 +76,4 @@ border: 1px solid var(--alert-secondary-border-color); } } - - } \ No newline at end of file diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.ts index d76cfe8dc38..5c9b5387a15 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.ts @@ -177,7 +177,7 @@ export class ApiRevisionOptionsComponent implements OnChanges { version: apiRevision.packageVersion, prNo: apiRevision.pullRequestNo, createdOn: apiRevision.createdOn, - creatorBy: apiRevision.createdBy, + createdBy: apiRevision.createdBy, lastUpdatedOn: apiRevision.lastUpdatedOn, isApproved: apiRevision.isApproved, isReleased: apiRevision.isReleased, diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.html b/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.html new file mode 100644 index 00000000000..85fa3dd9559 --- /dev/null +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.html @@ -0,0 +1,19 @@ + + + + + + + {{ apiRevision.pullRequestNo }} + version: {{ apiRevision.packageVersion }} + + {{ apiRevision.createdBy }} + released: {{ apiRevision.releasedOn | timeago }} + created: {{ apiRevision.createdOn | timeago }} + lastUpdated: {{ apiRevision | lastUpdatedOn | timeago }} + {{ apiRevision.label }} + +
+
+
+
\ No newline at end of file diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.scss b/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.scss new file mode 100644 index 00000000000..9e1a81c353a --- /dev/null +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.scss @@ -0,0 +1,44 @@ +:host ::ng-deep { + .p-timeline-event-opposite { + flex: 0 0 auto !important; + } + + .p-timeline .p-timeline-event-connector { + background-color: var(--border-color) !important; + } + + .p-timeline.p-timeline-vertical .p-timeline-event-connector { + width: 2px; + } + + .emphasis-badge { + border-radius: 5px; + padding: 0px 5px 2px 5px; + font-size: smaller; + font-weight: bold; + + &.info { + background-color: var(--alert-info-bg); + color: var(--alert-info-color); + border: 1px solid var(--alert-info-border-color); + } + + &.warn { + background-color: var(--alert-warn-bg); + color: var(--alert-warn-color); + border: 1px solid var(--alert-warn-border-color); + } + + &.success { + background-color: var(--alert-success-bg); + color: var(--alert-success-color); + border: 1px solid var(--alert-success-border-color); + } + + &.secondary { + background-color: var(--alert-secondary-bg); + color: var(--alert-secondary-color); + border: 1px solid var(--alert-secondary-border-color); + } + } +} \ No newline at end of file diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.spec.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.spec.ts new file mode 100644 index 00000000000..a25dc1953aa --- /dev/null +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConversationComponent } from './conversation.component'; + +describe('ConversationComponent', () => { + let component: ConversationComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ConversationComponent] + }); + fixture = TestBed.createComponent(ConversationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.ts new file mode 100644 index 00000000000..f524b53c4fa --- /dev/null +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.ts @@ -0,0 +1,17 @@ +import { Component, Input } from '@angular/core'; +import { CommentItemModel } from 'src/app/_models/commentItemModel'; +import { APIRevision } from 'src/app/_models/revision'; + +@Component({ + selector: 'app-conversation', + templateUrl: './conversation.component.html', + styleUrls: ['./conversation.component.scss'] +}) +export class ConversationComponent { + @Input() apiRevisions: APIRevision[] = []; + @Input() comments: CommentItemModel[] = []; + + getFilteredComments(apiRevision: APIRevision): CommentItemModel[] { + return this.comments.filter(c => c.aPIRevisionId === apiRevision.id); + } +} diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html index df63fef727e..378e4da61f3 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html @@ -67,9 +67,14 @@ - + + [revisionSidePanel]="revisionSidePanel!"> + + + \ No newline at end of file diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.scss b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.scss index 88abf90e894..4ff068632b9 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.scss +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.scss @@ -40,7 +40,11 @@ } } - .revisions-sidebar { + .revisions-sidebar, .conversation-sidebar { width: 75dvw; } + + .conversation-sidebar { + width: 50dvw; + } } \ No newline at end of file diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.ts index 20a017ad612..b1e7652262a 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.ts @@ -16,6 +16,7 @@ import { ACTIVE_API_REVISION_ID_QUERY_PARAM, DIFF_API_REVISION_ID_QUERY_PARAM, D import { CodePanelData, CodePanelRowData, CodePanelRowDatatype } from 'src/app/_models/codePanelModels'; import { UserProfile } from 'src/app/_models/userProfile'; import { ReviewPageWorkerMessageDirective } from 'src/app/_models/insertCodePanelRowDataMessage'; +import { CommentItemModel } from 'src/app/_models/commentItemModel'; @Component({ selector: 'app-review-page', @@ -33,9 +34,11 @@ export class ReviewPageComponent implements OnInit { userProfile : UserProfile | undefined; review : Review | undefined = undefined; apiRevisions: APIRevision[] = []; + comments: CommentItemModel[] = []; activeAPIRevision : APIRevision | undefined = undefined; diffAPIRevision : APIRevision | undefined = undefined; - revisionSideBarVisible : boolean = false; + revisionSidePanel : boolean | undefined = undefined; + conversationSidePanel : boolean | undefined = undefined; reviewPageNavigation : TreeNode[] = []; language: string | undefined; languageSafeName: string | undefined; @@ -68,7 +71,7 @@ export class ReviewPageComponent implements OnInit { constructor(private route: ActivatedRoute, private router: Router, private apiRevisionsService: RevisionsService, private reviewsService: ReviewsService, private workerService: WorkerService, private changeDetectorRef: ChangeDetectorRef, - private userProfileService: UserProfileService) {} + private userProfileService: UserProfileService, private commentsService: CommentsService) {} ngOnInit() { this.userProfileService.getUserProfile().subscribe( @@ -100,11 +103,16 @@ export class ReviewPageComponent implements OnInit { this.loadReview(this.reviewId!); this.loadPreferredApprovers(this.reviewId!); this.loadAPIRevisions(0, this.apiRevisionPageSize); + this.loadComments(); this.sideMenu = [ { icon: 'bi bi-clock-history', - command: () => { this.revisionSideBarVisible = !this.revisionSideBarVisible; } + command: () => { this.revisionSidePanel = !this.revisionSidePanel; } + }, + { + icon: 'bi bi-chat-left-dots', + command: () => { this.conversationSidePanel = !this.conversationSidePanel; } } ]; } @@ -215,6 +223,15 @@ export class ReviewPageComponent implements OnInit { }); } + loadComments() { + this.commentsService.getComments(this.reviewId!) + .pipe(takeUntil(this.destroy$)).subscribe({ + next: (comments: CommentItemModel[]) => { + this.comments = comments; + } + }); + } + handlePageOptionsEmitter(showPageOptions: boolean) { this.userProfile!.preferences.hideReviewPageOptions = !showPageOptions; this.userProfileService.updateUserPrefernece(this.userProfile!.preferences).pipe(takeUntil(this.destroy$)).subscribe({ diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/revisions-list/revisions-list.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/revisions-list/revisions-list.component.ts index c37ab00567b..b5626dbc67f 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/revisions-list/revisions-list.component.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/revisions-list/revisions-list.component.ts @@ -21,7 +21,7 @@ import { environment } from 'src/environments/environment'; }) export class RevisionsListComponent implements OnInit, OnChanges { @Input() review : Review | undefined = undefined; - @Input() revisionSideBarVisible : boolean = false; + @Input() revisionSidePanel : boolean = false; @ViewChild("revisionCreationFileUpload") revisionCreationFileUpload!: FileUpload; @@ -110,7 +110,7 @@ export class RevisionsListComponent implements OnInit, OnChanges { this.showDiffButton = false; } - if (changes['revisionSideBarVisible'] && changes['revisionSideBarVisible'].currentValue == false) { + if (changes['revisionSidePanel'] && changes['revisionSidePanel'].currentValue == false) { this.createRevisionSidebarVisible = false; this.optionsSidebarVisible = false; } diff --git a/src/dotnet/APIView/ClientSPA/src/app/_modules/review-page/review-page.module.ts b/src/dotnet/APIView/ClientSPA/src/app/_modules/review-page/review-page.module.ts index 3023b546676..28e53ad3929 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_modules/review-page/review-page.module.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_modules/review-page/review-page.module.ts @@ -19,9 +19,9 @@ import { PageOptionsSectionComponent } from 'src/app/_components/shared/page-opt import { ApiRevisionOptionsComponent } from 'src/app/_components/api-revision-options/api-revision-options.component'; import { MarkdownToHtmlPipe } from 'src/app/_pipes/markdown-to-html.pipe'; import { EditorComponent } from 'src/app/_components/shared/editor/editor.component'; -import { SelectButtonModule } from 'primeng/selectbutton'; import { ReviewPageOptionsComponent } from 'src/app/_components/review-page-options/review-page-options.component'; import { InputSwitchModule } from 'primeng/inputswitch'; +import { ConversationComponent } from 'src/app/_components/conversation/conversation.component'; const routes: Routes = [ { path: '', component: ReviewPageComponent } @@ -34,6 +34,7 @@ const routes: Routes = [ ReviewInfoComponent, CodePanelComponent, CommentThreadComponent, + ConversationComponent, PageOptionsSectionComponent, ReviewPageOptionsComponent, ApiRevisionOptionsComponent, diff --git a/src/dotnet/APIView/ClientSPA/src/app/app.module.ts b/src/dotnet/APIView/ClientSPA/src/app/app.module.ts index b69e9dcc59c..b90f72292d8 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/app.module.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/app.module.ts @@ -2,17 +2,14 @@ import { NgModule, APP_INITIALIZER } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; -import { ReactiveFormsModule } from '@angular/forms'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { IndexPageComponent } from './_components/index-page/index-page.component'; import { ReviewsListComponent } from './_components/reviews-list/reviews-list.component'; -import { InputTextModule } from 'primeng/inputtext'; import { TabMenuModule } from 'primeng/tabmenu'; import { ToolbarModule } from 'primeng/toolbar'; import { BadgeModule } from 'primeng/badge'; -import { FileUploadModule } from 'primeng/fileupload'; import { Observable } from 'rxjs'; import { ConfigService } from './_services/config/config.service'; import { CookieService } from 'ngx-cookie-service'; From 07ec4eb9f4c1b492a220cef0f30f23efb10be7f9 Mon Sep 17 00:00:00 2001 From: Chidozie Ononiwu Date: Thu, 8 Aug 2024 15:44:59 -0700 Subject: [PATCH 2/7] Show Comment Threads on Conversation Page --- .../conversation/conversation.component.ts | 17 ------ .../conversations.component.html} | 7 ++- .../conversations.component.scss} | 0 .../conversations.component.spec.ts} | 10 ++-- .../conversations/conversations.component.ts | 58 +++++++++++++++++++ .../review-page/review-page.component.html | 4 +- .../review-page/review-page.component.scss | 2 +- .../src/app/_models/commentItemModel.ts | 4 +- .../review-page/review-page.module.ts | 8 ++- .../ClientSPA/src/ng-prime-overrides.scss | 4 ++ 10 files changed, 82 insertions(+), 32 deletions(-) delete mode 100644 src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.ts rename src/dotnet/APIView/ClientSPA/src/app/_components/{conversation/conversation.component.html => conversations/conversations.component.html} (76%) rename src/dotnet/APIView/ClientSPA/src/app/_components/{conversation/conversation.component.scss => conversations/conversations.component.scss} (100%) rename src/dotnet/APIView/ClientSPA/src/app/_components/{conversation/conversation.component.spec.ts => conversations/conversations.component.spec.ts} (54%) create mode 100644 src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.ts diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.ts deleted file mode 100644 index f524b53c4fa..00000000000 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { CommentItemModel } from 'src/app/_models/commentItemModel'; -import { APIRevision } from 'src/app/_models/revision'; - -@Component({ - selector: 'app-conversation', - templateUrl: './conversation.component.html', - styleUrls: ['./conversation.component.scss'] -}) -export class ConversationComponent { - @Input() apiRevisions: APIRevision[] = []; - @Input() comments: CommentItemModel[] = []; - - getFilteredComments(apiRevision: APIRevision): CommentItemModel[] { - return this.comments.filter(c => c.aPIRevisionId === apiRevision.id); - } -} diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.html b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html similarity index 76% rename from src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.html rename to src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html index 85fa3dd9559..154b9d3e3f7 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.html +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html @@ -1,4 +1,4 @@ - + @@ -13,7 +13,10 @@ lastUpdated: {{ apiRevision | lastUpdatedOn | timeago }} {{ apiRevision.label }} -
+ \ No newline at end of file diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.scss b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.scss similarity index 100% rename from src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.scss rename to src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.scss diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.spec.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.spec.ts similarity index 54% rename from src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.spec.ts rename to src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.spec.ts index a25dc1953aa..1430b704422 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/conversation/conversation.component.spec.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.spec.ts @@ -1,16 +1,16 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ConversationComponent } from './conversation.component'; +import { ConversationsComponent } from './conversations.component'; describe('ConversationComponent', () => { - let component: ConversationComponent; - let fixture: ComponentFixture; + let component: ConversationsComponent; + let fixture: ComponentFixture; beforeEach(() => { TestBed.configureTestingModule({ - declarations: [ConversationComponent] + declarations: [ConversationsComponent] }); - fixture = TestBed.createComponent(ConversationComponent); + fixture = TestBed.createComponent(ConversationsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.ts new file mode 100644 index 00000000000..b907bd505fb --- /dev/null +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.ts @@ -0,0 +1,58 @@ +import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { CodePanelRowData, CodePanelRowDatatype } from 'src/app/_models/codePanelModels'; +import { CommentItemModel } from 'src/app/_models/commentItemModel'; +import { APIRevision } from 'src/app/_models/revision'; + +@Component({ + selector: 'app-conversations', + templateUrl: './conversations.component.html', + styleUrls: ['./conversations.component.scss'] +}) +export class ConversationsComponent implements OnChanges { + @Input() apiRevisions: APIRevision[] = []; + @Input() comments: CommentItemModel[] = []; + + commentThreads: Map = new Map(); + + ngOnChanges(changes: SimpleChanges) { + if (changes['apiRevisions'] || changes['comments']) { + if (this.apiRevisions.length > 0 && this.comments.length > 0) { + this.createCommentThreads(); + } + } + } + + createCommentThreads() { + for (const apiRevision of this.apiRevisions) { + const groupedCommentsForAPIRevision = this.comments + .filter(c => c.apiRevisionId === apiRevision.id) + .reduce((acc: { [key: string]: CommentItemModel[] }, comment) => { + const key = comment.elementId; + if (!acc[key]) { + acc[key] = []; + } + acc[key].push(comment); + return acc; + }, {}); + + if (Object.keys(groupedCommentsForAPIRevision).length > 0) { + this.commentThreads.set(apiRevision.id, []); + } + + for (const elementId in groupedCommentsForAPIRevision) { + if (groupedCommentsForAPIRevision.hasOwnProperty(elementId)) { + const comments = groupedCommentsForAPIRevision[elementId]; + const codePanelRowData = new CodePanelRowData(); + codePanelRowData.type = CodePanelRowDatatype.CommentThread; + codePanelRowData.comments = comments; + codePanelRowData.isResolvedCommentThread = comments.some(c => c.isResolved); + this.commentThreads.get(apiRevision.id)?.push(codePanelRowData); + } + } + } + } + + getAPIRevisionWithComments() { + return this.apiRevisions.filter(apiRevision => this.commentThreads.has(apiRevision.id)); + } +} diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html index 378e4da61f3..591e263d6f5 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html @@ -73,8 +73,8 @@ [revisionSidePanel]="revisionSidePanel!"> - + [comments]="comments"> \ No newline at end of file diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.scss b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.scss index 4ff068632b9..a192c35b8d2 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.scss +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.scss @@ -45,6 +45,6 @@ } .conversation-sidebar { - width: 50dvw; + width: 60dvw; } } \ No newline at end of file diff --git a/src/dotnet/APIView/ClientSPA/src/app/_models/commentItemModel.ts b/src/dotnet/APIView/ClientSPA/src/app/_models/commentItemModel.ts index 5af7fe0474e..fd5a15c7139 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_models/commentItemModel.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_models/commentItemModel.ts @@ -8,7 +8,7 @@ export enum CommentType { export class CommentItemModel { id: string = ''; reviewId: string = ''; - aPIRevisionId: string = ''; + apiRevisionId: string = ''; elementId: string = ''; sectionClass: string = ''; commentText: string = ''; @@ -28,7 +28,7 @@ export class CommentItemModel { constructor() { this.id = ''; this.reviewId = ''; - this.aPIRevisionId = ''; + this.apiRevisionId = ''; this.elementId = ''; this.sectionClass = ''; this.commentText = ''; diff --git a/src/dotnet/APIView/ClientSPA/src/app/_modules/review-page/review-page.module.ts b/src/dotnet/APIView/ClientSPA/src/app/_modules/review-page/review-page.module.ts index 28e53ad3929..cadbd3ecfa9 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_modules/review-page/review-page.module.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_modules/review-page/review-page.module.ts @@ -14,6 +14,7 @@ import { MenuModule } from 'primeng/menu'; import { TimelineModule } from 'primeng/timeline'; import { SharedAppModule } from '../shared/shared-app.module'; import { ButtonModule } from 'primeng/button'; +import { DividerModule } from 'primeng/divider'; import { UiScrollModule } from 'ngx-ui-scroll' ; import { PageOptionsSectionComponent } from 'src/app/_components/shared/page-options-section/page-options-section.component'; import { ApiRevisionOptionsComponent } from 'src/app/_components/api-revision-options/api-revision-options.component'; @@ -21,7 +22,7 @@ import { MarkdownToHtmlPipe } from 'src/app/_pipes/markdown-to-html.pipe'; import { EditorComponent } from 'src/app/_components/shared/editor/editor.component'; import { ReviewPageOptionsComponent } from 'src/app/_components/review-page-options/review-page-options.component'; import { InputSwitchModule } from 'primeng/inputswitch'; -import { ConversationComponent } from 'src/app/_components/conversation/conversation.component'; +import { ConversationsComponent } from 'src/app/_components/conversations/conversations.component'; const routes: Routes = [ { path: '', component: ReviewPageComponent } @@ -34,12 +35,12 @@ const routes: Routes = [ ReviewInfoComponent, CodePanelComponent, CommentThreadComponent, - ConversationComponent, + ConversationsComponent, PageOptionsSectionComponent, ReviewPageOptionsComponent, ApiRevisionOptionsComponent, MarkdownToHtmlPipe, - EditorComponent + EditorComponent, ], imports: [ SharedAppModule, @@ -53,6 +54,7 @@ const routes: Routes = [ ButtonModule, InputSwitchModule, UiScrollModule, + DividerModule, RouterModule.forChild(routes), ] }) diff --git a/src/dotnet/APIView/ClientSPA/src/ng-prime-overrides.scss b/src/dotnet/APIView/ClientSPA/src/ng-prime-overrides.scss index 49c864c7826..6511967b38a 100644 --- a/src/dotnet/APIView/ClientSPA/src/ng-prime-overrides.scss +++ b/src/dotnet/APIView/ClientSPA/src/ng-prime-overrides.scss @@ -163,6 +163,10 @@ p-contextmenusub { outline-offset: 0.15rem; } +.p-divider.p-divider-horizontal:before { + border-top: 1px solid var(--border-color); +} + .p-editor-container .p-editor-toolbar { background: var(--base-bg-color); } From ade90fc9b72f76d49dcff949477b6df85a64797b Mon Sep 17 00:00:00 2001 From: Chidozie Ononiwu Date: Mon, 12 Aug 2024 11:01:47 -0700 Subject: [PATCH 3/7] Ad test for comment thread --- .../conversations.component.html | 31 +++++++++------- .../conversations.component.spec.ts | 10 ++++- .../comment-thread.component.spec.ts | 37 +++++++++++++++++++ .../comment-thread.component.ts | 8 +++- 4 files changed, 70 insertions(+), 16 deletions(-) diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html index 154b9d3e3f7..48dbb6a24b4 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html @@ -3,20 +3,23 @@ - - {{ apiRevision.pullRequestNo }} - version: {{ apiRevision.packageVersion }} - - {{ apiRevision.createdBy }} - released: {{ apiRevision.releasedOn | timeago }} - created: {{ apiRevision.createdOn | timeago }} - lastUpdated: {{ apiRevision | lastUpdatedOn | timeago }} - {{ apiRevision.label }} - -
- - {{ commentThread!.comments[0].elementId }} - +
+ + {{ apiRevision.pullRequestNo }} + version: {{ apiRevision.packageVersion }} + + {{ apiRevision.createdBy }} + released: {{ apiRevision.releasedOn | timeago }} + created: {{ apiRevision.createdOn | timeago }} + lastUpdated: {{ apiRevision | lastUpdatedOn | timeago }} + {{ apiRevision.label }} + +
\ No newline at end of file diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.spec.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.spec.ts index 1430b704422..bb20a684162 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.spec.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.spec.ts @@ -1,6 +1,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ConversationsComponent } from './conversations.component'; +import { SharedAppModule } from 'src/app/_modules/shared/shared-app.module'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ReviewPageModule } from 'src/app/_modules/review-page/review-page.module'; describe('ConversationComponent', () => { let component: ConversationsComponent; @@ -8,7 +11,12 @@ describe('ConversationComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [ConversationsComponent] + declarations: [ConversationsComponent], + imports: [ + HttpClientTestingModule, + ReviewPageModule, + SharedAppModule + ], }); fixture = TestBed.createComponent(ConversationsComponent); component = fixture.componentInstance; diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.spec.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.spec.ts index 8f1d1b409f3..381e8f2fb5b 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.spec.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.spec.ts @@ -5,6 +5,7 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { SharedAppModule } from 'src/app/_modules/shared/shared-app.module'; import { CodePanelRowData } from 'src/app/_models/codePanelModels'; import { ReviewPageModule } from 'src/app/_modules/review-page/review-page.module'; +import { CommentItemModel } from 'src/app/_models/commentItemModel'; describe('CommentThreadComponent', () => { let component: CommentThreadComponent; @@ -28,4 +29,40 @@ describe('CommentThreadComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe('setCommentResolutionState', () => { + it ('should select latest user to resolve comment thread', () => { + const comment1 = { + id: '1', + isResolved: true, + changeHistory: [ { + changeAction: 'resolved', + changedBy: 'test user 1', + }, + { + changeAction: 'resolved', + changedBy: 'test user 2', + }] + } as CommentItemModel; + const comment2 = { + id: '2', + isResolved: true, + changeHistory: [ { + changeAction: 'resolved', + changedBy: 'test user 1', + }, + { + changeAction: 'resolved', + changedBy: 'test user 2', + }] + } as CommentItemModel; + + const codePanelRowData = new CodePanelRowData(); + codePanelRowData.comments = [comment1, comment2]; + codePanelRowData.isResolvedCommentThread = true; + component.codePanelRowData = codePanelRowData + fixture.detectChanges(); + expect(component.threadResolvedBy).toBe('test user 2'); + }); + }); }); diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.ts index 4580f03808a..29d89e5278c 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.ts @@ -109,7 +109,13 @@ export class CommentThreadComponent { setCommentResolutionState() { if (this.codePanelRowData?.isResolvedCommentThread) { - this.threadResolvedBy = this.codePanelRowData?.commentThreadIsResolvedBy ?? this.codePanelRowData?.comments?.find(comment => comment.isResolved)?.changeHistory.find(ch => ch.changeAction === 'resolved')?.changedBy; + this.threadResolvedBy = this.codePanelRowData?.commentThreadIsResolvedBy; + if (!this.threadResolvedBy) { + const lastestResolvedComment = Array.from(this.codePanelRowData?.comments || []).findLast(comment => comment.isResolved && comment.changeHistory && comment.changeHistory.some(ch => ch.changeAction === 'resolved')); + if (lastestResolvedComment) { + this.threadResolvedBy = lastestResolvedComment.changeHistory.findLast(ch => ch.changeAction === 'resolved')?.changedBy; + } + } this.spacingBasedOnResolvedState = 'mb-2'; this.resolveThreadButtonText = 'Unresolve'; } From 6c6da17d2c4bf17aeb75ca117967e6b0e4da6086 Mon Sep 17 00:00:00 2001 From: Chidozie Ononiwu Date: Mon, 12 Aug 2024 21:22:19 -0700 Subject: [PATCH 4/7] handle Save Comment from Conversation Page --- .../api-revision-options.component.ts | 21 ++---- .../conversations.component.html | 30 ++++---- .../conversations/conversations.component.ts | 70 +++++++++++++++---- .../review-page/review-page.component.html | 3 +- .../comment-thread.component.spec.ts | 9 ++- .../comment-thread.component.ts | 27 ++++--- .../src/app/_helpers/common-helpers.ts | 19 +++++ 7 files changed, 123 insertions(+), 56 deletions(-) diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.ts index 5c9b5387a15..57cdcf39ed1 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.ts @@ -1,5 +1,6 @@ import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { AUTOMATIC_ICON, getTypeClass, MANUAL_ICON, PR_ICON } from 'src/app/_helpers/common-helpers'; import { getQueryParams } from 'src/app/_helpers/router-helpers'; import { AzureEngSemanticVersion } from 'src/app/_models/azureEngSemanticVersion'; import { APIRevision } from 'src/app/_models/revision'; @@ -20,9 +21,9 @@ export class ApiRevisionOptionsComponent implements OnChanges { selectedActiveAPIRevision: any; selectedDiffAPIRevision: any = null; - manualIcon = "fa-solid fa-arrow-up-from-bracket"; - prIcon = "fa-solid fa-code-pull-request"; - automaticIcon = "fa-solid fa-robot"; + manualIcon = MANUAL_ICON; + prIcon = PR_ICON; + automaticIcon = AUTOMATIC_ICON; activeApiRevisionsSearchValue: string = ''; diffApiRevisionsSearchValue: string = ''; @@ -155,24 +156,12 @@ export class ApiRevisionOptionsComponent implements OnChanges { mapRevisionToMenu(apiRevisions: APIRevision[]) { return apiRevisions .map((apiRevision: APIRevision) => { - let typeClass = ''; - switch (apiRevision.apiRevisionType) { - case 'manual': - typeClass = this.manualIcon; - break; - case 'pullRequest': - typeClass = this.prIcon; - break; - case 'automatic': - typeClass = this.automaticIcon; - break; - } return { id : apiRevision.id, resolvedLabel: apiRevision.resolvedLabel, language: apiRevision.language, label: apiRevision.label, - typeClass: typeClass, + typeClass: getTypeClass(apiRevision.apiRevisionType), apiRevisionType: apiRevision.apiRevisionType, version: apiRevision.packageVersion, prNo: apiRevision.pullRequestNo, diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html index 48dbb6a24b4..06992308748 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html @@ -1,24 +1,26 @@ +

Conversations

+ -
+
- {{ apiRevision.pullRequestNo }} - version: {{ apiRevision.packageVersion }} - - {{ apiRevision.createdBy }} - released: {{ apiRevision.releasedOn | timeago }} - created: {{ apiRevision.createdOn | timeago }} - lastUpdated: {{ apiRevision | lastUpdatedOn | timeago }} - {{ apiRevision.label }} + + {{ apiRevision.pullRequestNo }} + version: {{ apiRevision.packageVersion }} + + {{ apiRevision.createdBy }} + released: {{ apiRevision.releasedOn | timeago }} + created: {{ apiRevision.createdOn | timeago }} + lastUpdated: {{ apiRevision | lastUpdatedOn | timeago }} + {{ apiRevision.label }} - diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.ts index b907bd505fb..2feb00d6a2e 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.ts @@ -1,7 +1,11 @@ import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { CodePanelRowData, CodePanelRowDatatype } from 'src/app/_models/codePanelModels'; -import { CommentItemModel } from 'src/app/_models/commentItemModel'; +import { CommentItemModel, CommentType } from 'src/app/_models/commentItemModel'; import { APIRevision } from 'src/app/_models/revision'; +import { getTypeClass } from 'src/app/_helpers/common-helpers'; +import { CommentsService } from 'src/app/_services/comments/comments.service'; +import { take } from 'rxjs'; +import { Review } from 'src/app/_models/review'; @Component({ selector: 'app-conversations', @@ -11,9 +15,12 @@ import { APIRevision } from 'src/app/_models/revision'; export class ConversationsComponent implements OnChanges { @Input() apiRevisions: APIRevision[] = []; @Input() comments: CommentItemModel[] = []; + @Input() review : Review | undefined = undefined; commentThreads: Map = new Map(); + constructor(private commentsService: CommentsService) { } + ngOnChanges(changes: SimpleChanges) { if (changes['apiRevisions'] || changes['comments']) { if (this.apiRevisions.length > 0 && this.comments.length > 0) { @@ -23,10 +30,9 @@ export class ConversationsComponent implements OnChanges { } createCommentThreads() { - for (const apiRevision of this.apiRevisions) { - const groupedCommentsForAPIRevision = this.comments - .filter(c => c.apiRevisionId === apiRevision.id) - .reduce((acc: { [key: string]: CommentItemModel[] }, comment) => { + const apiRevisionInOrder = this.apiRevisions.sort((a, b) => (new Date(b.createdOn) as any) - (new Date(a.createdOn) as any)); + const groupedComments = this.comments + .reduce((acc: { [key: string]: CommentItemModel[] }, comment) => { const key = comment.elementId; if (!acc[key]) { acc[key] = []; @@ -35,18 +41,33 @@ export class ConversationsComponent implements OnChanges { return acc; }, {}); - if (Object.keys(groupedCommentsForAPIRevision).length > 0) { - this.commentThreads.set(apiRevision.id, []); - } + for (const elementId in groupedComments) { + if (groupedComments.hasOwnProperty(elementId)) { + const comments = groupedComments[elementId]; + const apiRevisionIds = comments.map(c => c.apiRevisionId); + + let apiRevisionPostion = Number.MAX_SAFE_INTEGER; + + for (const apiRevisionId of apiRevisionIds) { + const apiRevisionIdPosition = apiRevisionInOrder.findIndex(apiRevision => apiRevision.id === apiRevisionId); + if (apiRevisionIdPosition >= 0 && apiRevisionIdPosition < apiRevisionPostion) { + apiRevisionPostion = apiRevisionIdPosition; + } + } - for (const elementId in groupedCommentsForAPIRevision) { - if (groupedCommentsForAPIRevision.hasOwnProperty(elementId)) { - const comments = groupedCommentsForAPIRevision[elementId]; + if (apiRevisionPostion >= 0 && apiRevisionPostion < apiRevisionInOrder.length) { + const apiRevisionIdForThread = apiRevisionInOrder[apiRevisionPostion].id; const codePanelRowData = new CodePanelRowData(); codePanelRowData.type = CodePanelRowDatatype.CommentThread; codePanelRowData.comments = comments; codePanelRowData.isResolvedCommentThread = comments.some(c => c.isResolved); - this.commentThreads.get(apiRevision.id)?.push(codePanelRowData); + + if (this.commentThreads.has(apiRevisionIdForThread)) { + this.commentThreads.get(apiRevisionIdForThread)?.push(codePanelRowData); + } + else { + this.commentThreads.set(apiRevisionIdForThread, [codePanelRowData]); + } } } } @@ -55,4 +76,29 @@ export class ConversationsComponent implements OnChanges { getAPIRevisionWithComments() { return this.apiRevisions.filter(apiRevision => this.commentThreads.has(apiRevision.id)); } + + getAPIRevisionTypeClass(apiRevision: APIRevision) { + return getTypeClass(apiRevision.apiRevisionType); + } + + + handleSaveCommentActionEmitter(data: any) { + console.log(data); + //if (data.commentId) { + // this.commentsService.updateComment(this.review?.id!, data.commentId, data.commentText).pipe(take(1)).subscribe({ + // next: () => { + // //this.updateHasActiveConversations(); + // } + // }); + //} + //else { + // this.commentsService.createComment(this.review?.id!, data.conversationGroupId!, data.nodeId, data.commentText, CommentType.APIRevision, data.allowAnyOneToResolve) + // .pipe(take(1)).subscribe({ + // next: (response: CommentItemModel) => { + // //this.updateHasActiveConversations(); + // } + // } + // ); + //} + } } diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html index 591e263d6f5..2183586b3eb 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html @@ -75,6 +75,7 @@ + [comments]="comments" + [review]="review"> \ No newline at end of file diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.spec.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.spec.ts index 381e8f2fb5b..0562dccfd86 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.spec.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.spec.ts @@ -3,9 +3,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CommentThreadComponent } from './comment-thread.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { SharedAppModule } from 'src/app/_modules/shared/shared-app.module'; -import { CodePanelRowData } from 'src/app/_models/codePanelModels'; import { ReviewPageModule } from 'src/app/_modules/review-page/review-page.module'; import { CommentItemModel } from 'src/app/_models/commentItemModel'; +import { CodePanelRowData } from 'src/app/_models/codePanelModels'; describe('CommentThreadComponent', () => { let component: CommentThreadComponent; @@ -57,11 +57,10 @@ describe('CommentThreadComponent', () => { }] } as CommentItemModel; - const codePanelRowData = new CodePanelRowData(); - codePanelRowData.comments = [comment1, comment2]; - codePanelRowData.isResolvedCommentThread = true; - component.codePanelRowData = codePanelRowData + component.codePanelRowData!.comments = [comment1, comment2]; + component.codePanelRowData!.isResolvedCommentThread = true; fixture.detectChanges(); + component.setCommentResolutionState(); expect(component.threadResolvedBy).toBe('test user 2'); }); }); diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.ts index 29d89e5278c..4d38ea5cbac 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.ts @@ -6,6 +6,7 @@ import { environment } from 'src/environments/environment'; import { EditorComponent } from '../editor/editor.component'; import { CodePanelRowData } from 'src/app/_models/codePanelModels'; import { UserProfile } from 'src/app/_models/userProfile'; +import { first } from 'rxjs'; @Component({ selector: 'app-comment-thread', @@ -18,6 +19,7 @@ import { UserProfile } from 'src/app/_models/userProfile'; export class CommentThreadComponent { @Input() codePanelRowData: CodePanelRowData | undefined = undefined; @Input() associatedCodeLine: CodePanelRowData | undefined; + @Input() instanceLocation: "code-panel" | "conversations" = "code-panel"; @Output() cancelCommentActionEmitter : EventEmitter = new EventEmitter(); @Output() saveCommentActionEmitter : EventEmitter = new EventEmitter(); @Output() deleteCommentActionEmitter : EventEmitter = new EventEmitter(); @@ -111,17 +113,17 @@ export class CommentThreadComponent { if (this.codePanelRowData?.isResolvedCommentThread) { this.threadResolvedBy = this.codePanelRowData?.commentThreadIsResolvedBy; if (!this.threadResolvedBy) { - const lastestResolvedComment = Array.from(this.codePanelRowData?.comments || []).findLast(comment => comment.isResolved && comment.changeHistory && comment.changeHistory.some(ch => ch.changeAction === 'resolved')); + const lastestResolvedComment = Array.from(this.codePanelRowData?.comments || []).reverse().find(comment => comment.isResolved && comment.changeHistory && comment.changeHistory.some(ch => ch.changeAction === 'resolved')); if (lastestResolvedComment) { - this.threadResolvedBy = lastestResolvedComment.changeHistory.findLast(ch => ch.changeAction === 'resolved')?.changedBy; + this.threadResolvedBy = lastestResolvedComment.changeHistory.reverse().find(ch => ch.changeAction === 'resolved')?.changedBy; } } - this.spacingBasedOnResolvedState = 'mb-2'; + this.spacingBasedOnResolvedState = (this.instanceLocation === "code-panel") ? 'mb-2' : ""; this.resolveThreadButtonText = 'Unresolve'; } else { this.threadResolvedBy = ''; - this.spacingBasedOnResolvedState = 'my-2'; + this.spacingBasedOnResolvedState = (this.instanceLocation === "code-panel") ? 'my-2' : ""; this.resolveThreadButtonText = 'Resolve'; } } @@ -232,6 +234,13 @@ export class CommentThreadComponent { saveCommentAction(event: Event) { const target = event.target as Element; const replyEditorContainer = target.closest(".reply-editor-container") as Element; + let revisionIdForConversationGroup: string | null | undefined = null; + let elementIdForConversationGroup: string | null | undefined = null; + + if (this.instanceLocation === "conversations") { + revisionIdForConversationGroup = target.closest(".conversation-group-revision-id")?.getAttribute("data-conversation-group-revision-id"); + elementIdForConversationGroup = (target.closest(".conversation-group-threads")?.getElementsByClassName("conversation-group-element-id")[0] as HTMLElement).innerText; + } if (replyEditorContainer) { const replyEditor = this.editor.find(e => e.editorId === "replyEditor"); @@ -239,10 +248,11 @@ export class CommentThreadComponent { this.saveCommentActionEmitter.emit( { nodeId: this.codePanelRowData!.nodeId, - nodeIdHashed: this.codePanelRowData!.nodeIdHashed, + nodeIdHashed: (this.instanceLocation === "conversations") ? elementIdForConversationGroup : this.codePanelRowData!.nodeIdHashed, commentText: content, allowAnyOneToResolve: this.allowAnyOneToResolve, - associatedRowPositionInGroup: this.codePanelRowData!.associatedRowPositionInGroup + associatedRowPositionInGroup: this.codePanelRowData!.associatedRowPositionInGroup, + revisionIdForConversationGroup: revisionIdForConversationGroup } ); this.codePanelRowData!.showReplyTextBox = false; @@ -254,10 +264,11 @@ export class CommentThreadComponent { this.saveCommentActionEmitter.emit( { nodeId: this.codePanelRowData!.nodeId, - nodeIdHashed: this.codePanelRowData!.nodeIdHashed, + nodeIdHashed: (this.instanceLocation === "conversations") ? elementIdForConversationGroup : this.codePanelRowData!.nodeIdHashed, commentId: commentId, commentText: content, - associatedRowPositionInGroup: this.codePanelRowData!.associatedRowPositionInGroup + associatedRowPositionInGroup: this.codePanelRowData!.associatedRowPositionInGroup, + revisionIdForConversationGroup: revisionIdForConversationGroup } ); this.codePanelRowData!.comments!.find(comment => comment.id === commentId)!.isInEditMode = false; diff --git a/src/dotnet/APIView/ClientSPA/src/app/_helpers/common-helpers.ts b/src/dotnet/APIView/ClientSPA/src/app/_helpers/common-helpers.ts index b9abf5b92e3..86e6d5b908b 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_helpers/common-helpers.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_helpers/common-helpers.ts @@ -6,6 +6,9 @@ export const SCROLL_TO_NODE_QUERY_PARAM = "nId"; export const FULL_DIFF_STYLE = "full"; export const TREE_DIFF_STYLE = "trees"; export const NODE_DIFF_STYLE = "nodes"; +export const MANUAL_ICON = "fa-solid fa-arrow-up-from-bracket"; +export const PR_ICON = "fa-solid fa-code-pull-request"; +export const AUTOMATIC_ICON = "fa-solid fa-robot"; export function getLanguageCssSafeName(language: string): string { switch (language.toLowerCase()) { @@ -29,4 +32,20 @@ export function mapLanguageAliases(languages: Iterable): string[] { result.add(language); } return Array.from(result); +} + +export function getTypeClass(type: string): string { + let result = ""; + switch (type) { + case 'manual': + result = MANUAL_ICON; + break; + case 'pullRequest': + result = PR_ICON; + break; + case 'automatic': + result = AUTOMATIC_ICON; + break; + } + return result; } \ No newline at end of file From aedd03a06570b6ee357b52ef5c122fbc0b040b8d Mon Sep 17 00:00:00 2001 From: Chidozie Ononiwu Date: Tue, 13 Aug 2024 14:22:11 -0700 Subject: [PATCH 5/7] Save, Edit and Delete form Conversation Page --- .../conversations.component.html | 6 +- .../conversations.component.spec.ts | 78 +++++++++++++++++ .../conversations/conversations.component.ts | 87 +++++++++++++++---- .../comment-thread.component.ts | 16 ++-- 4 files changed, 160 insertions(+), 27 deletions(-) diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html index 06992308748..7011a573840 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html @@ -20,7 +20,11 @@

Conversations

{{ commentThread!.comments[0].elementId }} + (saveCommentActionEmitter)="handleSaveCommentActionEmitter($event)" + (deleteCommentActionEmitter)="handleDeleteCommentActionEmitter($event)" + (commentUpvoteActionEmitter)="handleCommentUpvoteActionEmitter($event)" + (commentResolutionActionEmitter)="handleCommentResolutionActionEmitter($event)" + >
diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.spec.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.spec.ts index bb20a684162..cf486d54c0a 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.spec.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.spec.ts @@ -4,6 +4,8 @@ import { ConversationsComponent } from './conversations.component'; import { SharedAppModule } from 'src/app/_modules/shared/shared-app.module'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ReviewPageModule } from 'src/app/_modules/review-page/review-page.module'; +import { APIRevision } from 'src/app/_models/revision'; +import { CommentItemModel } from 'src/app/_models/commentItemModel'; describe('ConversationComponent', () => { let component: ConversationsComponent; @@ -26,4 +28,80 @@ describe('ConversationComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe('createCommentThreads', () => { + it('should group conversation by elementId and latest API revision of comments', () => { + const apiRevisions = [ + { + id: '1', + createdOn: '2021-10-01T00:00:00Z' + }, + { + id: '2', + createdOn: '2022-10-01T00:00:00Z' + }, + { + id: '3', + createdOn: '2023-10-01T00:00:00Z' + }, + { + id: '4', + createdOn: '2024-10-01T00:00:00Z' + } + ] as APIRevision[]; + + const comments = [ + { + id: '1', + elementId: '1', + apiRevisionId: '1' + }, + { + id: '2', + elementId: '2', + apiRevisionId: '1' + }, + { + id: '3', + elementId: '3', + apiRevisionId: '1' + }, + { + id: '4', + elementId: '1', + apiRevisionId: '2' + }, + { + id: '5', + elementId: '2', + apiRevisionId: '2' + }, + { + id: '6', + elementId: '3', + apiRevisionId: '2' + }, + { + id: '7', + elementId: '2', + apiRevisionId: '3' + }, + { + id: '8', + elementId: '2', + apiRevisionId: '4' + }, + ] as CommentItemModel[]; + + component.apiRevisions = apiRevisions; + component.comments = comments; + fixture.detectChanges(); + component.createCommentThreads(); + + expect(component.commentThreads.size).toBe(2); + + const keys = Array.from(component.commentThreads.keys()); + expect(keys).toEqual(['2', '4']); + }); + }); }); diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.ts index 2feb00d6a2e..3c353763066 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { CodePanelRowData, CodePanelRowDatatype } from 'src/app/_models/codePanelModels'; import { CommentItemModel, CommentType } from 'src/app/_models/commentItemModel'; import { APIRevision } from 'src/app/_models/revision'; @@ -6,6 +6,7 @@ import { getTypeClass } from 'src/app/_helpers/common-helpers'; import { CommentsService } from 'src/app/_services/comments/comments.service'; import { take } from 'rxjs'; import { Review } from 'src/app/_models/review'; +import { UserProfile } from 'src/app/_models/userProfile'; @Component({ selector: 'app-conversations', @@ -16,6 +17,7 @@ export class ConversationsComponent implements OnChanges { @Input() apiRevisions: APIRevision[] = []; @Input() comments: CommentItemModel[] = []; @Input() review : Review | undefined = undefined; + @Input() userProfile : UserProfile | undefined; commentThreads: Map = new Map(); @@ -30,6 +32,7 @@ export class ConversationsComponent implements OnChanges { } createCommentThreads() { + this.commentThreads = new Map(); const apiRevisionInOrder = this.apiRevisions.sort((a, b) => (new Date(b.createdOn) as any) - (new Date(a.createdOn) as any)); const groupedComments = this.comments .reduce((acc: { [key: string]: CommentItemModel[] }, comment) => { @@ -80,25 +83,71 @@ export class ConversationsComponent implements OnChanges { getAPIRevisionTypeClass(apiRevision: APIRevision) { return getTypeClass(apiRevision.apiRevisionType); } - handleSaveCommentActionEmitter(data: any) { - console.log(data); - //if (data.commentId) { - // this.commentsService.updateComment(this.review?.id!, data.commentId, data.commentText).pipe(take(1)).subscribe({ - // next: () => { - // //this.updateHasActiveConversations(); - // } - // }); - //} - //else { - // this.commentsService.createComment(this.review?.id!, data.conversationGroupId!, data.nodeId, data.commentText, CommentType.APIRevision, data.allowAnyOneToResolve) - // .pipe(take(1)).subscribe({ - // next: (response: CommentItemModel) => { - // //this.updateHasActiveConversations(); - // } - // } - // ); - //} + if (data.commentId) { + this.commentsService.updateComment(this.review?.id!, data.commentId, data.commentText).pipe(take(1)).subscribe({ + next: () => { + this.comments.find(c => c.id === data.commentId)!.commentText = data.commentText; + } + }); + } + else { + this.commentsService.createComment(this.review?.id!, data.revisionIdForConversationGroup!, data.nodeId, data.commentText, CommentType.APIRevision, data.allowAnyOneToResolve) + .pipe(take(1)).subscribe({ + next: (response: CommentItemModel) => { + this.comments.push(response); + this.createCommentThreads(); + } + } + ); + } + } + + handleCommentUpvoteActionEmitter(data: any){ + this.commentsService.toggleCommentUpVote(this.review?.id!, data.commentId).pipe(take(1)).subscribe({ + next: () => { + const comment = this.comments.find(c => c.id === data.commentId) + if (comment) { + if (comment.upvotes.includes(this.userProfile?.userName!)) { + comment.upvotes.splice(comment.upvotes.indexOf(this.userProfile?.userName!), 1); + } else { + comment.upvotes.push(this.userProfile?.userName!); + } + } + } + }); + } + + handleDeleteCommentActionEmitter(data: any) { + this.commentsService.deleteComment(this.review?.id!, data.commentId).pipe(take(1)).subscribe({ + next: () => { + this.comments = this.comments.filter(c => c.id !== data.commentId); + this.createCommentThreads(); + } + }); + } + + handleCommentResolutionActionEmitter(data: any) { + if (data.action === "Resolve") { + this.commentsService.resolveComments(this.review?.id!, data.elementId).pipe(take(1)).subscribe({ + next: () => { + this.comments.filter(c => c.elementId === data.elementId).forEach(c => { + c.isResolved = true; + }); + this.createCommentThreads(); + } + }); + } + if (data.action === "Unresolve") { + this.commentsService.unresolveComments(this.review?.id!, data.elementId).pipe(take(1)).subscribe({ + next: () => { + this.comments.filter(c => c.elementId === data.elementId).forEach(c => { + c.isResolved = false; + }); + this.createCommentThreads(); + } + }); + } } } diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.ts index 4d38ea5cbac..d8647b968db 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.ts @@ -179,14 +179,16 @@ export class CommentThreadComponent { const commentId = target.getAttribute("data-item-id"); const commentData = this.codePanelRowData?.comments?.find(comment => comment.id === commentId)?.commentText.replace(/<[^>]*>/g, '').trim(); - console.log(this.associatedCodeLine); - - const codeLineContent = this.associatedCodeLine + let codeLineContent = this.associatedCodeLine ? this.associatedCodeLine.rowOfTokens .map(token => token.value) .join('') : ''; + if (!codeLineContent) { + codeLineContent = this.codePanelRowData?.comments[0].elementId!; + } + const nodeId: string = this.codePanelRowData?.nodeId ?? 'defaultNodeId'; const apiViewUrl = `${window.location.href.split("#")[0]}&nId=${encodeURIComponent(nodeId)}`; const issueBody = encodeURIComponent(`\`\`\`${event.item?.title}\n${codeLineContent}\n\`\`\`\n#\n${commentData}\n#\n[Created from ApiView comment](${apiViewUrl})`); @@ -247,8 +249,8 @@ export class CommentThreadComponent { const content = replyEditor?.getEditorContent(); this.saveCommentActionEmitter.emit( { - nodeId: this.codePanelRowData!.nodeId, - nodeIdHashed: (this.instanceLocation === "conversations") ? elementIdForConversationGroup : this.codePanelRowData!.nodeIdHashed, + nodeId: (this.instanceLocation === "conversations") ? elementIdForConversationGroup : this.codePanelRowData!.nodeId, + nodeIdHashed: this.codePanelRowData!.nodeIdHashed, commentText: content, allowAnyOneToResolve: this.allowAnyOneToResolve, associatedRowPositionInGroup: this.codePanelRowData!.associatedRowPositionInGroup, @@ -263,8 +265,8 @@ export class CommentThreadComponent { const content = replyEditor?.getEditorContent(); this.saveCommentActionEmitter.emit( { - nodeId: this.codePanelRowData!.nodeId, - nodeIdHashed: (this.instanceLocation === "conversations") ? elementIdForConversationGroup : this.codePanelRowData!.nodeIdHashed, + nodeId: (this.instanceLocation === "conversations") ? elementIdForConversationGroup : this.codePanelRowData!.nodeId, + nodeIdHashed: this.codePanelRowData!.nodeIdHashed, commentId: commentId, commentText: content, associatedRowPositionInGroup: this.codePanelRowData!.associatedRowPositionInGroup, From b0ca5682ba5ebe01c19658239dc7872d4c3eb310 Mon Sep 17 00:00:00 2001 From: Chidozie Ononiwu Date: Tue, 13 Aug 2024 18:25:44 -0700 Subject: [PATCH 6/7] Conversiation Page Fully Working --- .../code-panel/code-panel.component.ts | 8 ++--- .../conversations.component.html | 3 +- .../conversations.component.scss | 4 +++ .../conversations.component.spec.ts | 20 ++++++++++-- .../conversations/conversations.component.ts | 32 ++++++++++++++++--- .../review-page-options.component.ts | 2 +- .../review-page/review-page.component.html | 16 ++++++++-- .../review-page/review-page.component.scss | 16 +++++++--- .../review-page/review-page.component.ts | 15 +++++++++ .../comment-thread.component.html | 2 +- .../comment-thread.component.scss | 4 +++ .../comment-thread.component.spec.ts | 4 --- .../comment-thread.component.ts | 1 - .../src/app/_helpers/router-helpers.ts | 3 +- .../app/_modules/shared/shared-app.module.ts | 5 ++- 15 files changed, 108 insertions(+), 27 deletions(-) diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/code-panel/code-panel.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/code-panel/code-panel.component.ts index 07ffae8e113..bcf7198470e 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/code-panel/code-panel.component.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/code-panel/code-panel.component.ts @@ -30,7 +30,7 @@ export class CodePanelComponent implements OnChanges{ @Input() showLineNumbers: boolean = true; @Input() loadFailed : boolean = false; - @Output() hasActiveConversation : EventEmitter = new EventEmitter(); + @Output() hasActiveConversationEmitter : EventEmitter = new EventEmitter(); noDiffInContentMessage : Message[] = [{ severity: 'info', icon:'bi bi-info-circle', detail: 'There is no difference between the two API revisions.' }]; isLoading: boolean = true; @@ -524,16 +524,16 @@ export class CodePanelComponent implements OnChanges{ } private updateHasActiveConversations() { - let hasActiveConversations = false; + let hasActiveConversation = false; for (let row of this.codePanelRowData) { if (row.type === CodePanelRowDatatype.CommentThread) { if (row.comments && row.comments.length > 0 && row.isResolvedCommentThread === false) { - hasActiveConversations = true; + hasActiveConversation = true; break; } } } - this.hasActiveConversation.emit(hasActiveConversations); + this.hasActiveConversationEmitter.emit(hasActiveConversation); } private loadCodePanelViewPort() { diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html index 7011a573840..5d6486442f0 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html @@ -1,5 +1,6 @@

Conversations

+

This Review has no comments

@@ -18,7 +19,7 @@

Conversations

{{ apiRevision.label }}
- {{ commentThread!.comments[0].elementId }} + {{ commentThread!.comments[0].elementId }} { let component: ConversationsComponent; @@ -19,6 +21,17 @@ describe('ConversationComponent', () => { ReviewPageModule, SharedAppModule ], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({ reviewId: 'test' }), + }, + queryParams: of(convertToParamMap({ activeApiRevisionId: 'test', diffApiRevisionId: 'test' })) + } + } + ] }); fixture = TestBed.createComponent(ConversationsComponent); component = fixture.componentInstance; @@ -69,7 +82,8 @@ describe('ConversationComponent', () => { { id: '4', elementId: '1', - apiRevisionId: '2' + apiRevisionId: '2', + isResolved: true }, { id: '5', @@ -79,7 +93,8 @@ describe('ConversationComponent', () => { { id: '6', elementId: '3', - apiRevisionId: '2' + apiRevisionId: '2', + isResolved: true }, { id: '7', @@ -102,6 +117,7 @@ describe('ConversationComponent', () => { const keys = Array.from(component.commentThreads.keys()); expect(keys).toEqual(['2', '4']); + expect(component.numberOfActiveThreads).toBe(1); }); }); }); diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.ts index 3c353763066..f2d3e825330 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.ts @@ -1,12 +1,13 @@ -import { ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; import { CodePanelRowData, CodePanelRowDatatype } from 'src/app/_models/codePanelModels'; import { CommentItemModel, CommentType } from 'src/app/_models/commentItemModel'; import { APIRevision } from 'src/app/_models/revision'; -import { getTypeClass } from 'src/app/_helpers/common-helpers'; +import { getTypeClass, SCROLL_TO_NODE_QUERY_PARAM } from 'src/app/_helpers/common-helpers'; import { CommentsService } from 'src/app/_services/comments/comments.service'; import { take } from 'rxjs'; import { Review } from 'src/app/_models/review'; import { UserProfile } from 'src/app/_models/userProfile'; +import { ActivatedRoute, Router } from '@angular/router'; @Component({ selector: 'app-conversations', @@ -15,13 +16,18 @@ import { UserProfile } from 'src/app/_models/userProfile'; }) export class ConversationsComponent implements OnChanges { @Input() apiRevisions: APIRevision[] = []; + @Input() activeApiRevisionId: string | null = null; @Input() comments: CommentItemModel[] = []; @Input() review : Review | undefined = undefined; @Input() userProfile : UserProfile | undefined; + @Output() scrollToNodeEmitter : EventEmitter = new EventEmitter(); + @Output() numberOfActiveThreadsEmitter : EventEmitter = new EventEmitter(); + commentThreads: Map = new Map(); + numberOfActiveThreads: number = 0; - constructor(private commentsService: CommentsService) { } + constructor(private commentsService: CommentsService, private route: ActivatedRoute, private router: Router) { } ngOnChanges(changes: SimpleChanges) { if (changes['apiRevisions'] || changes['comments']) { @@ -33,6 +39,7 @@ export class ConversationsComponent implements OnChanges { createCommentThreads() { this.commentThreads = new Map(); + this.numberOfActiveThreads = 0; const apiRevisionInOrder = this.apiRevisions.sort((a, b) => (new Date(b.createdOn) as any) - (new Date(a.createdOn) as any)); const groupedComments = this.comments .reduce((acc: { [key: string]: CommentItemModel[] }, comment) => { @@ -65,15 +72,20 @@ export class ConversationsComponent implements OnChanges { codePanelRowData.comments = comments; codePanelRowData.isResolvedCommentThread = comments.some(c => c.isResolved); + if (!codePanelRowData.isResolvedCommentThread) { + this.numberOfActiveThreads++; + } + if (this.commentThreads.has(apiRevisionIdForThread)) { this.commentThreads.get(apiRevisionIdForThread)?.push(codePanelRowData); } else { - this.commentThreads.set(apiRevisionIdForThread, [codePanelRowData]); + this.commentThreads.set(apiRevisionIdForThread, [codePanelRowData]); } } } } + this.numberOfActiveThreadsEmitter.emit(this.numberOfActiveThreads); } getAPIRevisionWithComments() { @@ -83,6 +95,18 @@ export class ConversationsComponent implements OnChanges { getAPIRevisionTypeClass(apiRevision: APIRevision) { return getTypeClass(apiRevision.apiRevisionType); } + + navigateToCommentThreadOnRevisionPage(event: Event) { + const target = event.target as Element; + const revisionIdForConversationGroup = target.closest(".conversation-group-revision-id")?.getAttribute("data-conversation-group-revision-id"); + const elementIdForConversationGroup = (target.closest(".conversation-group-threads")?.getElementsByClassName("conversation-group-element-id")[0] as HTMLElement).innerText; + + if (this.activeApiRevisionId && this.activeApiRevisionId === revisionIdForConversationGroup) { + this.scrollToNodeEmitter.emit(elementIdForConversationGroup); + } else { + window.open(`review/${this.review?.id}?activeApiRevisionId=${revisionIdForConversationGroup}&nId=${elementIdForConversationGroup}`, '_blank'); + } + } handleSaveCommentActionEmitter(data: any) { if (data.commentId) { diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page-options/review-page-options.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page-options/review-page-options.component.ts index d8730b70e6c..d543a36b7ea 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page-options/review-page-options.component.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page-options/review-page-options.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { InputSwitchOnChangeEvent } from 'primeng/inputswitch'; import { getQueryParams } from 'src/app/_helpers/router-helpers'; diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html index 2183586b3eb..5e0589773c7 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html @@ -9,7 +9,14 @@ (pageOptionsEmitter)="handlePageOptionsEmitter($event)">
- + + + + + + + +
+ (hasActiveConversationEmitter)="handleHasActiveConversationEmitter($event)">
@@ -76,6 +83,9 @@ + [review]="review" + [activeApiRevisionId]="activeApiRevisionId" + (scrollToNodeEmitter)="handleScrollToNodeEmitter($event)" + (numberOfActiveThreadsEmitter)="handleNumberOfActiveThreadsEmitter($event)"> \ No newline at end of file diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.scss b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.scss index a192c35b8d2..e9b8f815d47 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.scss +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.scss @@ -11,6 +11,18 @@ display: block; min-width: 0; } + + .side-menu { + .p-menuitem-link { + font-size: x-large; + } + + p-badge { + position: relative; + left: -1.2rem; + top: -1.4rem; + } + } .p-menu { background: var(--base-fg-color); @@ -43,8 +55,4 @@ .revisions-sidebar, .conversation-sidebar { width: 75dvw; } - - .conversation-sidebar { - width: 60dvw; - } } \ No newline at end of file diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.ts index b1e7652262a..7b07254b933 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.ts @@ -48,6 +48,7 @@ export class ReviewPageComponent implements OnInit { preferredApprovers : string[] = []; hasFatalDiagnostics : boolean = false; hasActiveConversation : boolean = false; + numberOfActiveConversation : number = 0; hasHiddenAPIs : boolean = false; loadFailed : boolean = false; @@ -104,7 +105,10 @@ export class ReviewPageComponent implements OnInit { this.loadPreferredApprovers(this.reviewId!); this.loadAPIRevisions(0, this.apiRevisionPageSize); this.loadComments(); + this.createSideMenu(); + } + createSideMenu() { this.sideMenu = [ { icon: 'bi bi-clock-history', @@ -112,6 +116,7 @@ export class ReviewPageComponent implements OnInit { }, { icon: 'bi bi-chat-left-dots', + badge: (this.numberOfActiveConversation > 0) ? this.numberOfActiveConversation.toString() : undefined, command: () => { this.conversationSidePanel = !this.conversationSidePanel; } } ]; @@ -419,6 +424,16 @@ export class ReviewPageComponent implements OnInit { this.hasActiveConversation = value; } + handleNumberOfActiveThreadsEmitter(value: number) { + this.numberOfActiveConversation = value; + this.createSideMenu(); + } + + handleScrollToNodeEmitter (value: string) { + this.conversationSidePanel = false; + this.codePanelComponent.scrollToNode(undefined, value); + } + checkForFatalDiagnostics() { for (const rowData of this.codePanelRowData) { if (rowData.diagnostics && rowData.diagnostics.level === 'fatal') { diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.html b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.html index 99cb19d8829..03e5f7b3ca7 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.html +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.html @@ -1,7 +1,7 @@
This thread is marked resolved by {{ threadResolvedBy }}  {{threadResolvedStateToggleText}} Resolved
-
+
diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.scss b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.scss index 285c4f75d8b..d0256c539c8 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.scss +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.scss @@ -1,5 +1,9 @@ :host ::ng-deep { font-family: var(--font-family); + + .comment-thread-container { + max-width: 1000px; + } .user-avartar { border: 2px solid var(--border-color); diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.spec.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.spec.ts index 0562dccfd86..3f8cf2ccf42 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.spec.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.spec.ts @@ -38,10 +38,6 @@ describe('CommentThreadComponent', () => { changeHistory: [ { changeAction: 'resolved', changedBy: 'test user 1', - }, - { - changeAction: 'resolved', - changedBy: 'test user 2', }] } as CommentItemModel; const comment2 = { diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.ts index d8647b968db..f9c5490e591 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.ts @@ -6,7 +6,6 @@ import { environment } from 'src/environments/environment'; import { EditorComponent } from '../editor/editor.component'; import { CodePanelRowData } from 'src/app/_models/codePanelModels'; import { UserProfile } from 'src/app/_models/userProfile'; -import { first } from 'rxjs'; @Component({ selector: 'app-comment-thread', diff --git a/src/dotnet/APIView/ClientSPA/src/app/_helpers/router-helpers.ts b/src/dotnet/APIView/ClientSPA/src/app/_helpers/router-helpers.ts index 6200da84d04..10ac49c1f6f 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_helpers/router-helpers.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_helpers/router-helpers.ts @@ -1,6 +1,7 @@ import { ActivatedRoute } from "@angular/router"; +import { SCROLL_TO_NODE_QUERY_PARAM } from "./common-helpers"; -export function getQueryParams(route: ActivatedRoute, excludedKeys: string[] = ["nId"]) { +export function getQueryParams(route: ActivatedRoute, excludedKeys: string[] = [SCROLL_TO_NODE_QUERY_PARAM]) { return route.snapshot.queryParamMap.keys.reduce((params: { [key: string]: any; }, key) => { if (!excludedKeys.includes(key)) { params[key] = route.snapshot.queryParamMap.get(key); diff --git a/src/dotnet/APIView/ClientSPA/src/app/_modules/shared/shared-app.module.ts b/src/dotnet/APIView/ClientSPA/src/app/_modules/shared/shared-app.module.ts index 971a28d4928..2474e4426a8 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_modules/shared/shared-app.module.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_modules/shared/shared-app.module.ts @@ -20,6 +20,7 @@ import { SelectButtonModule } from 'primeng/selectbutton'; import { FileUploadModule } from 'primeng/fileupload'; import { InputTextModule } from 'primeng/inputtext'; import { MessagesModule } from 'primeng/messages'; +import { BadgeModule } from 'primeng/badge'; @NgModule({ @@ -38,6 +39,7 @@ import { MessagesModule } from 'primeng/messages'; LanguageNamesPipe, LastUpdatedOnPipe, ApprovalPipe, + BadgeModule, ContextMenuModule, TableModule, ChipModule, @@ -52,9 +54,10 @@ import { MessagesModule } from 'primeng/messages'; SplitterModule, SidebarModule, TimeagoModule, - InputTextModule + InputTextModule, ], imports: [ + BadgeModule, CommonModule, ContextMenuModule, TableModule, From 0beb2e9a2d8e2e2707a6dbf8271404784b87dd4f Mon Sep 17 00:00:00 2001 From: Chidozie Ononiwu Date: Wed, 14 Aug 2024 15:09:29 -0700 Subject: [PATCH 7/7] Adjust lastUpdated to last updated --- .../api-revision-options/api-revision-options.component.html | 4 ++-- .../_components/conversations/conversations.component.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.html b/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.html index d08019b1bb7..71cb5bfd29e 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.html +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/api-revision-options/api-revision-options.component.html @@ -57,7 +57,7 @@ Latest Released
created: {{ apiRevision.createdOn | timeago }} - lastUpdated: {{ apiRevision | lastUpdatedOn | timeago }} + last updated: {{ apiRevision | lastUpdatedOn | timeago }} released: {{ apiRevision.releasedOn | timeago }}
{{ apiRevision.label }}
@@ -126,7 +126,7 @@ Latest Main Latest Released
created: {{ apiRevision.createdOn | timeago }} - lastUpdated: {{ apiRevision | lastUpdatedOn | timeago }} + last updated: {{ apiRevision | lastUpdatedOn | timeago }} released: {{ apiRevision.releasedOn | timeago }}
{{ apiRevision.label }}
diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html index 5d6486442f0..9964bbfc728 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html @@ -15,7 +15,7 @@

Conversations

{{ apiRevision.createdBy }} released: {{ apiRevision.releasedOn | timeago }} created: {{ apiRevision.createdOn | timeago }} - lastUpdated: {{ apiRevision | lastUpdatedOn | timeago }} + last updated: {{ apiRevision | lastUpdatedOn | timeago }} {{ apiRevision.label }}