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:
parent
05783249b2
commit
a4db5279b7
@ -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"
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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",
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -181,3 +181,4 @@ export const NEW_DEVICE_VERIFICATION_NOTICE = new StateDefinition(
|
||||
"newDeviceVerificationNotice",
|
||||
"disk",
|
||||
);
|
||||
export const VAULT_APPEARANCE = new StateDefinition("vaultAppearance", "disk");
|
||||
|
Loading…
Reference in New Issue
Block a user