071824.01/conversation (#8834)
* APIView Conversation Page * Show Comment Threads on Conversation Page * Ad test for comment thread * handle Save Comment from Conversation Page * Save, Edit and Delete form Conversation Page * Conversiation Page Fully Working * Adjust lastUpdated to last updated
This commit is contained in:
Родитель
0b9efdb190
Коммит
a62328ff6a
|
@ -40,7 +40,7 @@
|
|||
<span *ngIf="selectedActiveAPIRevision.prNo"> {{ selectedActiveAPIRevision.prNo }}</span>
|
||||
<span class="emphasis-badge info ms-2">version: {{ selectedActiveAPIRevision.version }}</span>
|
||||
<i class="fas fa-check-circle text-success ms-2" *ngIf="selectedActiveAPIRevision.isApproved"></i>
|
||||
<span *ngIf="selectedActiveAPIRevision.apiRevisionType !== 'Automatic'" class="ms-2">{{ selectedActiveAPIRevision.creatorBy }}</span>
|
||||
<span *ngIf="selectedActiveAPIRevision.apiRevisionType !== 'Automatic'" class="ms-2">{{ selectedActiveAPIRevision.createdBy }}</span>
|
||||
<span *ngIf="selectedActiveAPIRevision.isReleased" class="emphasis-badge success ms-2">released: {{ selectedActiveAPIRevision.releasedOn | timeago }}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@ -50,14 +50,14 @@
|
|||
<span *ngIf="apiRevision.prNo"> {{ apiRevision.prNo }}</span>
|
||||
<span class="emphasis-badge info ms-2">version: {{ apiRevision.version }}</span>
|
||||
<i class="fas fa-check-circle text-success ms-2" *ngIf="apiRevision.isApproved"></i>
|
||||
<span *ngIf="apiRevision.apiRevisionType !== 'Automatic'" class="ms-2">{{ apiRevision.creatorBy }}</span>
|
||||
<span *ngIf="apiRevision.apiRevisionType !== 'Automatic'" class="ms-2">{{ apiRevision.createdBy }}</span>
|
||||
<span *ngIf="apiRevision.isLatestGA" class="emphasis-badge warn small ms-2">Latest GA</span>
|
||||
<span *ngIf="apiRevision.isLatestApproved" class="emphasis-badge warn small ms-2">Latest Approved</span>
|
||||
<span *ngIf="apiRevision.isLatestMain" class="emphasis-badge warn small ms-2">Latest Main</span>
|
||||
<span *ngIf="apiRevision.isLatestReleased" class="emphasis-badge warn small ms-2">Latest Released</span>
|
||||
<div class="small">
|
||||
<span class="emphasis-badge secondary">created: {{ apiRevision.createdOn | timeago }}</span>
|
||||
<span class="emphasis-badge secondary ms-2">lastUpdated: {{ apiRevision | lastUpdatedOn | timeago }}</span>
|
||||
<span class="emphasis-badge secondary ms-2">last updated: {{ apiRevision | lastUpdatedOn | timeago }}</span>
|
||||
<span *ngIf="apiRevision.isReleased" class="emphasis-badge success ms-2">released: {{ apiRevision.releasedOn | timeago }}</span>
|
||||
</div>
|
||||
<div *ngIf="apiRevision.apiRevisionType === 'Manual' && apiRevision.label">{{ apiRevision.label }}</div>
|
||||
|
@ -110,7 +110,7 @@
|
|||
<span *ngIf="selectedDiffAPIRevision.prNo"> {{ selectedDiffAPIRevision.prNo }}</span>
|
||||
<span class="emphasis-badge info ms-2">version: {{ selectedDiffAPIRevision.version }}</span>
|
||||
<i class="fas fa-check-circle text-success ms-2" *ngIf="selectedDiffAPIRevision.isApproved"></i>
|
||||
<span *ngIf="selectedDiffAPIRevision.apiRevisionType !== 'Automatic'" class="ms-2">{{ selectedDiffAPIRevision.creatorBy }}</span>
|
||||
<span *ngIf="selectedDiffAPIRevision.apiRevisionType !== 'Automatic'" class="ms-2">{{ selectedDiffAPIRevision.createdBy }}</span>
|
||||
<span *ngIf="selectedDiffAPIRevision.isReleased" class="emphasis-badge success ms-2">released: {{ selectedDiffAPIRevision.releasedOn | timeago }}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@ -120,13 +120,13 @@
|
|||
<span *ngIf="apiRevision.prNo"> {{ apiRevision.prNo }}</span>
|
||||
<span class="emphasis-badge info ms-2">version: {{ apiRevision.version }}</span>
|
||||
<i class="fas fa-check-circle text-success ms-2" *ngIf="apiRevision.isApproved"></i>
|
||||
<span *ngIf="apiRevision.apiRevisionType !== 'Automatic'" class="ms-2">{{ apiRevision.creatorBy }}</span>
|
||||
<span *ngIf="apiRevision.apiRevisionType !== 'Automatic'" class="ms-2">{{ apiRevision.createdBy }}</span>
|
||||
<span *ngIf="apiRevision.isLatestGA" class="emphasis-badge warn small ms-2">Latest GA</span>
|
||||
<span *ngIf="apiRevision.isLatestApproved" class="emphasis-badge warn small ms-2">Latest Approved</span>
|
||||
<span *ngIf="apiRevision.isLatestMain" class="emphasis-badge warn small ms-2">Latest Main</span>
|
||||
<span *ngIf="apiRevision.isLatestReleased" class="emphasis-badge warn small ms-2">Latest Released</span>
|
||||
<div class="small"><span class="emphasis-badge secondary">created: {{ apiRevision.createdOn | timeago }}</span>
|
||||
<span class="emphasis-badge secondary ms-2">lastUpdated: {{ apiRevision | lastUpdatedOn | timeago }}</span>
|
||||
<span class="emphasis-badge secondary ms-2">last updated: {{ apiRevision | lastUpdatedOn | timeago }}</span>
|
||||
<span *ngIf="apiRevision.isReleased" class="emphasis-badge success ms-2">released: {{ apiRevision.releasedOn | timeago }}</span></div>
|
||||
<div *ngIf="apiRevision.apiRevisionType === 'Manual' && apiRevision.label">{{ apiRevision.label }}</div>
|
||||
</div>
|
||||
|
|
|
@ -76,6 +76,4 @@
|
|||
border: 1px solid var(--alert-secondary-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -30,7 +30,7 @@ export class CodePanelComponent implements OnChanges{
|
|||
@Input() showLineNumbers: boolean = true;
|
||||
@Input() loadFailed : boolean = false;
|
||||
|
||||
@Output() hasActiveConversation : EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
@Output() hasActiveConversationEmitter : EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
|
||||
noDiffInContentMessage : Message[] = [{ severity: 'info', icon:'bi bi-info-circle', detail: 'There is no difference between the two API revisions.' }];
|
||||
isLoading: boolean = true;
|
||||
|
@ -528,16 +528,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() {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<h4>Conversations</h4>
|
||||
<p-divider />
|
||||
<p *ngIf="commentThreads.size === 0">This Review has no comments</p>
|
||||
<p-timeline [value]="getAPIRevisionWithComments()">
|
||||
<ng-template pTemplate="marker" let-apiRevision>
|
||||
<i class="bi bi-clock-history"></i>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="content" let-apiRevision>
|
||||
<div class="pb-3 conversation-group-revision-id" [attr.data-conversation-group-revision-id]="apiRevision.id">
|
||||
<span>
|
||||
<i class="{{ getAPIRevisionTypeClass(apiRevision) }} me-2"></i>
|
||||
<span class="me-2" *ngIf="apiRevision.pullRequestNo"> {{ apiRevision.pullRequestNo }}</span>
|
||||
<span class="emphasis-badge info me-2">version: {{ apiRevision.packageVersion }}</span>
|
||||
<i class="fas fa-check-circle text-success me-2" *ngIf="apiRevision.isApproved"></i>
|
||||
<span *ngIf="apiRevision.apiRevisionType !== 'Automatic'" class="me-2">{{ apiRevision.createdBy }}</span>
|
||||
<span *ngIf="apiRevision.isReleased" class="emphasis-badge success me-2">released: {{ apiRevision.releasedOn | timeago }}</span>
|
||||
<span class="emphasis-badge secondary me-2">created: {{ apiRevision.createdOn | timeago }}</span>
|
||||
<span class="emphasis-badge secondary me-2">last updated: {{ apiRevision | lastUpdatedOn | timeago }}</span>
|
||||
<span class="me-2" *ngIf="apiRevision.label">{{ apiRevision.label }}</span>
|
||||
</span>
|
||||
<div *ngFor="let commentThread of commentThreads.get(apiRevision.id); let isLast = last" class="my-2 conversation-group-threads">
|
||||
<a class="small conversation-group-element-id" (click)="navigateToCommentThreadOnRevisionPage($event)">{{ commentThread!.comments[0].elementId }}</a>
|
||||
<app-comment-thread [codePanelRowData]="commentThread" [instanceLocation]='"conversations"'
|
||||
(saveCommentActionEmitter)="handleSaveCommentActionEmitter($event)"
|
||||
(deleteCommentActionEmitter)="handleDeleteCommentActionEmitter($event)"
|
||||
(commentUpvoteActionEmitter)="handleCommentUpvoteActionEmitter($event)"
|
||||
(commentResolutionActionEmitter)="handleCommentResolutionActionEmitter($event)"
|
||||
></app-comment-thread>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</p-timeline>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<ConversationsComponent>;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<string> = new EventEmitter<string>();
|
||||
@Output() numberOfActiveThreadsEmitter : EventEmitter<number> = new EventEmitter<number>();
|
||||
|
||||
commentThreads: Map<string, CodePanelRowData[]> = new Map<string, CodePanelRowData[]>();
|
||||
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<string, CodePanelRowData[]>();
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -9,7 +9,14 @@
|
|||
(pageOptionsEmitter)="handlePageOptionsEmitter($event)"></app-review-info>
|
||||
<div class="mt-2" style="display: flex;">
|
||||
<div>
|
||||
<p-menu [model]="sideMenu"></p-menu>
|
||||
<p-menu [model]="sideMenu" class="side-menu">
|
||||
<ng-template pTemplate="item" let-item>
|
||||
<a pRipple class="flex align-items-center p-menuitem-link">
|
||||
<span [class]="item.icon"></span>
|
||||
<p-badge *ngIf="item.badge" class="ml-auto" [value]="item.badge" severity="danger" />
|
||||
</a>
|
||||
</ng-template>
|
||||
</p-menu>
|
||||
</div>
|
||||
<div style="flex-grow: 1; min-width: 0; margin-left: 7px;">
|
||||
<p-splitter
|
||||
|
@ -34,7 +41,7 @@
|
|||
[loadFailed]="loadFailed"
|
||||
[showLineNumbers]="showLineNumbers" [scrollToNodeIdHashed]="scrollToNodeIdHashed"
|
||||
[scrollToNodeId]="scrollToNodeId"
|
||||
(hasActiveConversation)="handleHasActiveConversationEmitter($event)"></app-code-panel>
|
||||
(hasActiveConversationEmitter)="handleHasActiveConversationEmitter($event)"></app-code-panel>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template pTemplate>
|
||||
|
@ -68,9 +75,18 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p-sidebar [(visible)]="revisionSideBarVisible" position="right" [modal]="true" styleClass="revisions-sidebar">
|
||||
<p-sidebar [(visible)]="revisionSidePanel!" position="right" [modal]="true" styleClass="revisions-sidebar">
|
||||
<app-revisions-list
|
||||
[review]="review"
|
||||
[revisionSideBarVisible]="revisionSideBarVisible"></app-revisions-list>
|
||||
[revisionSidePanel]="revisionSidePanel!"></app-revisions-list>
|
||||
</p-sidebar>
|
||||
<p-sidebar [(visible)]="conversationSidePanel!" position="right" [modal]="true" styleClass="conversation-sidebar">
|
||||
<app-conversations
|
||||
[apiRevisions]="apiRevisions"
|
||||
[comments]="comments"
|
||||
[review]="review"
|
||||
[activeApiRevisionId]="activeApiRevisionId"
|
||||
(scrollToNodeEmitter)="handleScrollToNodeEmitter($event)"
|
||||
(numberOfActiveThreadsEmitter)="handleNumberOfActiveThreadsEmitter($event)"></app-conversations >
|
||||
</p-sidebar>
|
||||
<app-footer></app-footer>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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({
|
||||
|
@ -413,6 +435,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') {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div *ngIf="codePanelRowData!.isResolvedCommentThread" class="resolution-info">
|
||||
<small class="d-flex align-items-center">This thread is marked resolved by<b> {{ threadResolvedBy }} </b><span class="resolved-toggler" (click)="toggleResolvedCommentExpandState()"><i class="bi {{threadResolvedStateToggleIcon}}"></i> {{threadResolvedStateToggleText}} Resolved</span></small>
|
||||
</div>
|
||||
<div *ngIf="!codePanelRowData!.isResolvedCommentThread || threadResolvedAndExpanded" class="border rounded {{spacingBasedOnResolvedState}} py-2">
|
||||
<div *ngIf="!codePanelRowData!.isResolvedCommentThread || threadResolvedAndExpanded" class="border rounded {{spacingBasedOnResolvedState}} py-2 comment-thread-container">
|
||||
<p-timeline [value]="codePanelRowData!.comments">
|
||||
<ng-template pTemplate="marker" let-comment>
|
||||
<img [alt]="comment.createdBy" src="https://github.com/{{ comment.createdBy }}.png?size=40" width="40" height="40" class="user-avartar"/>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -3,8 +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;
|
||||
|
@ -28,4 +29,35 @@ 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',
|
||||
}]
|
||||
} as CommentItemModel;
|
||||
const comment2 = {
|
||||
id: '2',
|
||||
isResolved: true,
|
||||
changeHistory: [ {
|
||||
changeAction: 'resolved',
|
||||
changedBy: 'test user 1',
|
||||
},
|
||||
{
|
||||
changeAction: 'resolved',
|
||||
changedBy: 'test user 2',
|
||||
}]
|
||||
} as CommentItemModel;
|
||||
|
||||
component.codePanelRowData!.comments = [comment1, comment2];
|
||||
component.codePanelRowData!.isResolvedCommentThread = true;
|
||||
fixture.detectChanges();
|
||||
component.setCommentResolutionState();
|
||||
expect(component.threadResolvedBy).toBe('test user 2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,6 +18,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<any> = new EventEmitter<any>();
|
||||
@Output() saveCommentActionEmitter : EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() deleteCommentActionEmitter : EventEmitter<any> = new EventEmitter<any>();
|
||||
|
@ -109,13 +110,19 @@ 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.spacingBasedOnResolvedState = 'mb-2';
|
||||
this.threadResolvedBy = this.codePanelRowData?.commentThreadIsResolvedBy;
|
||||
if (!this.threadResolvedBy) {
|
||||
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.reverse().find(ch => ch.changeAction === 'resolved')?.changedBy;
|
||||
}
|
||||
}
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
@ -171,14 +178,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})`);
|
||||
|
@ -226,17 +235,25 @@ 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");
|
||||
const content = replyEditor?.getEditorContent();
|
||||
this.saveCommentActionEmitter.emit(
|
||||
{
|
||||
nodeId: this.codePanelRowData!.nodeId,
|
||||
nodeId: (this.instanceLocation === "conversations") ? elementIdForConversationGroup : this.codePanelRowData!.nodeId,
|
||||
nodeIdHashed: this.codePanelRowData!.nodeIdHashed,
|
||||
commentText: content,
|
||||
allowAnyOneToResolve: this.allowAnyOneToResolve,
|
||||
associatedRowPositionInGroup: this.codePanelRowData!.associatedRowPositionInGroup
|
||||
associatedRowPositionInGroup: this.codePanelRowData!.associatedRowPositionInGroup,
|
||||
revisionIdForConversationGroup: revisionIdForConversationGroup
|
||||
}
|
||||
);
|
||||
this.codePanelRowData!.showReplyTextBox = false;
|
||||
|
@ -247,11 +264,12 @@ export class CommentThreadComponent {
|
|||
const content = replyEditor?.getEditorContent();
|
||||
this.saveCommentActionEmitter.emit(
|
||||
{
|
||||
nodeId: this.codePanelRowData!.nodeId,
|
||||
nodeId: (this.instanceLocation === "conversations") ? elementIdForConversationGroup : this.codePanelRowData!.nodeId,
|
||||
nodeIdHashed: 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;
|
||||
|
|
|
@ -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>): 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;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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 = '';
|
||||
|
|
|
@ -14,14 +14,15 @@ 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';
|
||||
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 { ConversationsComponent } from 'src/app/_components/conversations/conversations.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: ReviewPageComponent }
|
||||
|
@ -34,11 +35,12 @@ const routes: Routes = [
|
|||
ReviewInfoComponent,
|
||||
CodePanelComponent,
|
||||
CommentThreadComponent,
|
||||
ConversationsComponent,
|
||||
PageOptionsSectionComponent,
|
||||
ReviewPageOptionsComponent,
|
||||
ApiRevisionOptionsComponent,
|
||||
MarkdownToHtmlPipe,
|
||||
EditorComponent
|
||||
EditorComponent,
|
||||
],
|
||||
imports: [
|
||||
SharedAppModule,
|
||||
|
@ -52,6 +54,7 @@ const routes: Routes = [
|
|||
ButtonModule,
|
||||
InputSwitchModule,
|
||||
UiScrollModule,
|
||||
DividerModule,
|
||||
RouterModule.forChild(routes),
|
||||
]
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче