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