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..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
@@ -40,7 +40,7 @@
{{ selectedActiveAPIRevision.prNo }}
version: {{ selectedActiveAPIRevision.version }}
- {{ selectedActiveAPIRevision.creatorBy }}
+ {{ selectedActiveAPIRevision.createdBy }}
released: {{ selectedActiveAPIRevision.releasedOn | timeago }}
@@ -50,14 +50,14 @@
{{ apiRevision.prNo }}
version: {{ apiRevision.version }}
- {{ apiRevision.creatorBy }}
+ {{ apiRevision.createdBy }}
Latest GA
Latest Approved
Latest Main
Latest Released
created: {{ apiRevision.createdOn | timeago }}
- lastUpdated: {{ apiRevision | lastUpdatedOn | timeago }}
+ last updated: {{ apiRevision | lastUpdatedOn | timeago }}
released: {{ apiRevision.releasedOn | timeago }}
{{ apiRevision.label }}
@@ -110,7 +110,7 @@
{{ selectedDiffAPIRevision.prNo }}
version: {{ selectedDiffAPIRevision.version }}
- {{ selectedDiffAPIRevision.creatorBy }}
+ {{ selectedDiffAPIRevision.createdBy }}
released: {{ selectedDiffAPIRevision.releasedOn | timeago }}
@@ -120,13 +120,13 @@
{{ apiRevision.prNo }}
version: {{ apiRevision.version }}
- {{ apiRevision.creatorBy }}
+ {{ apiRevision.createdBy }}
Latest GA
Latest Approved
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/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..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,29 +156,17 @@ 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,
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/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
new file mode 100644
index 00000000000..9964bbfc728
--- /dev/null
+++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.html
@@ -0,0 +1,32 @@
+Conversations
+
+This Review has no comments
+
+
+
+
+
+
+
+
+ {{ apiRevision.pullRequestNo }}
+ version: {{ apiRevision.packageVersion }}
+
+ {{ apiRevision.createdBy }}
+ released: {{ apiRevision.releasedOn | timeago }}
+ created: {{ apiRevision.createdOn | timeago }}
+ last updated: {{ apiRevision | lastUpdatedOn | timeago }}
+ {{ apiRevision.label }}
+
+
+
+
+
\ No newline at end of file
diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.scss b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.scss
new file mode 100644
index 00000000000..b910b800b1a
--- /dev/null
+++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.scss
@@ -0,0 +1,48 @@
+: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);
+ }
+ }
+
+ .conversation-group-element-id {
+ cursor: pointer;
+ }
+}
\ 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
new file mode 100644
index 00000000000..637f0ec8e84
--- /dev/null
+++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.spec.ts
@@ -0,0 +1,123 @@
+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';
+import { APIRevision } from 'src/app/_models/revision';
+import { CommentItemModel } from 'src/app/_models/commentItemModel';
+import { ActivatedRoute, convertToParamMap } from '@angular/router';
+import { of } from 'rxjs';
+
+describe('ConversationComponent', () => {
+ let component: ConversationsComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [ConversationsComponent],
+ imports: [
+ HttpClientTestingModule,
+ 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;
+ fixture.detectChanges();
+ });
+
+ 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',
+ isResolved: true
+ },
+ {
+ id: '5',
+ elementId: '2',
+ apiRevisionId: '2'
+ },
+ {
+ id: '6',
+ elementId: '3',
+ apiRevisionId: '2',
+ isResolved: true
+ },
+ {
+ 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']);
+ 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
new file mode 100644
index 00000000000..f2d3e825330
--- /dev/null
+++ b/src/dotnet/APIView/ClientSPA/src/app/_components/conversations/conversations.component.ts
@@ -0,0 +1,177 @@
+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, 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',
+ templateUrl: './conversations.component.html',
+ styleUrls: ['./conversations.component.scss']
+})
+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, private route: ActivatedRoute, private router: Router) { }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes['apiRevisions'] || changes['comments']) {
+ if (this.apiRevisions.length > 0 && this.comments.length > 0) {
+ this.createCommentThreads();
+ }
+ }
+ }
+
+ 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) => {
+ const key = comment.elementId;
+ if (!acc[key]) {
+ acc[key] = [];
+ }
+ acc[key].push(comment);
+ return acc;
+ }, {});
+
+ 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;
+ }
+ }
+
+ 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);
+
+ if (!codePanelRowData.isResolvedCommentThread) {
+ this.numberOfActiveThreads++;
+ }
+
+ if (this.commentThreads.has(apiRevisionIdForThread)) {
+ this.commentThreads.get(apiRevisionIdForThread)?.push(codePanelRowData);
+ }
+ else {
+ this.commentThreads.set(apiRevisionIdForThread, [codePanelRowData]);
+ }
+ }
+ }
+ }
+ this.numberOfActiveThreadsEmitter.emit(this.numberOfActiveThreads);
+ }
+
+ getAPIRevisionWithComments() {
+ return this.apiRevisions.filter(apiRevision => this.commentThreads.has(apiRevision.id));
+ }
+
+ 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) {
+ 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/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 df63fef727e..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)">
@@ -67,9 +74,18 @@
-
+
+ [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..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);
@@ -40,7 +52,7 @@
}
}
- .revisions-sidebar {
+ .revisions-sidebar, .conversation-sidebar {
width: 75dvw;
}
}
\ 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..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
@@ -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;
@@ -45,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;
@@ -68,7 +72,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 +104,20 @@ export class ReviewPageComponent implements OnInit {
this.loadReview(this.reviewId!);
this.loadPreferredApprovers(this.reviewId!);
this.loadAPIRevisions(0, this.apiRevisionPageSize);
+ this.loadComments();
+ this.createSideMenu();
+ }
+ createSideMenu() {
this.sideMenu = [
{
icon: 'bi bi-clock-history',
- command: () => { this.revisionSideBarVisible = !this.revisionSideBarVisible; }
+ command: () => { this.revisionSidePanel = !this.revisionSidePanel; }
+ },
+ {
+ icon: 'bi bi-chat-left-dots',
+ badge: (this.numberOfActiveConversation > 0) ? this.numberOfActiveConversation.toString() : undefined,
+ command: () => { this.conversationSidePanel = !this.conversationSidePanel; }
}
];
}
@@ -215,6 +228,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({
@@ -402,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/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/_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
-
+