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 07ffae8e1136..bd7109b70c13 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,15 @@ 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; - break; + hasActiveConversation = true; } } } - 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 7011a573840c..3c9ac5c25435 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; 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 3c3537630668..f2d3e825330d 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 d8730b70e6ce..d543a36b7eaa 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 2183586b3eb7..5e0589773c77 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 a192c35b8d2a..e9b8f815d47b 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 b1e7652262a3..7b07254b9335 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 99cb19d88293..03e5f7b3ca76 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 285c4f75d8b7..d0256c539c87 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.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/shared/comment-thread/comment-thread.component.ts index d8647b968dbe..f9c5490e591b 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 6200da84d04c..10ac49c1f6f8 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 971a28d49280..2474e4426a84 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,