Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accesibility/linefeatures #9456

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,17 @@
[attr.data-row-position-in-group]="item.rowPositionInGroup" [attr.data-row-type]="item.type" [ngClass]="getRowClassObject(item)">
<ng-container *ngIf="item.type === CodePanelRowDatatype.CodeLine || item.type === CodePanelRowDatatype.Documentation">
<div class="line-actions">
<span *ngIf="showLineNumbers" class="line-number">{{index+1}}</span>
<span *ngIf="showLineNumbers" class="line-number">
<span class="text-decoration-none" (click)="toggleLineActionMenu($event, index)">{{index+1}}</span>
<p-menu appendTo="body" [attr.data-line-action-menu-id]="index" [model]="menuItemsLineActions" [popup]="true">
<ng-template pTemplate="item" let-item>
<span pRipple [attr.data-item-id]="index" class="flex align-items-center p-menuitem-link">
<i *ngIf="item?.icon" class="mx-2 {{item?.icon}}"></i>
{{ item.label }}
</span>
</ng-template>
</p-menu>
</span>
<span class="small toggle-user-comments-btn {{item.toggleCommentsClasses}}"></span>
<span class="small toggle-documentation-btn {{item.toggleDocumentationClasses}}"></span>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { ActivatedRoute, convertToParamMap } from '@angular/router';
import { SharedAppModule } from 'src/app/_modules/shared/shared-app.module';
import { ReviewPageModule } from 'src/app/_modules/review-page.module';
import { MessageService } from 'primeng/api';
import { StructuredToken } from 'src/app/_models/structuredToken';
import { CodePanelRowData } from 'src/app/_models/codePanelModels';

