1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-18 15:47:57 +01:00

[PM-16097] Separate copy buttons appearance setting (#12428)

---------

Co-authored-by: William Martin <contact@willmartian.com>
This commit is contained in:
Kyle Spearrin 2024-12-16 16:10:32 -05:00 committed by GitHub
parent 05783249b2
commit a4db5279b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 194 additions and 50 deletions

View File

@ -4679,6 +4679,9 @@
"showNumberOfAutofillSuggestions": { "showNumberOfAutofillSuggestions": {
"message": "Show number of login autofill suggestions on extension icon" "message": "Show number of login autofill suggestions on extension icon"
}, },
"showQuickCopyActions": {
"message": "Show quick copy actions on Vault"
},
"systemDefault": { "systemDefault": {
"message": "System default" "message": "System default"
}, },

View File

@ -1,53 +1,117 @@
<bit-item-action *ngIf="cipher.type === CipherType.Login"> <ng-container *ngIf="cipher.type === CipherType.Login">
<button <ng-container *ngIf="showQuickCopyActions$ | async; else loginCopyMenu">
type="button" <bit-item-action>
bitIconButton="bwi-clone" <button
size="small" type="button"
[appA11yTitle]=" bitIconButton="bwi-user"
hasLoginValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n) size="small"
" appCopyField="username"
[disabled]="!hasLoginValues" [cipher]="cipher"
[bitMenuTriggerFor]="loginOptions" [appA11yTitle]="'copyUsername' | i18n"
></button> ></button>
<bit-menu #loginOptions> </bit-item-action>
<button type="button" bitMenuItem appCopyField="username" [cipher]="cipher"> <bit-item-action>
{{ "copyUsername" | i18n }} <button
</button> *ngIf="cipher.viewPassword"
<button type="button"
*ngIf="cipher.viewPassword" bitIconButton="bwi-key"
type="button" size="small"
bitMenuItem appCopyField="password"
appCopyField="password" [cipher]="cipher"
[cipher]="cipher" [appA11yTitle]="'copyPassword' | i18n"
> ></button>
{{ "copyPassword" | i18n }} </bit-item-action>
</button> <bit-item-action>
<button type="button" bitMenuItem appCopyField="totp" [cipher]="cipher"> <button
{{ "copyVerificationCode" | i18n }} type="button"
</button> bitIconButton="bwi-clock"
</bit-menu> size="small"
</bit-item-action> appCopyField="totp"
[cipher]="cipher"
[appA11yTitle]="'copyVerificationCode' | i18n"
></button>
</bit-item-action>
</ng-container>
<bit-item-action *ngIf="cipher.type === CipherType.Card"> <ng-template #loginCopyMenu>
<button <bit-item-action>
type="button" <button
bitIconButton="bwi-clone" type="button"
size="small" bitIconButton="bwi-clone"
[appA11yTitle]=" size="small"
hasCardValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n) [appA11yTitle]="
" hasLoginValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
[disabled]="!hasCardValues" "
[bitMenuTriggerFor]="cardOptions" [disabled]="!hasLoginValues"
></button> [bitMenuTriggerFor]="loginOptions"
<bit-menu #cardOptions> ></button>
<button type="button" bitMenuItem appCopyField="cardNumber" [cipher]="cipher"> <bit-menu #loginOptions>
{{ "copyNumber" | i18n }} <button type="button" bitMenuItem appCopyField="username" [cipher]="cipher">
</button> {{ "copyUsername" | i18n }}
<button type="button" bitMenuItem appCopyField="securityCode" [cipher]="cipher"> </button>
{{ "copySecurityCode" | i18n }} <button
</button> *ngIf="cipher.viewPassword"
</bit-menu> type="button"
</bit-item-action> bitMenuItem
appCopyField="password"
[cipher]="cipher"
>
{{ "copyPassword" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="totp" [cipher]="cipher">
{{ "copyVerificationCode" | i18n }}
</button>
</bit-menu>
</bit-item-action>
</ng-template>
</ng-container>
<ng-container *ngIf="cipher.type === CipherType.Card">
<ng-container *ngIf="showQuickCopyActions$ | async; else cardCopyMenu">
<bit-item-action>
<button
type="button"
bitIconButton="bwi-hashtag"
size="small"
appCopyField="cardNumber"
[cipher]="cipher"
[appA11yTitle]="'copyNumber' | i18n"
></button>
</bit-item-action>
<bit-item-action>
<button
type="button"
bitIconButton="bwi-key"
size="small"
appCopyField="securityCode"
[cipher]="cipher"
[appA11yTitle]="'copySecurityCode' | i18n"
></button>
</bit-item-action>
</ng-container>
<ng-template #cardCopyMenu>
<bit-item-action>
<button
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
hasCardValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
"
[disabled]="!hasCardValues"
[bitMenuTriggerFor]="cardOptions"
></button>
<bit-menu #cardOptions>
<button type="button" bitMenuItem appCopyField="cardNumber" [cipher]="cipher">
{{ "copyNumber" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="securityCode" [cipher]="cipher">
{{ "copySecurityCode" | i18n }}
</button>
</bit-menu>
</bit-item-action>
</ng-template>
</ng-container>
<bit-item-action *ngIf="cipher.type === CipherType.Identity"> <bit-item-action *ngIf="cipher.type === CipherType.Identity">
<button <button

View File

@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core"; import { Component, Input, inject } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
@ -9,6 +9,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { IconButtonModule, ItemModule, MenuModule } from "@bitwarden/components"; import { IconButtonModule, ItemModule, MenuModule } from "@bitwarden/components";
import { CopyCipherFieldDirective } from "@bitwarden/vault"; import { CopyCipherFieldDirective } from "@bitwarden/vault";
import { VaultPopupCopyButtonsService } from "../../../services/vault-popup-copy-buttons.service";
@Component({ @Component({
standalone: true, standalone: true,
selector: "app-item-copy-actions", selector: "app-item-copy-actions",
@ -23,6 +25,8 @@ import { CopyCipherFieldDirective } from "@bitwarden/vault";
], ],
}) })
export class ItemCopyActionsComponent { export class ItemCopyActionsComponent {
protected showQuickCopyActions$ = inject(VaultPopupCopyButtonsService).showQuickCopyActions$;
@Input() cipher: CipherView; @Input() cipher: CipherView;
protected CipherType = CipherType; protected CipherType = CipherType;

View File

@ -0,0 +1,39 @@
import { inject, Injectable } from "@angular/core";
import { map, Observable } from "rxjs";
import {
GlobalStateProvider,
KeyDefinition,
VAULT_APPEARANCE,
} from "@bitwarden/common/platform/state";
export type CopyButtonDisplayMode = "combined" | "quick";
const COPY_BUTTON = new KeyDefinition<CopyButtonDisplayMode>(VAULT_APPEARANCE, "copyButtons", {
deserializer: (s) => s,
});
/**
* Settings service for vault copy button settings
**/
@Injectable({ providedIn: "root" })
export class VaultPopupCopyButtonsService {
private readonly DEFAULT_DISPLAY_MODE = "combined";
private state = inject(GlobalStateProvider).get(COPY_BUTTON);
displayMode$: Observable<CopyButtonDisplayMode> = this.state.state$.pipe(
map((state) => state ?? this.DEFAULT_DISPLAY_MODE),
);
async setDisplayMode(displayMode: CopyButtonDisplayMode) {
await this.state.update(() => displayMode);
}
showQuickCopyActions$: Observable<boolean> = this.displayMode$.pipe(
map((displayMode) => displayMode === "quick"),
);
async setShowQuickCopyActions(value: boolean) {
await this.setDisplayMode(value ? "quick" : "combined");
}
}

View File

@ -31,6 +31,11 @@
> >
</bit-form-control> </bit-form-control>
<bit-form-control>
<input bitCheckbox formControlName="showQuickCopyActions" type="checkbox" />
<bit-label>{{ "showQuickCopyActions" | i18n }}</bit-label>
</bit-form-control>
<bit-form-control> <bit-form-control>
<input bitCheckbox formControlName="enableBadgeCounter" type="checkbox" /> <input bitCheckbox formControlName="enableBadgeCounter" type="checkbox" />
<bit-label>{{ "showNumberOfAutofillSuggestions" | i18n }}</bit-label> <bit-label>{{ "showNumberOfAutofillSuggestions" | i18n }}</bit-label>

View File

@ -17,6 +17,7 @@ import { PopupCompactModeService } from "../../../platform/popup/layout/popup-co
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
import { PopupWidthService } from "../../../platform/popup/layout/popup-width.service"; import { PopupWidthService } from "../../../platform/popup/layout/popup-width.service";
import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-buttons.service";
import { AppearanceV2Component } from "./appearance-v2.component"; import { AppearanceV2Component } from "./appearance-v2.component";
@ -46,11 +47,13 @@ describe("AppearanceV2Component", () => {
const selectedTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Nord); const selectedTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Nord);
const enableRoutingAnimation$ = new BehaviorSubject<boolean>(true); const enableRoutingAnimation$ = new BehaviorSubject<boolean>(true);
const enableCompactMode$ = new BehaviorSubject<boolean>(false); const enableCompactMode$ = new BehaviorSubject<boolean>(false);
const showQuickCopyActions$ = new BehaviorSubject<boolean>(false);
const setSelectedTheme = jest.fn().mockResolvedValue(undefined); const setSelectedTheme = jest.fn().mockResolvedValue(undefined);
const setShowFavicons = jest.fn().mockResolvedValue(undefined); const setShowFavicons = jest.fn().mockResolvedValue(undefined);
const setEnableBadgeCounter = jest.fn().mockResolvedValue(undefined); const setEnableBadgeCounter = jest.fn().mockResolvedValue(undefined);
const setEnableRoutingAnimation = jest.fn().mockResolvedValue(undefined); const setEnableRoutingAnimation = jest.fn().mockResolvedValue(undefined);
const setEnableCompactMode = jest.fn().mockResolvedValue(undefined); const setEnableCompactMode = jest.fn().mockResolvedValue(undefined);
const setShowQuickCopyActions = jest.fn().mockResolvedValue(undefined);
const mockWidthService: Partial<PopupWidthService> = { const mockWidthService: Partial<PopupWidthService> = {
width$: new BehaviorSubject("default"), width$: new BehaviorSubject("default"),
@ -84,6 +87,13 @@ describe("AppearanceV2Component", () => {
provide: PopupCompactModeService, provide: PopupCompactModeService,
useValue: { enabled$: enableCompactMode$, setEnabled: setEnableCompactMode }, useValue: { enabled$: enableCompactMode$, setEnabled: setEnableCompactMode },
}, },
{
provide: VaultPopupCopyButtonsService,
useValue: {
showQuickCopyActions$,
setShowQuickCopyActions,
} as Partial<VaultPopupCopyButtonsService>,
},
{ {
provide: PopupWidthService, provide: PopupWidthService,
useValue: mockWidthService, useValue: mockWidthService,
@ -112,6 +122,7 @@ describe("AppearanceV2Component", () => {
enableBadgeCounter: true, enableBadgeCounter: true,
theme: ThemeType.Nord, theme: ThemeType.Nord,
enableCompactMode: false, enableCompactMode: false,
showQuickCopyActions: false,
width: "default", width: "default",
}); });
}); });

View File

@ -27,6 +27,7 @@ import {
PopupWidthOption, PopupWidthOption,
PopupWidthService, PopupWidthService,
} from "../../../platform/popup/layout/popup-width.service"; } from "../../../platform/popup/layout/popup-width.service";
import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-buttons.service";
@Component({ @Component({
standalone: true, standalone: true,
@ -47,6 +48,7 @@ import {
}) })
export class AppearanceV2Component implements OnInit { export class AppearanceV2Component implements OnInit {
private compactModeService = inject(PopupCompactModeService); private compactModeService = inject(PopupCompactModeService);
private copyButtonsService = inject(VaultPopupCopyButtonsService);
private popupWidthService = inject(PopupWidthService); private popupWidthService = inject(PopupWidthService);
private i18nService = inject(I18nService); private i18nService = inject(I18nService);
@ -56,6 +58,7 @@ export class AppearanceV2Component implements OnInit {
theme: ThemeType.System, theme: ThemeType.System,
enableAnimations: true, enableAnimations: true,
enableCompactMode: false, enableCompactMode: false,
showQuickCopyActions: false,
width: "default" as PopupWidthOption, width: "default" as PopupWidthOption,
}); });
@ -97,6 +100,9 @@ export class AppearanceV2Component implements OnInit {
this.animationControlService.enableRoutingAnimation$, this.animationControlService.enableRoutingAnimation$,
); );
const enableCompactMode = await firstValueFrom(this.compactModeService.enabled$); const enableCompactMode = await firstValueFrom(this.compactModeService.enabled$);
const showQuickCopyActions = await firstValueFrom(
this.copyButtonsService.showQuickCopyActions$,
);
const width = await firstValueFrom(this.popupWidthService.width$); const width = await firstValueFrom(this.popupWidthService.width$);
// Set initial values for the form // Set initial values for the form
@ -106,6 +112,7 @@ export class AppearanceV2Component implements OnInit {
theme, theme,
enableAnimations, enableAnimations,
enableCompactMode, enableCompactMode,
showQuickCopyActions,
width, width,
}); });
@ -141,6 +148,12 @@ export class AppearanceV2Component implements OnInit {
void this.updateCompactMode(enableCompactMode); void this.updateCompactMode(enableCompactMode);
}); });
this.appearanceForm.controls.showQuickCopyActions.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((showQuickCopyActions) => {
void this.updateQuickCopyActions(showQuickCopyActions);
});
this.appearanceForm.controls.width.valueChanges this.appearanceForm.controls.width.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((width) => { .subscribe((width) => {
@ -169,6 +182,10 @@ export class AppearanceV2Component implements OnInit {
await this.compactModeService.setEnabled(enableCompactMode); await this.compactModeService.setEnabled(enableCompactMode);
} }
async updateQuickCopyActions(showQuickCopyActions: boolean) {
await this.copyButtonsService.setShowQuickCopyActions(showQuickCopyActions);
}
async updateWidth(width: PopupWidthOption) { async updateWidth(width: PopupWidthOption) {
await this.popupWidthService.setWidth(width); await this.popupWidthService.setWidth(width);
} }

View File

@ -181,3 +181,4 @@ export const NEW_DEVICE_VERIFICATION_NOTICE = new StateDefinition(
"newDeviceVerificationNotice", "newDeviceVerificationNotice",
"disk", "disk",
); );
export const VAULT_APPEARANCE = new StateDefinition("vaultAppearance", "disk");