mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 14:34:02 +00:00
[PM-26703] - Update Item Action Behavior for Extension (#18921)
* Revert "Revert "[PM-26703]- Browser - Update autofill Behavior (#18467)" (#18723)"
This reverts commit 5d17d9ee71.
* fix title in non-autofill list
* add feature flag
* add old logic. add specs
* revert changes
* remove comments
* update language in spec
* update appearance spec
* revert change to security-tasks
* fix logic for blocked uri. add deprecated notice.
* fix test
* fix type error
This commit is contained in:
@@ -5,8 +5,9 @@
|
||||
[showRefresh]="showRefresh"
|
||||
(onRefresh)="refreshCurrentTab()"
|
||||
[description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : undefined"
|
||||
showAutofillButton
|
||||
isAutofillList
|
||||
[disableDescriptionMargin]="showEmptyAutofillTip$ | async"
|
||||
[primaryActionAutofill]="clickItemsToAutofillVaultView$ | async"
|
||||
[groupByType]="groupByType()"
|
||||
[showAutofillButton]="(clickItemsToAutofillVaultView$ | async) === false"
|
||||
[primaryActionAutofill]="clickItemsToAutofillVaultView$ | async"
|
||||
></app-vault-list-items-container>
|
||||
|
||||
@@ -8,18 +8,18 @@
|
||||
></button>
|
||||
<bit-menu #moreOptions>
|
||||
@if (!decryptionFailure) {
|
||||
<ng-container *ngIf="canAutofill && !hideAutofillOptions">
|
||||
@if (canAutofill && showAutofill()) {
|
||||
<ng-container *ngIf="autofillAllowed$ | async">
|
||||
<button type="button" bitMenuItem (click)="doAutofill()">
|
||||
{{ "autofill" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showViewOption">
|
||||
}
|
||||
@if (showViewOption()) {
|
||||
<button type="button" bitMenuItem (click)="onView()">
|
||||
{{ "view" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
}
|
||||
<button type="button" bitMenuItem (click)="toggleFavorite()">
|
||||
{{ favoriteText | i18n }}
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { booleanAttribute, Component, Input } from "@angular/core";
|
||||
import { booleanAttribute, Component, input, Input } from "@angular/core";
|
||||
import { Router, RouterModule } from "@angular/router";
|
||||
import { BehaviorSubject, combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs";
|
||||
import { filter } from "rxjs/operators";
|
||||
@@ -76,22 +76,17 @@ export class ItemMoreOptionsComponent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag to show view item menu option. Used when something else is
|
||||
* assigned as the primary action for the item, such as autofill.
|
||||
* Flag to show the autofill menu option.
|
||||
* When true, the "Autofill" option appears in the menu.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input({ transform: booleanAttribute })
|
||||
showViewOption = false;
|
||||
readonly showAutofill = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* Flag to hide the autofill menu options. Used for items that are
|
||||
* already in the autofill list suggestion.
|
||||
* Flag to show the view menu option.
|
||||
* When true, the "View" option appears in the menu.
|
||||
* Used when the primary action is autofill (so users can view without autofilling).
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input({ transform: booleanAttribute })
|
||||
hideAutofillOptions = false;
|
||||
readonly showViewOption = input(false, { transform: booleanAttribute });
|
||||
|
||||
protected autofillAllowed$ = this.vaultPopupAutofillService.autofillAllowed$;
|
||||
|
||||
|
||||
@@ -90,11 +90,11 @@
|
||||
</ng-container>
|
||||
|
||||
<cdk-virtual-scroll-viewport [itemSize]="itemHeight$ | async" bitScrollLayout>
|
||||
<bit-item *cdkVirtualFor="let cipher of group.ciphers">
|
||||
<bit-item *cdkVirtualFor="let cipher of group.ciphers" class="tw-group/vault-item">
|
||||
<button
|
||||
bit-item-content
|
||||
type="button"
|
||||
(click)="primaryActionOnSelect(cipher)"
|
||||
(click)="onCipherSelect(cipher)"
|
||||
(dblclick)="launchCipher(cipher)"
|
||||
[appA11yTitle]="
|
||||
cipherItemTitleKey()(cipher)
|
||||
@@ -125,32 +125,45 @@
|
||||
</button>
|
||||
|
||||
<ng-container slot="end">
|
||||
<bit-item-action *ngIf="!hideAutofillButton()">
|
||||
<button
|
||||
type="button"
|
||||
bitBadge
|
||||
variant="primary"
|
||||
(click)="doAutofill(cipher)"
|
||||
[title]="autofillShortcutTooltip() ?? ('autofillTitle' | i18n: cipher.name)"
|
||||
[attr.aria-label]="'autofillTitle' | i18n: cipher.name"
|
||||
>
|
||||
{{ "fill" | i18n }}
|
||||
</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action *ngIf="!showAutofillButton() && CipherViewLikeUtils.canLaunch(cipher)">
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-external-link"
|
||||
size="small"
|
||||
(click)="launchCipher(cipher)"
|
||||
[label]="'launchWebsiteName' | i18n: cipher.name"
|
||||
></button>
|
||||
</bit-item-action>
|
||||
@if (showFillTextOnHover()) {
|
||||
<bit-item-action>
|
||||
<span
|
||||
class="tw-opacity-0 tw-text-sm tw-text-primary-600 tw-px-2 group-hover/vault-item:tw-opacity-100 group-focus-within/vault-item:tw-opacity-100 tw-cursor-pointer"
|
||||
>
|
||||
{{ "fill" | i18n }}
|
||||
</span>
|
||||
</bit-item-action>
|
||||
}
|
||||
@if (showAutofillBadge()) {
|
||||
<bit-item-action>
|
||||
<button
|
||||
type="button"
|
||||
bitBadge
|
||||
variant="primary"
|
||||
(click)="doAutofill(cipher)"
|
||||
[title]="autofillShortcutTooltip() ?? ('autofillTitle' | i18n: cipher.name)"
|
||||
[attr.aria-label]="'autofillTitle' | i18n: cipher.name"
|
||||
>
|
||||
{{ "fill" | i18n }}
|
||||
</button>
|
||||
</bit-item-action>
|
||||
}
|
||||
@if (showLaunchButton() && CipherViewLikeUtils.canLaunch(cipher)) {
|
||||
<bit-item-action>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-external-link"
|
||||
size="small"
|
||||
(click)="launchCipher(cipher)"
|
||||
[label]="'launchWebsiteName' | i18n: cipher.name"
|
||||
></button>
|
||||
</bit-item-action>
|
||||
}
|
||||
<app-item-copy-actions [cipher]="cipher"></app-item-copy-actions>
|
||||
<app-item-more-options
|
||||
[cipher]="cipher"
|
||||
[hideAutofillOptions]="hideAutofillMenuOptions()"
|
||||
[showViewOption]="primaryActionAutofill()"
|
||||
[showAutofill]="showAutofillInMenu()"
|
||||
[showViewOption]="showViewInMenu()"
|
||||
></app-item-more-options>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { Router } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CompactModeService, DialogService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||
import { VaultPopupSectionService } from "../../../services/vault-popup-section.service";
|
||||
import { PopupCipherViewLike } from "../../../views/popup-cipher.view";
|
||||
|
||||
import { VaultListItemsContainerComponent } from "./vault-list-items-container.component";
|
||||
|
||||
describe("VaultListItemsContainerComponent", () => {
|
||||
let fixture: ComponentFixture<VaultListItemsContainerComponent>;
|
||||
let component: VaultListItemsContainerComponent;
|
||||
|
||||
const featureFlag$ = new BehaviorSubject<boolean>(false);
|
||||
const currentTabIsOnBlocklist$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
const mockCipher = {
|
||||
id: "cipher-1",
|
||||
name: "Test Login",
|
||||
type: CipherType.Login,
|
||||
login: {
|
||||
username: "user@example.com",
|
||||
uris: [{ uri: "https://example.com", match: null }],
|
||||
},
|
||||
favorite: false,
|
||||
reprompt: 0,
|
||||
organizationId: null,
|
||||
collectionIds: [],
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
} as any;
|
||||
|
||||
const configService = {
|
||||
getFeatureFlag$: jest.fn().mockImplementation((flag: FeatureFlag) => {
|
||||
if (flag === FeatureFlag.PM31039ItemActionInExtension) {
|
||||
return featureFlag$.asObservable();
|
||||
}
|
||||
return of(false);
|
||||
}),
|
||||
};
|
||||
|
||||
const vaultPopupAutofillService = {
|
||||
currentTabIsOnBlocklist$: currentTabIsOnBlocklist$.asObservable(),
|
||||
doAutofill: jest.fn(),
|
||||
};
|
||||
|
||||
const compactModeService = {
|
||||
enabled$: of(false),
|
||||
};
|
||||
|
||||
const vaultPopupSectionService = {
|
||||
getOpenDisplayStateForSection: jest.fn().mockReturnValue(() => true),
|
||||
updateSectionOpenStoredState: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
featureFlag$.next(false);
|
||||
currentTabIsOnBlocklist$.next(false);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [VaultListItemsContainerComponent, NoopAnimationsModule],
|
||||
providers: [
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{ provide: VaultPopupAutofillService, useValue: vaultPopupAutofillService },
|
||||
{ provide: CompactModeService, useValue: compactModeService },
|
||||
{ provide: VaultPopupSectionService, useValue: vaultPopupSectionService },
|
||||
{ provide: I18nService, useValue: { t: (k: string) => k } },
|
||||
{ provide: AccountService, useValue: { activeAccount$: of({ id: "UserId" }) } },
|
||||
{ provide: CipherService, useValue: mock<CipherService>() },
|
||||
{ provide: Router, useValue: { navigate: jest.fn() } },
|
||||
{ provide: PlatformUtilsService, useValue: { getAutofillKeyboardShortcut: () => "" } },
|
||||
{ provide: DialogService, useValue: mock<DialogService>() },
|
||||
{ provide: PasswordRepromptService, useValue: mock<PasswordRepromptService>() },
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(VaultListItemsContainerComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
describe("Updated item action feature flag", () => {
|
||||
describe("when feature flag is OFF", () => {
|
||||
beforeEach(() => {
|
||||
featureFlag$.next(false);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should not show fill text on hover", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showFillTextOnHover()).toBe(false);
|
||||
});
|
||||
|
||||
it("should show autofill badge when showAutofillButton is true and primaryActionAutofill is false", () => {
|
||||
fixture.componentRef.setInput("showAutofillButton", true);
|
||||
fixture.componentRef.setInput("primaryActionAutofill", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showAutofillBadge()).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide autofill badge when primaryActionAutofill is true", () => {
|
||||
fixture.componentRef.setInput("showAutofillButton", true);
|
||||
fixture.componentRef.setInput("primaryActionAutofill", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showAutofillBadge()).toBe(false);
|
||||
});
|
||||
|
||||
it("should show launch button when showAutofillButton is false", () => {
|
||||
fixture.componentRef.setInput("showAutofillButton", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showLaunchButton()).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide launch button when showAutofillButton is true", () => {
|
||||
fixture.componentRef.setInput("showAutofillButton", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showLaunchButton()).toBe(false);
|
||||
});
|
||||
|
||||
it("should show autofill in menu when showAutofillButton is false", () => {
|
||||
fixture.componentRef.setInput("showAutofillButton", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showAutofillInMenu()).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide autofill in menu when showAutofillButton is true", () => {
|
||||
fixture.componentRef.setInput("showAutofillButton", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showAutofillInMenu()).toBe(false);
|
||||
});
|
||||
|
||||
it("should show view in menu when primaryActionAutofill is true", () => {
|
||||
fixture.componentRef.setInput("primaryActionAutofill", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showViewInMenu()).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide view in menu when primaryActionAutofill is false", () => {
|
||||
fixture.componentRef.setInput("primaryActionAutofill", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showViewInMenu()).toBe(false);
|
||||
});
|
||||
|
||||
it("should autofill on select when primaryActionAutofill is true", () => {
|
||||
fixture.componentRef.setInput("primaryActionAutofill", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.canAutofill()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not autofill on select when primaryActionAutofill is false", () => {
|
||||
fixture.componentRef.setInput("primaryActionAutofill", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.canAutofill()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when feature flag is ON", () => {
|
||||
beforeEach(() => {
|
||||
featureFlag$.next(true);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should show fill text on hover for autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showFillTextOnHover()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not show fill text on hover for non-autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showFillTextOnHover()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not show autofill badge", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", true);
|
||||
fixture.componentRef.setInput("showAutofillButton", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showAutofillBadge()).toBe(false);
|
||||
});
|
||||
|
||||
it("should hide launch button for autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showLaunchButton()).toBe(false);
|
||||
});
|
||||
|
||||
it("should show launch button for non-autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showLaunchButton()).toBe(true);
|
||||
});
|
||||
|
||||
it("should show autofill in menu for non-autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showAutofillInMenu()).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide autofill in menu for autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showAutofillInMenu()).toBe(false);
|
||||
});
|
||||
|
||||
it("should show view in menu for autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showViewInMenu()).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide view in menu for non-autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showViewInMenu()).toBe(false);
|
||||
});
|
||||
|
||||
it("should autofill on select for autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.canAutofill()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not autofill on select for non-autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.canAutofill()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when current URI is blocked", () => {
|
||||
beforeEach(() => {
|
||||
currentTabIsOnBlocklist$.next(true);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should not autofill on select even when feature flag is ON and isAutofillList is true", () => {
|
||||
featureFlag$.next(true);
|
||||
fixture.componentRef.setInput("isAutofillList", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.canAutofill()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not autofill on select even when primaryActionAutofill is true", () => {
|
||||
featureFlag$.next(false);
|
||||
fixture.componentRef.setInput("primaryActionAutofill", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.canAutofill()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("cipherItemTitleKey", () => {
|
||||
it("should return autofillTitle when canAutofill is true", () => {
|
||||
featureFlag$.next(true);
|
||||
fixture.componentRef.setInput("isAutofillList", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
const titleKeyFn = component.cipherItemTitleKey();
|
||||
const result = titleKeyFn(mockCipher);
|
||||
|
||||
expect(result).toBe("autofillTitleWithField");
|
||||
});
|
||||
|
||||
it("should return viewItemTitle when canAutofill is false", () => {
|
||||
featureFlag$.next(true);
|
||||
fixture.componentRef.setInput("isAutofillList", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
const titleKeyFn = component.cipherItemTitleKey();
|
||||
const result = titleKeyFn(mockCipher);
|
||||
|
||||
expect(result).toBe("viewItemTitleWithField");
|
||||
});
|
||||
|
||||
it("should return title without WithField when cipher has no username", () => {
|
||||
featureFlag$.next(true);
|
||||
fixture.componentRef.setInput("isAutofillList", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
const cipherWithoutUsername = {
|
||||
...mockCipher,
|
||||
login: { ...mockCipher.login, username: null },
|
||||
} as PopupCipherViewLike;
|
||||
|
||||
const titleKeyFn = component.cipherItemTitleKey();
|
||||
const result = titleKeyFn(cipherWithoutUsername);
|
||||
|
||||
expect(result).toBe("viewItemTitle");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,8 @@ import { firstValueFrom, map } from "rxjs";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
@@ -88,8 +90,15 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
|
||||
export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
private compactModeService = inject(CompactModeService);
|
||||
private vaultPopupSectionService = inject(VaultPopupSectionService);
|
||||
private configService = inject(ConfigService);
|
||||
protected CipherViewLikeUtils = CipherViewLikeUtils;
|
||||
|
||||
/** Signal for the feature flag that controls simplified item action behavior */
|
||||
protected readonly simplifiedItemActionEnabled = toSignal(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM31039ItemActionInExtension),
|
||||
{ initialValue: false },
|
||||
);
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild(CdkVirtualScrollViewport, { static: false }) viewPort!: CdkVirtualScrollViewport;
|
||||
@@ -136,24 +145,18 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
*/
|
||||
private viewCipherTimeout?: number;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
ciphers = input<PopupCipherViewLike[]>([]);
|
||||
readonly ciphers = input<PopupCipherViewLike[]>([]);
|
||||
|
||||
/**
|
||||
* If true, we will group ciphers by type (Login, Card, Identity)
|
||||
* within subheadings in a single container, converted to a WritableSignal.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
groupByType = input<boolean | undefined>(false);
|
||||
readonly groupByType = input<boolean | undefined>(false);
|
||||
|
||||
/**
|
||||
* Computed signal for a grouped list of ciphers with an optional header
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
cipherGroups = computed<
|
||||
readonly cipherGroups = computed<
|
||||
{
|
||||
subHeaderKey?: string;
|
||||
ciphers: PopupCipherViewLike[];
|
||||
@@ -195,9 +198,7 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
/**
|
||||
* Title for the vault list item section.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
title = input<string | undefined>(undefined);
|
||||
readonly title = input<string | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Optionally allow the items to be collapsed.
|
||||
@@ -205,24 +206,20 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
* The key must be added to the state definition in `vault-popup-section.service.ts` since the
|
||||
* collapsed state is stored locally.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
collapsibleKey = input<keyof PopupSectionOpen | undefined>(undefined);
|
||||
readonly collapsibleKey = input<keyof PopupSectionOpen | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Optional description for the vault list item section. Will be shown below the title even when
|
||||
* no ciphers are available.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
description = input<string | undefined>(undefined);
|
||||
|
||||
readonly description = input<string | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Option to show a refresh button in the section header.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
showRefresh = input(false, { transform: booleanAttribute });
|
||||
|
||||
readonly showRefresh = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* Event emitted when the refresh button is clicked.
|
||||
@@ -235,71 +232,124 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
/**
|
||||
* Flag indicating that the current tab location is blocked
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
currentURIIsBlocked = toSignal(this.vaultPopupAutofillService.currentTabIsOnBlocklist$);
|
||||
readonly currentUriIsBlocked = toSignal(this.vaultPopupAutofillService.currentTabIsOnBlocklist$);
|
||||
|
||||
/**
|
||||
* Resolved i18n key to use for suggested cipher items
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
cipherItemTitleKey = computed(() => {
|
||||
readonly cipherItemTitleKey = computed(() => {
|
||||
return (cipher: CipherViewLike) => {
|
||||
const login = CipherViewLikeUtils.getLogin(cipher);
|
||||
const hasUsername = login?.username != null;
|
||||
const key =
|
||||
this.primaryActionAutofill() && !this.currentURIIsBlocked()
|
||||
? "autofillTitle"
|
||||
: "viewItemTitle";
|
||||
// Use autofill title when autofill is the primary action
|
||||
const key = this.canAutofill() ? "autofillTitle" : "viewItemTitle";
|
||||
return hasUsername ? `${key}WithField` : key;
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated - To be removed when PM31039ItemActionInExtension is fully rolled out
|
||||
* Option to show the autofill button for each item.
|
||||
* Used when feature flag is disabled.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
showAutofillButton = input(false, { transform: booleanAttribute });
|
||||
readonly showAutofillButton = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* Flag indicating whether the suggested cipher item autofill button should be shown or not
|
||||
* @deprecated - To be removed when PM31039ItemActionInExtension is fully rolled out
|
||||
* Whether to show the autofill badge button (old behavior).
|
||||
* Only shown when feature flag is disabled AND conditions are met.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
hideAutofillButton = computed(
|
||||
() => !this.showAutofillButton() || this.currentURIIsBlocked() || this.primaryActionAutofill(),
|
||||
readonly showAutofillBadge = computed(
|
||||
() => !this.simplifiedItemActionEnabled() && !this.hideAutofillButton(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Flag indicating whether the cipher item autofill menu options should be shown or not
|
||||
* @deprecated - To be removed when PM31039ItemActionInExtension is fully rolled out
|
||||
* Flag indicating whether the cipher item autofill menu options should be shown or not.
|
||||
* Used when feature flag is disabled.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
hideAutofillMenuOptions = computed(() => this.currentURIIsBlocked() || this.showAutofillButton());
|
||||
readonly hideAutofillMenuOptions = computed(
|
||||
() => this.currentUriIsBlocked() || this.showAutofillButton(),
|
||||
);
|
||||
|
||||
/**
|
||||
* @deprecated - To be removed when PM31039ItemActionInExtension is fully rolled out
|
||||
* Option to perform autofill operation as the primary action for autofill suggestions.
|
||||
* Used when feature flag is disabled.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
primaryActionAutofill = input(false, { transform: booleanAttribute });
|
||||
readonly primaryActionAutofill = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* @deprecated - To be removed when PM31039ItemActionInExtension is fully rolled out
|
||||
* Flag indicating whether the suggested cipher item autofill button should be shown or not.
|
||||
* Used when feature flag is disabled.
|
||||
*/
|
||||
readonly hideAutofillButton = computed(
|
||||
() => !this.showAutofillButton() || this.currentUriIsBlocked() || this.primaryActionAutofill(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Option to mark this container as an autofill list.
|
||||
*/
|
||||
readonly isAutofillList = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* Computed property whether the cipher action may perform autofill.
|
||||
* When feature flag is enabled, uses isAutofillList.
|
||||
* When feature flag is disabled, uses primaryActionAutofill.
|
||||
*/
|
||||
readonly canAutofill = computed(() => {
|
||||
if (this.currentUriIsBlocked()) {
|
||||
return false;
|
||||
}
|
||||
return this.isAutofillList()
|
||||
? this.simplifiedItemActionEnabled()
|
||||
: this.primaryActionAutofill();
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether to show the "Fill" text on hover.
|
||||
* Only shown when feature flag is enabled AND this is an autofill list.
|
||||
*/
|
||||
readonly showFillTextOnHover = computed(
|
||||
() => this.simplifiedItemActionEnabled() && this.canAutofill(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Whether to show the launch button.
|
||||
*/
|
||||
readonly showLaunchButton = computed(() =>
|
||||
this.simplifiedItemActionEnabled() ? !this.isAutofillList() : !this.showAutofillButton(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Whether to show the "Autofill" option in the more options menu.
|
||||
* New behavior: show for non-autofill list items.
|
||||
* Old behavior: show when not hidden by hideAutofillMenuOptions.
|
||||
*/
|
||||
readonly showAutofillInMenu = computed(() =>
|
||||
this.simplifiedItemActionEnabled() ? !this.canAutofill() : !this.hideAutofillMenuOptions(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Whether to show the "View" option in the more options menu.
|
||||
* New behavior: show for autofill list items (since click = autofill).
|
||||
* Old behavior: show when primary action is autofill.
|
||||
*/
|
||||
readonly showViewInMenu = computed(() =>
|
||||
this.simplifiedItemActionEnabled() ? this.isAutofillList() : this.primaryActionAutofill(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Remove the bottom margin from the bit-section in this component
|
||||
* (used for containers at the end of the page where bottom margin is not needed)
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
disableSectionMargin = input(false, { transform: booleanAttribute });
|
||||
readonly disableSectionMargin = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* Remove the description margin
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
disableDescriptionMargin = input(false, { transform: booleanAttribute });
|
||||
readonly disableDescriptionMargin = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* The tooltip text for the organization icon for ciphers that belong to an organization.
|
||||
@@ -313,9 +363,7 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
return collections[0]?.name;
|
||||
}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
protected autofillShortcutTooltip = signal<string | undefined>(undefined);
|
||||
protected readonly autofillShortcutTooltip = signal<string | undefined>(undefined);
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
@@ -340,10 +388,8 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
primaryActionOnSelect(cipher: PopupCipherViewLike) {
|
||||
return this.primaryActionAutofill() && !this.currentURIIsBlocked()
|
||||
? this.doAutofill(cipher)
|
||||
: this.onViewCipher(cipher);
|
||||
onCipherSelect(cipher: PopupCipherViewLike) {
|
||||
return this.canAutofill() ? this.doAutofill(cipher) : this.onViewCipher(cipher);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -50,16 +50,18 @@
|
||||
<vault-permit-cipher-details-popover></vault-permit-cipher-details-popover>
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<bit-form-control [disableMargin]="simplifiedItemActionEnabled()">
|
||||
<input bitCheckbox formControlName="showQuickCopyActions" type="checkbox" />
|
||||
<bit-label>{{ "showQuickCopyActions" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control disableMargin>
|
||||
<input bitCheckbox formControlName="clickItemsToAutofillVaultView" type="checkbox" />
|
||||
<bit-label>
|
||||
{{ "clickToAutofill" | i18n }}
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
@if (!simplifiedItemActionEnabled()) {
|
||||
<bit-form-control disableMargin>
|
||||
<input bitCheckbox formControlName="clickItemsToAutofillVaultView" type="checkbox" />
|
||||
<bit-label>
|
||||
{{ "clickToAutofill" | i18n }}
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
}
|
||||
</bit-card>
|
||||
</form>
|
||||
</popup-page>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -59,7 +61,7 @@ describe("AppearanceComponent", () => {
|
||||
const enableRoutingAnimation$ = new BehaviorSubject<boolean>(true);
|
||||
const enableCompactMode$ = new BehaviorSubject<boolean>(false);
|
||||
const showQuickCopyActions$ = new BehaviorSubject<boolean>(false);
|
||||
const clickItemsToAutofillVaultView$ = new BehaviorSubject<boolean>(false);
|
||||
const featureFlag$ = new BehaviorSubject<boolean>(false);
|
||||
const setSelectedTheme = jest.fn().mockResolvedValue(undefined);
|
||||
const setShowFavicons = jest.fn().mockResolvedValue(undefined);
|
||||
const setEnableBadgeCounter = jest.fn().mockResolvedValue(undefined);
|
||||
@@ -78,11 +80,20 @@ describe("AppearanceComponent", () => {
|
||||
setShowFavicons.mockClear();
|
||||
setEnableBadgeCounter.mockClear();
|
||||
setEnableRoutingAnimation.mockClear();
|
||||
setClickItemsToAutofillVaultView.mockClear();
|
||||
|
||||
const configService = mock<ConfigService>();
|
||||
configService.getFeatureFlag$.mockImplementation((flag: FeatureFlag) => {
|
||||
if (flag === FeatureFlag.PM31039ItemActionInExtension) {
|
||||
return featureFlag$.asObservable();
|
||||
}
|
||||
return of(false);
|
||||
});
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AppearanceComponent],
|
||||
providers: [
|
||||
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
{ provide: MessagingService, useValue: mock<MessagingService>() },
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
@@ -114,7 +125,7 @@ describe("AppearanceComponent", () => {
|
||||
{
|
||||
provide: VaultSettingsService,
|
||||
useValue: {
|
||||
clickItemsToAutofillVaultView$,
|
||||
clickItemsToAutofillVaultView$: of(false),
|
||||
setClickItemsToAutofillVaultView,
|
||||
},
|
||||
},
|
||||
@@ -193,11 +204,40 @@ describe("AppearanceComponent", () => {
|
||||
|
||||
expect(mockWidthService.setWidth).toHaveBeenCalledWith("wide");
|
||||
});
|
||||
});
|
||||
|
||||
it("updates the click items to autofill vault view setting", () => {
|
||||
component.appearanceForm.controls.clickItemsToAutofillVaultView.setValue(true);
|
||||
describe("PM31039ItemActionInExtension feature flag", () => {
|
||||
describe("when set to OFF", () => {
|
||||
it("should show clickItemsToAutofillVaultView checkbox", () => {
|
||||
featureFlag$.next(false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(setClickItemsToAutofillVaultView).toHaveBeenCalledWith(true);
|
||||
const checkbox = fixture.debugElement.query(
|
||||
By.css('input[formControlName="clickItemsToAutofillVaultView"]'),
|
||||
);
|
||||
expect(checkbox).not.toBeNull();
|
||||
});
|
||||
|
||||
it("should update the clickItemsToAutofillVaultView setting when changed", () => {
|
||||
featureFlag$.next(false);
|
||||
fixture.detectChanges();
|
||||
|
||||
component.appearanceForm.controls.clickItemsToAutofillVaultView.setValue(true);
|
||||
|
||||
expect(setClickItemsToAutofillVaultView).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when set to ON", () => {
|
||||
it("should hide clickItemsToAutofillVaultView checkbox", () => {
|
||||
featureFlag$.next(true);
|
||||
fixture.detectChanges();
|
||||
|
||||
const checkbox = fixture.debugElement.query(
|
||||
By.css('input[formControlName="clickItemsToAutofillVaultView"]'),
|
||||
);
|
||||
expect(checkbox).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
@@ -57,6 +59,13 @@ export class AppearanceComponent implements OnInit {
|
||||
private copyButtonsService = inject(VaultPopupCopyButtonsService);
|
||||
private popupSizeService = inject(PopupSizeService);
|
||||
private i18nService = inject(I18nService);
|
||||
private configService = inject(ConfigService);
|
||||
|
||||
/** Signal for the feature flag that controls simplified item action behavior */
|
||||
protected readonly simplifiedItemActionEnabled = toSignal(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM31039ItemActionInExtension),
|
||||
{ initialValue: false },
|
||||
);
|
||||
|
||||
appearanceForm = this.formBuilder.group({
|
||||
enableFavicon: false,
|
||||
|
||||
@@ -72,6 +72,7 @@ export enum FeatureFlag {
|
||||
BrowserPremiumSpotlight = "pm-23384-browser-premium-spotlight",
|
||||
MigrateMyVaultToMyItems = "pm-20558-migrate-myvault-to-myitems",
|
||||
PM27632_SdkCipherCrudOperations = "pm-27632-cipher-crud-operations-to-sdk",
|
||||
PM31039ItemActionInExtension = "pm-31039-item-action-in-extension",
|
||||
|
||||
/* Platform */
|
||||
ContentScriptIpcChannelFramework = "content-script-ipc-channel-framework",
|
||||
@@ -117,6 +118,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.WindowsDesktopAutotype]: FALSE,
|
||||
[FeatureFlag.WindowsDesktopAutotypeGA]: FALSE,
|
||||
[FeatureFlag.SSHAgentV2]: FALSE,
|
||||
[FeatureFlag.PM31039ItemActionInExtension]: FALSE,
|
||||
|
||||
/* Tools */
|
||||
[FeatureFlag.UseSdkPasswordGenerators]: FALSE,
|
||||
|
||||
Reference in New Issue
Block a user