describe('CodePanelComponent', () => {
let component: CodePanelComponent;
Expand Down Expand Up @@ -41,4 +43,77 @@ describe('CodePanelComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});

describe('copyReviewTextToClipBoard', () => {
let clipboardSpy: jasmine.Spy;

beforeEach(() => {
clipboardSpy = spyOn(navigator.clipboard, 'writeText').and.callFake(() => Promise.resolve());
});

it('should copy formatted review text to clipboard', () => {
const token1 = new StructuredToken();
token1.value = 'token1';
const token2 = new StructuredToken();
token2.value = 'token2';
const token3 = new StructuredToken();
token3.value = 'token3';

const codePanelRowData1 = new CodePanelRowData();
codePanelRowData1.rowOfTokens = [token1, token2];
codePanelRowData1.indent = 2;
const codePanelRowData2 = new CodePanelRowData();
codePanelRowData2.rowOfTokens = [token3];

component.codePanelRowData = [codePanelRowData1, codePanelRowData2];

component.copyReviewTextToClipBoard();

expect(clipboardSpy).toHaveBeenCalledWith('\ttoken1token2\ntoken3');
});

it('should handle empty codePanelRowData', () => {
component.codePanelRowData = [];

component.copyReviewTextToClipBoard();

expect(clipboardSpy).toHaveBeenCalledWith('');
});

it('should handle rows without rowOfTokens', () => {
const token1 = new StructuredToken();
token1.value = 'token1';
const codePanelRowData1 = new CodePanelRowData();
const codePanelRowData2 = new CodePanelRowData();
codePanelRowData2.indent = 1;
codePanelRowData2.rowOfTokens = [token1];
component.codePanelRowData = [codePanelRowData1, codePanelRowData2];

component.copyReviewTextToClipBoard();

expect(clipboardSpy).toHaveBeenCalledWith('token1');
});

it('should handle rows with indentation correctly', () => {
const token1 = new StructuredToken();
token1.value = 'token1';
const token2 = new StructuredToken();
token2.value = 'token2';

const codePanelRowData1 = new CodePanelRowData();
codePanelRowData1.rowOfTokens = [token1];
codePanelRowData1.indent = 2;
const codePanelRowData2 = new CodePanelRowData();
codePanelRowData2.rowOfTokens = [token2];
codePanelRowData2.indent = 1;


component.codePanelRowData = [codePanelRowData1, codePanelRowData2];

component.copyReviewTextToClipBoard();

expect(clipboardSpy).toHaveBeenCalledWith('\ttoken1\ntoken2');
});
});

});
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core';
import { take, takeUntil } from 'rxjs/operators';
import { Datasource, IDatasource, SizeStrategy } from 'ngx-ui-scroll';
import { CommentsService } from 'src/app/_services/comments/comments.service';
import { getQueryParams } from 'src/app/_helpers/router-helpers';
import { ActivatedRoute, Router } from '@angular/router';
import { CodeLineRowNavigationDirection, isDiffRow } from 'src/app/_helpers/common-helpers';
import { CodeLineRowNavigationDirection, convertRowOfTokensToString, isDiffRow } from 'src/app/_helpers/common-helpers';
import { SCROLL_TO_NODE_QUERY_PARAM } from 'src/app/_helpers/router-helpers';
import { CodePanelData, CodePanelRowData, CodePanelRowDatatype } from 'src/app/_models/codePanelModels';
import { StructuredToken } from 'src/app/_models/structuredToken';
import { CommentItemModel, CommentType } from 'src/app/_models/commentItemModel';
import { UserProfile } from 'src/app/_models/userProfile';
import { Message } from 'primeng/api/message';
import { MessageService } from 'primeng/api';
import { MenuItem, MenuItemCommandEvent, MessageService } from 'primeng/api';
import { SignalRService } from 'src/app/_services/signal-r/signal-r.service';
import { Subject } from 'rxjs';
import { CommentThreadUpdateAction, CommentUpdatesDto } from 'src/app/_dtos/commentThreadUpdateDto';
import { Menu } from 'primeng/menu';

@Component({
selector: 'app-code-panel',
Expand All @@ -36,7 +37,7 @@ export class CodePanelComponent implements OnChanges{
@Input() loadFailed : boolean = false;

@Output() hasActiveConversationEmitter : EventEmitter<boolean> = new EventEmitter<boolean>();

@ViewChildren(Menu) menus!: QueryList<Menu>;

noDiffInContentMessage : Message[] = [{ severity: 'info', icon:'bi bi-info-circle', detail: 'There is no difference between the two API revisions.' }];

Expand All @@ -52,12 +53,19 @@ export class CodePanelComponent implements OnChanges{
commentThreadNavaigationPointer: number | undefined = undefined;
diffNodeNavaigationPointer: number | undefined = undefined;

menuItemsLineActions: MenuItem[] = [];

constructor(private changeDetectorRef: ChangeDetectorRef, private commentsService: CommentsService,
private signalRService: SignalRService, private route: ActivatedRoute, private router: Router, private messageService: MessageService) { }

ngOnInit() {
this.codeWindowHeight = `${window.innerHeight - 80}`;
this.handleRealTimeCommentUpdates();

this.menuItemsLineActions = [
{ label: 'Copy line', icon: 'bi bi-clipboard', command: (event) => this.copyCodeLineToClipBoard(event) },
{ label: 'Copy permalink', icon: 'bi bi-clipboard', command: (event) => this.copyCodeLinePermaLinkToClipBoard(event) }
];
}

ngOnChanges(changes: SimpleChanges) {
Expand Down Expand Up @@ -158,6 +166,13 @@ export class CodePanelComponent implements OnChanges{
return "";
}

toggleLineActionMenu(event: any, id: string) {
const menu: Menu | undefined = this.menus.find(menu => menu.el.nativeElement.getAttribute('data-line-action-menu-id') == id);
if (menu) {
menu.toggle(event);
}
}

toggleNodeComments(target: Element) {
const codeLine = target.closest('.code-line')!;
const nodeIdHashed = codeLine.getAttribute('data-node-id');
Expand Down Expand Up @@ -619,6 +634,50 @@ export class CodePanelComponent implements OnChanges{
}
}

copyReviewTextToClipBoard() {
const reviewText : string [] = [];
chidozieononiwu marked this conversation as resolved.
Show resolved Hide resolved
this.codePanelRowData.forEach((row) => {
if (row.rowOfTokens && row.rowOfTokens.length > 0) {
let codeLineText = convertRowOfTokensToString(row.rowOfTokens);
if (row.indent && row.indent > 0) {
codeLineText = '\t'.repeat(row.indent - 1) + codeLineText;
}
reviewText.push(codeLineText);
}
});
navigator.clipboard.writeText(reviewText.join('\n'));
}

showNoDiffInContentMessage() {
return this.codePanelData && !this.isLoading && this.isDiffView && !this.codePanelData?.hasDiff
}

private getCodeLineIndex(event: MenuItemCommandEvent) {
const target = (event.originalEvent?.target as Element).closest("span") as Element;
return target.getAttribute('data-item-id');
}

private copyCodeLinePermaLinkToClipBoard(event: MenuItemCommandEvent) {
const codeLineIndex = this.getCodeLineIndex(event);
const codeLine = this.codePanelRowData[parseInt(codeLineIndex!, 10)];
const queryParams = { ...this.route.snapshot.queryParams };
queryParams[SCROLL_TO_NODE_QUERY_PARAM] = codeLine.nodeId;
const updatedUrl = this.router.createUrlTree([], {
relativeTo: this.route,
queryParams: queryParams,
queryParamsHandling: 'merge'
}).toString();
const fullExternalUrl = window.location.origin + updatedUrl;
navigator.clipboard.writeText(fullExternalUrl);
}

private copyCodeLineToClipBoard(event: MenuItemCommandEvent) {
const codeLineIndex = this.getCodeLineIndex(event);
const codeLine = this.codePanelRowData[parseInt(codeLineIndex!, 10)];
const codeLineText = convertRowOfTokensToString(codeLine.rowOfTokens);
navigator.clipboard.writeText(codeLineText);
}

private findNextCommentThread (index: number) : CodePanelRowData | undefined {
while (index < this.codePanelRowData.length) {
if (this.codePanelRowData[index].type === CodePanelRowDatatype.CommentThread && !this.codePanelRowData![index].isResolvedCommentThread) {
Expand Down Expand Up @@ -670,10 +729,6 @@ export class CodePanelComponent implements OnChanges{
}
return undefined;
}

showNoDiffInContentMessage() {
return this.codePanelData && !this.isLoading && this.isDiffView && !this.codePanelData?.hasDiff
}

private updateHasActiveConversations() {
let hasActiveConversation = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
</ul>
</app-page-options-section>

<app-page-options-section *ngIf="showCommentsSwitch || (isDiffView && contentHasDiff)" sectionName="Find on Page">
<app-page-options-section *ngIf="showCommentsSwitch || (isDiffView && contentHasDiff)" sectionName="Page Utils">
<ul class="list-group">
<li class="list-group-item" *ngIf="showCommentsSwitch">
<label class="small mx-1 fw-semibold">Comment:</label>
Expand All @@ -86,11 +86,19 @@
<div class="btn-group btn-group-sm" role="group" aria-label="Diff Navigation">
<button type="button" class="btn btn-outline-secondary" pTooltip="Previous Diff" tooltipPosition="top" (click)="diffNavaigationEmitter.emit(CodeLineRowNavigationDirection.prev)">
<i class="bi bi-arrow-up"></i>Prev</button>
<button type="button" class="btn btn-outline-secondary" pTooltip="Next Diff" tooltipPosition="top" (click)="diffNavaigationEmitter.emit(CodeLineRowNavigationDirection.next)">
<button type="button" class="btn btn-lg btn-outline-secondary" pTooltip="Next Diff" tooltipPosition="top" (click)="diffNavaigationEmitter.emit(CodeLineRowNavigationDirection.next)">
<i class="bi bi-arrow-down"></i>Next</button>
</div>
</div>
</li>
<li class="list-group-item" *ngIf="!isDiffView">
<div class="d-grid gap-2">
<button type="button" class="btn btn-outline-primary" tooltipPosition="top" (click)="copyReviewText($event)">
<i class="bi bi-clipboard"></i>
{{ copyReviewTextButtonText }}
</button>
</div>
</li>
</ul>
</app-page-options-section>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { take } from 'rxjs';
import { UserProfile } from 'src/app/_models/userProfile';
import { PullRequestsService } from 'src/app/_services/pull-requests/pull-requests.service';
import { PullRequestModel } from 'src/app/_models/pullRequestModel';
import { MenuItemCommandEvent } from 'primeng/api';

@Component({
selector: 'app-review-page-options',
Expand Down Expand Up @@ -45,7 +46,7 @@ export class ReviewPageOptionsComponent implements OnInit, OnChanges{
@Output() reviewApprovalEmitter : EventEmitter<boolean> = new EventEmitter<boolean>();
@Output() commentThreadNavaigationEmitter : EventEmitter<CodeLineRowNavigationDirection> = new EventEmitter<CodeLineRowNavigationDirection>();
@Output() diffNavaigationEmitter : EventEmitter<CodeLineRowNavigationDirection> = new EventEmitter<CodeLineRowNavigationDirection>();

@Output() copyReviewTextEmitter : EventEmitter<boolean> = new EventEmitter<boolean>();

webAppUrl : string = this.configService.webAppUrl

Expand All @@ -72,6 +73,7 @@ export class ReviewPageOptionsComponent implements OnInit, OnChanges{
canApproveReview: boolean | undefined = undefined;
reviewIsApproved: boolean | undefined = undefined;
reviewApprover: string = 'azure-sdk';
copyReviewTextButtonText : string = 'Copy review text';

associatedPullRequests : PullRequestModel[] = [];
pullRequestsOfAssociatedAPIRevisions : PullRequestModel[] = [];
Expand Down Expand Up @@ -318,6 +320,22 @@ export class ReviewPageOptionsComponent implements OnInit, OnChanges{
this.markedAsViewSwitch = (this.activeAPIRevision && this.userProfile)? this.activeAPIRevision!.viewedBy.includes(this.userProfile?.userName!): this.markedAsViewSwitch;
}

copyReviewText(event: Event) {
const icon = (event?.target as Element).firstChild as HTMLElement;

icon.classList.remove('bi-clipboard');
icon.classList.add('bi-clipboard-check');
this.copyReviewTextButtonText = 'Review text copied!';

setTimeout(() => {
this.copyReviewTextButtonText = 'Copy review text';
icon.classList.remove('bi-clipboard-check');
icon.classList.add('bi-clipboard');
}, 1500);

this.copyReviewTextEmitter.emit(true);
}

handleAPIRevisionApprovalAction() {
if (!this.activeAPIRevisionIsApprovedByCurrentUser && (this.hasActiveConversation || this.hasFatalDiagnostics)) {
this.showAPIRevisionApprovalModal = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
(showHiddenAPIEmitter)="handleShowHiddenAPIEmitter($event)"
(disableCodeLinesLazyLoadingEmitter)="handleDisableCodeLinesLazyLoadingEmitter($event)"
(commentThreadNavaigationEmitter)="handleCommentThreadNavaigationEmitter($event)"
(diffNavaigationEmitter)="handleDiffNavaigationEmitter($event)"></app-review-page-options>
(diffNavaigationEmitter)="handleDiffNavaigationEmitter($event)"
(copyReviewTextEmitter)="handleCopyReviewTextEmitter($event)"></app-review-page-options>
</div>
</ng-template>
</p-splitter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,10 @@ export class ReviewPageComponent implements OnInit {
this.codePanelComponent.navigateToDiffNode(direction);
}

handleCopyReviewTextEmitter(event: boolean) {
this.codePanelComponent.copyReviewTextToClipBoard();
}

handleHasActiveConversationEmitter(value: boolean) {
this.hasActiveConversation = value;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CodePanelRowData, CodePanelRowDatatype } from "../_models/codePanelModels";
import { StructuredToken } from "../_models/structuredToken";

export const FULL_DIFF_STYLE = "full";
export const TREE_DIFF_STYLE = "trees";
Expand Down Expand Up @@ -56,4 +57,8 @@ export function getTypeClass(type: string): string {

export function isDiffRow(row: CodePanelRowData) {
return row.type === CodePanelRowDatatype.CodeLine && (row.diffKind === DIFF_REMOVED || row.diffKind === DIFF_ADDED);
}

export function convertRowOfTokensToString(rowOfTokens: StructuredToken[]): string {
return rowOfTokens.map(token => token.value).join('');
}
9 changes: 7 additions & 2 deletions src/dotnet/APIView/ClientSPA/src/ng-prime-overrides.scss
Original file line number Diff line number Diff line change
Expand Up @@ -303,9 +303,14 @@ p-contextmenusub {
}
}

.p-menu .p-menuitem > .p-menuitem-content .p-menuitem-link .p-menuitem-text, .p-menu .p-menuitem > .p-menuitem-content .p-menuitem-link .p-menuitem-icon {
text-decoration: none;
.p-menu .p-menuitem > .p-menuitem-content .p-menuitem-link {
color: var(--base-text-color);
padding: 0.2rem 0.25rem;

.p-menuitem-text, .p-menuitem-icon {
text-decoration: none;
color: var(--base-text-color);
}
}

.p-menu .p-menuitem:not(.p-highlight):not(.p-disabled) > .p-menuitem-content:hover {
Expand Down
Loading