1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 14:23:32 +00: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
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,4 +1,40 @@
<bit-item-action *ngIf="cipher.type === CipherType.Login"> <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>
<ng-template #loginCopyMenu>
<bit-item-action>
<button <button
type="button" type="button"
bitIconButton="bwi-clone" bitIconButton="bwi-clone"
@@ -26,9 +62,35 @@
{{ "copyVerificationCode" | i18n }} {{ "copyVerificationCode" | i18n }}
</button> </button>
</bit-menu> </bit-menu>
</bit-item-action> </bit-item-action>
</ng-template>
</ng-container>
<bit-item-action *ngIf="cipher.type === CipherType.Card"> <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 <button
type="button" type="button"
bitIconButton="bwi-clone" bitIconButton="bwi-clone"
@@ -47,7 +109,9 @@
{{ "copySecurityCode" | i18n }} {{ "copySecurityCode" | i18n }}
</button> </button>
</bit-menu> </bit-menu>
</bit-item-action> </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");