mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-10128] [BEEEP] Add Right-Click Menu Options to Vault (#10954)
* Added right click functionality on cipher row * Updated menu directive to position menu option on mouse event location * Updated menu directive to reopen menu option on new mouse event location and close previously opened menu-option * removed preventdefault call * Added new events for favorite and edit cipher * Added new menu options favorite, edit cipher Added new copy options for the other cipher types Simplified the copy by using the copy cipher field directive * Listen to new events * Refactored parameter to be MouseEvent * Added locales * Remove the backdrop from `MenuTriggerForDirective` * Handle the Angular overlay's outside pointer events * Cleaned up cipher row component as copy functions and disable menu functions would not be needed anymore * Fixed bug with right clicking on a row * Add right click to collections * Disable backdrop on right click * Fixed bug where dvivided didn't show for secure notes * Added comments to enable to disable context menu * Removed conditionals * Removed preferences setting to enable to disable setting * Removed setting from right click listener * improve context menu positioning to prevent viewport clipping * Keep icon consisten when favorite or not * fixed prettier issues * removed duplicate translation keys * Fix favorite status not persisting by toggling in handleFavoriteEvent * Addressed claude comments * Added comment to variable --------- Co-authored-by: Addison Beck <github@addisonbeck.com>
This commit is contained in:
@@ -93,7 +93,7 @@
|
||||
</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
@if (!decryptionFailure && !hideMenu) {
|
||||
@if (!decryptionFailure) {
|
||||
<button
|
||||
[bitMenuTriggerFor]="cipherOptions"
|
||||
[disabled]="disabled"
|
||||
@@ -104,16 +104,16 @@
|
||||
label="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #cipherOptions>
|
||||
<ng-container *ngIf="isActiveLoginCipher">
|
||||
<button bitMenuItem type="button" (click)="copy('username')" *ngIf="hasUsernameToCopy">
|
||||
<ng-container *ngIf="isLoginCipher">
|
||||
<button type="button" bitMenuItem appCopyField="username" [cipher]="cipher">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyUsername" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem type="button" (click)="copy('password')" *ngIf="hasPasswordToCopy">
|
||||
<button bitMenuItem type="button" appCopyField="password" [cipher]="cipher">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyPassword" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem type="button" (click)="copy('totp')" *ngIf="showTotpCopyButton">
|
||||
<button bitMenuItem type="button" appCopyField="totp" [cipher]="cipher">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyVerificationCode" | i18n }}
|
||||
</button>
|
||||
@@ -130,6 +130,53 @@
|
||||
</a>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="isCardCipher">
|
||||
<button type="button" bitMenuItem appCopyField="cardNumber" [cipher]="cipher">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyNumber" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem appCopyField="securityCode" [cipher]="cipher">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copySecurityCode" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="isIdentityCipher">
|
||||
<button type="button" bitMenuItem appCopyField="username" [cipher]="cipher">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyUsername" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem appCopyField="email" [cipher]="cipher">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyEmail" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem appCopyField="phone" [cipher]="cipher">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyPhone" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem appCopyField="address" [cipher]="cipher">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyAddress" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="isSecureNoteCipher">
|
||||
<button type="button" bitMenuItem appCopyField="secureNote" [cipher]="cipher">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyNote" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<bit-menu-divider *ngIf="showMenuDivider"></bit-menu-divider>
|
||||
|
||||
<button bitMenuItem type="button" (click)="toggleFavorite()">
|
||||
<i class="bwi bwi-fw bwi-star" aria-hidden="true"></i>
|
||||
{{ (cipher.favorite ? "unfavorite" : "favorite") | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem type="button" (click)="editCipher()" *ngIf="canEditCipher">
|
||||
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
||||
{{ "edit" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem *ngIf="showAttachments" type="button" (click)="attachments()">
|
||||
<i class="bwi bwi-fw bwi-paperclip" aria-hidden="true"></i>
|
||||
{{ "attachments" | i18n }}
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from "@angular/core";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { MenuTriggerForDirective } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
convertToPermission,
|
||||
@@ -26,6 +36,8 @@ import { RowHeightClass } from "./vault-items.component";
|
||||
export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit {
|
||||
protected RowHeightClass = RowHeightClass;
|
||||
|
||||
@ViewChild(MenuTriggerForDirective, { static: false }) menuTrigger: MenuTriggerForDirective;
|
||||
|
||||
@Input() disabled: boolean;
|
||||
@Input() cipher: C;
|
||||
@Input() showOwner: boolean;
|
||||
@@ -73,7 +85,10 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
|
||||
];
|
||||
protected organization?: Organization;
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private vaultSettingsService: VaultSettingsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Lifecycle hook for component initialization.
|
||||
@@ -180,7 +195,7 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
|
||||
return this.useEvents && this.cipher.organizationId;
|
||||
}
|
||||
|
||||
protected get isActiveLoginCipher() {
|
||||
protected get isLoginCipher() {
|
||||
return (
|
||||
CipherViewLikeUtils.getType(this.cipher) === this.CipherType.Login &&
|
||||
!CipherViewLikeUtils.isDeleted(this.cipher) &&
|
||||
@@ -230,43 +245,63 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
|
||||
return this.i18nService.t("noAccess");
|
||||
}
|
||||
|
||||
protected get showCopyUsername(): boolean {
|
||||
const usernameCopy = CipherViewLikeUtils.hasCopyableValue(this.cipher, "username");
|
||||
return this.isActiveLoginCipher && usernameCopy;
|
||||
}
|
||||
|
||||
protected get showCopyPassword(): boolean {
|
||||
const passwordCopy = CipherViewLikeUtils.hasCopyableValue(this.cipher, "password");
|
||||
return this.isActiveLoginCipher && this.cipher.viewPassword && passwordCopy;
|
||||
}
|
||||
|
||||
protected get showCopyTotp(): boolean {
|
||||
return this.isActiveLoginCipher && this.showTotpCopyButton;
|
||||
}
|
||||
|
||||
protected get showLaunchUri(): boolean {
|
||||
return this.isActiveLoginCipher && this.canLaunch;
|
||||
}
|
||||
|
||||
protected get isDeletedCanRestore(): boolean {
|
||||
return CipherViewLikeUtils.isDeleted(this.cipher) && this.canRestoreCipher;
|
||||
}
|
||||
|
||||
protected get hideMenu() {
|
||||
return !(
|
||||
this.isDeletedCanRestore ||
|
||||
this.showCopyUsername ||
|
||||
this.showCopyPassword ||
|
||||
this.showCopyTotp ||
|
||||
this.showLaunchUri ||
|
||||
this.showAttachments ||
|
||||
this.showClone ||
|
||||
this.canEditCipher
|
||||
protected get hasVisibleLoginOptions() {
|
||||
return (
|
||||
this.isLoginCipher &&
|
||||
(CipherViewLikeUtils.hasCopyableValue(this.cipher, "username") ||
|
||||
(this.cipher.viewPassword &&
|
||||
CipherViewLikeUtils.hasCopyableValue(this.cipher, "password")) ||
|
||||
this.showTotpCopyButton ||
|
||||
this.canLaunch)
|
||||
);
|
||||
}
|
||||
|
||||
protected copy(field: "username" | "password" | "totp") {
|
||||
this.onEvent.emit({ type: "copyField", item: this.cipher, field });
|
||||
protected get isCardCipher(): boolean {
|
||||
return CipherViewLikeUtils.getType(this.cipher) === this.CipherType.Card && !this.isDeleted;
|
||||
}
|
||||
|
||||
protected get hasVisibleCardOptions(): boolean {
|
||||
return (
|
||||
this.isCardCipher &&
|
||||
(CipherViewLikeUtils.hasCopyableValue(this.cipher, "cardNumber") ||
|
||||
CipherViewLikeUtils.hasCopyableValue(this.cipher, "securityCode"))
|
||||
);
|
||||
}
|
||||
|
||||
protected get isIdentityCipher() {
|
||||
return CipherViewLikeUtils.getType(this.cipher) === this.CipherType.Identity && !this.isDeleted;
|
||||
}
|
||||
|
||||
protected get hasVisibleIdentityOptions(): boolean {
|
||||
return (
|
||||
this.isIdentityCipher &&
|
||||
(CipherViewLikeUtils.hasCopyableValue(this.cipher, "address") ||
|
||||
CipherViewLikeUtils.hasCopyableValue(this.cipher, "email") ||
|
||||
CipherViewLikeUtils.hasCopyableValue(this.cipher, "username") ||
|
||||
CipherViewLikeUtils.hasCopyableValue(this.cipher, "phone"))
|
||||
);
|
||||
}
|
||||
|
||||
protected get isSecureNoteCipher() {
|
||||
return (
|
||||
CipherViewLikeUtils.getType(this.cipher) === this.CipherType.SecureNote &&
|
||||
!(this.isDeleted && this.canRestoreCipher)
|
||||
);
|
||||
}
|
||||
|
||||
protected get hasVisibleSecureNoteOptions(): boolean {
|
||||
return (
|
||||
this.isSecureNoteCipher && CipherViewLikeUtils.hasCopyableValue(this.cipher, "secureNote")
|
||||
);
|
||||
}
|
||||
|
||||
protected get showMenuDivider() {
|
||||
return (
|
||||
this.hasVisibleLoginOptions ||
|
||||
this.hasVisibleCardOptions ||
|
||||
this.hasVisibleIdentityOptions ||
|
||||
this.hasVisibleSecureNoteOptions
|
||||
);
|
||||
}
|
||||
|
||||
protected clone() {
|
||||
@@ -308,4 +343,26 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
|
||||
|
||||
return this.organization.canEditAllCiphers || (this.cipher.edit && this.cipher.viewPassword);
|
||||
}
|
||||
|
||||
protected toggleFavorite() {
|
||||
this.onEvent.emit({
|
||||
type: "toggleFavorite",
|
||||
item: this.cipher,
|
||||
});
|
||||
}
|
||||
|
||||
protected editCipher() {
|
||||
this.onEvent.emit({ type: "editCipher", item: this.cipher });
|
||||
}
|
||||
|
||||
@HostListener("contextmenu", ["$event"])
|
||||
protected onRightClick(event: MouseEvent) {
|
||||
if (event.shiftKey && event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.disabled && this.menuTrigger) {
|
||||
this.menuTrigger.toggleMenuOnRightClick(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { Component, EventEmitter, HostListener, Input, Output, ViewChild } from "@angular/core";
|
||||
|
||||
import {
|
||||
CollectionAdminView,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { MenuTriggerForDirective } from "@bitwarden/components";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
|
||||
@@ -33,6 +34,8 @@ export class VaultCollectionRowComponent<C extends CipherViewLike> {
|
||||
protected CollectionPermission = CollectionPermission;
|
||||
protected DefaultCollectionType = CollectionTypes.DefaultUserCollection;
|
||||
|
||||
@ViewChild(MenuTriggerForDirective, { static: false }) menuTrigger: MenuTriggerForDirective;
|
||||
|
||||
@Input() disabled: boolean;
|
||||
@Input() collection: CollectionView;
|
||||
@Input() showOwner: boolean;
|
||||
@@ -122,4 +125,15 @@ export class VaultCollectionRowComponent<C extends CipherViewLike> {
|
||||
protected deleteCollection() {
|
||||
this.onEvent.next({ type: "delete", items: [{ collection: this.collection }] });
|
||||
}
|
||||
|
||||
@HostListener("contextmenu", ["$event"])
|
||||
protected onRightClick(event: MouseEvent) {
|
||||
if (event.shiftKey && event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.disabled && this.menuTrigger) {
|
||||
this.menuTrigger.toggleMenuOnRightClick(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,4 +22,6 @@ export type VaultItemEvent<C extends CipherViewLike> =
|
||||
| { type: "moveToFolder"; items: C[] }
|
||||
| { type: "assignToCollections"; items: C[] }
|
||||
| { type: "archive"; items: C[] }
|
||||
| { type: "unarchive"; items: C[] };
|
||||
| { type: "unarchive"; items: C[] }
|
||||
| { type: "toggleFavorite"; item: C }
|
||||
| { type: "editCipher"; item: C };
|
||||
|
||||
@@ -4,6 +4,7 @@ import { NgModule } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
|
||||
import { ScrollLayoutDirective, TableModule } from "@bitwarden/components";
|
||||
import { CopyCipherFieldDirective } from "@bitwarden/vault";
|
||||
|
||||
import { CollectionNameBadgeComponent } from "../../../admin-console/organizations/collections";
|
||||
import { GroupBadgeModule } from "../../../admin-console/organizations/collections/group-badge/group-badge.module";
|
||||
@@ -26,6 +27,7 @@ import { VaultItemsComponent } from "./vault-items.component";
|
||||
CollectionNameBadgeComponent,
|
||||
GroupBadgeModule,
|
||||
PipesModule,
|
||||
CopyCipherFieldDirective,
|
||||
ScrollLayoutDirective,
|
||||
],
|
||||
declarations: [VaultItemsComponent, VaultCipherRowComponent, VaultCollectionRowComponent],
|
||||
|
||||
@@ -679,6 +679,12 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
await this.bulkUnarchive(event.items);
|
||||
}
|
||||
break;
|
||||
case "toggleFavorite":
|
||||
await this.handleFavoriteEvent(event.item);
|
||||
break;
|
||||
case "editCipher":
|
||||
await this.editCipher(event.item);
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
this.processingEvent = false;
|
||||
@@ -1452,6 +1458,27 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the favorite status of the cipher and updates it on the server.
|
||||
*/
|
||||
async handleFavoriteEvent(cipher: C) {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const cipherFullView = await this.cipherService.getFullCipherView(cipher);
|
||||
cipherFullView.favorite = !cipherFullView.favorite;
|
||||
const encryptedCipher = await this.cipherService.encrypt(cipherFullView, activeUserId);
|
||||
await this.cipherService.updateWithServer(encryptedCipher);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t(
|
||||
cipherFullView.favorite ? "itemAddedToFavorites" : "itemRemovedFromFavorites",
|
||||
),
|
||||
});
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
protected deleteCipherWithServer(id: string, userId: UserId, permanent: boolean) {
|
||||
return permanent
|
||||
? this.cipherService.deleteWithServer(id, userId)
|
||||
|
||||
@@ -11028,6 +11028,15 @@
|
||||
"domainClaimed": {
|
||||
"message": "Domain claimed"
|
||||
},
|
||||
"itemAddedToFavorites": {
|
||||
"message": "Item added to favorites"
|
||||
},
|
||||
"itemRemovedFromFavorites": {
|
||||
"message": "Item removed from favorites"
|
||||
},
|
||||
"copyNote": {
|
||||
"message": "Copy note"
|
||||
},
|
||||
"organizationNameMaxLength": {
|
||||
"message": "Organization name cannot exceed 50 characters."
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user