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": {
"message": "Show number of login autofill suggestions on extension icon"
},
"showQuickCopyActions": {
"message": "Show quick copy actions on Vault"
},
"systemDefault": {
"message": "System default"
},

View File

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

View File

@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
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 { 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 { CopyCipherFieldDirective } from "@bitwarden/vault";
import { VaultPopupCopyButtonsService } from "../../../services/vault-popup-copy-buttons.service";
@Component({
standalone: true,
selector: "app-item-copy-actions",
@ -23,6 +25,8 @@ import { CopyCipherFieldDirective } from "@bitwarden/vault";
],
})
export class ItemCopyActionsComponent {
protected showQuickCopyActions$ = inject(VaultPopupCopyButtonsService).showQuickCopyActions$;
@Input() cipher: CipherView;
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>
<input bitCheckbox formControlName="showQuickCopyActions" type="checkbox" />
<bit-label>{{ "showQuickCopyActions" | i18n }}</bit-label>
</bit-form-control>
<bit-form-control>
<input bitCheckbox formControlName="enableBadgeCounter" type="checkbox" />
<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 { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
import { PopupWidthService } from "../../../platform/popup/layout/popup-width.service";
import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-buttons.service";
import { AppearanceV2Component } from "./appearance-v2.component";
@ -46,11 +47,13 @@ describe("AppearanceV2Component", () => {
const selectedTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Nord);
const enableRoutingAnimation$ = new BehaviorSubject<boolean>(true);
const enableCompactMode$ = new BehaviorSubject<boolean>(false);
const showQuickCopyActions$ = new BehaviorSubject<boolean>(false);
const setSelectedTheme = jest.fn().mockResolvedValue(undefined);
const setShowFavicons = jest.fn().mockResolvedValue(undefined);
const setEnableBadgeCounter = jest.fn().mockResolvedValue(undefined);
const setEnableRoutingAnimation = jest.fn().mockResolvedValue(undefined);
const setEnableCompactMode = jest.fn().mockResolvedValue(undefined);
const setShowQuickCopyActions = jest.fn().mockResolvedValue(undefined);
const mockWidthService: Partial<PopupWidthService> = {
width$: new BehaviorSubject("default"),
@ -84,6 +87,13 @@ describe("AppearanceV2Component", () => {
provide: PopupCompactModeService,
useValue: { enabled$: enableCompactMode$, setEnabled: setEnableCompactMode },
},
{
provide: VaultPopupCopyButtonsService,
useValue: {
showQuickCopyActions$,
setShowQuickCopyActions,
} as Partial<VaultPopupCopyButtonsService>,
},
{
provide: PopupWidthService,
useValue: mockWidthService,
@ -112,6 +122,7 @@ describe("AppearanceV2Component", () => {
enableBadgeCounter: true,
theme: ThemeType.Nord,
enableCompactMode: false,
showQuickCopyActions: false,
width: "default",
});
});

View File

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

View File

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