1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-20 11:24:07 +00:00

Merge branch 'main' into beeep/dev-container

This commit is contained in:
Conner Turnbull
2026-02-12 12:42:03 -05:00
committed by GitHub
189 changed files with 5258 additions and 1409 deletions

View File

@@ -2054,7 +2054,6 @@ jobs:
sudo apt-get update
sudo apt-get install -y libasound2 flatpak xvfb dbus-x11
flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install -y --user flathub
- name: Install flatpak
working-directory: apps/desktop/artifacts/linux/flatpak

View File

@@ -91,7 +91,9 @@ jobs:
apps/cli/bw-macos-${{ env.PKG_VERSION }}.zip,
apps/cli/bw-macos-arm64-${{ env.PKG_VERSION }}.zip,
apps/cli/bw-oss-linux-${{ env.PKG_VERSION }}.zip,
apps/cli/bw-oss-linux-arm64-${{ env.PKG_VERSION }}.zip,
apps/cli/bw-linux-${{ env.PKG_VERSION }}.zip,
apps/cli/bw-linux-arm64-${{ env.PKG_VERSION }}.zip,
apps/cli/bitwarden-cli.${{ env.PKG_VERSION }}.nupkg,
apps/cli/bw_${{ env.PKG_VERSION }}_amd64.snap,
apps/cli/bitwarden-cli-${{ env.PKG_VERSION }}-npm-build.zip"

View File

@@ -2747,9 +2747,6 @@
"excludedDomainsDesc": {
"message": "Bitwarden will not ask to save login details for these domains. You must refresh the page for changes to take effect."
},
"excludedDomainsDescAlt": {
"message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect."
},
"blockedDomainsDesc": {
"message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect."
},
@@ -5966,6 +5963,9 @@
"cardNumberLabel": {
"message": "Card number"
},
"errorCannotDecrypt": {
"message": "Error: Cannot decrypt"
},
"removeMasterPasswordForOrgUserKeyConnector": {
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
},
@@ -6127,6 +6127,10 @@
"emailPlaceholder": {
"message": "user@bitwarden.com , user@acme.com"
},
"downloadBitwardenApps": {
"message": "Download Bitwarden apps"
},
"emailProtected": {
"message": "Email protected"
},
@@ -6134,4 +6138,4 @@
"message": "Individuals will need to enter the password to view this Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}
}
}

View File

@@ -4,70 +4,70 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { CollectionView } from "../../content/components/common-types";
import { NotificationType, NotificationTypes } from "../../enums/notification-type.enum";
import { NotificationType } from "../../enums/notification-type.enum";
import AutofillPageDetails from "../../models/autofill-page-details";
/**
* @todo Remove Standard_ label when implemented as standard NotificationQueueMessage.
* Generic notification queue message structure.
* All notification types use this structure with type-specific data.
*/
export interface Standard_NotificationQueueMessage<T, D> {
// universal notification properties
export interface NotificationQueueMessage<T, D> {
domain: string;
tab: chrome.tabs.Tab;
launchTimestamp: number;
expires: Date;
wasVaultLocked: boolean;
type: T; // NotificationType
data: D; // notification-specific data
type: T;
data: D;
}
/**
* @todo Deprecate in favor of Standard_NotificationQueueMessage.
*/
interface NotificationQueueMessage {
type: NotificationTypes;
domain: string;
tab: chrome.tabs.Tab;
launchTimestamp: number;
expires: Date;
wasVaultLocked: boolean;
}
// Notification data type definitions
export type AddLoginNotificationData = {
username: string;
password: string;
uri: string;
};
type ChangePasswordNotificationData = {
export type ChangePasswordNotificationData = {
cipherIds: CipherView["id"][];
newPassword: string;
};
type AddChangePasswordNotificationQueueMessage = Standard_NotificationQueueMessage<
export type UnlockVaultNotificationData = never;
export type AtRiskPasswordNotificationData = {
organizationName: string;
passwordChangeUri?: string;
};
// Notification queue message types using generic pattern
export type AddLoginQueueMessage = NotificationQueueMessage<
typeof NotificationType.AddLogin,
AddLoginNotificationData
>;
export type AddChangePasswordNotificationQueueMessage = NotificationQueueMessage<
typeof NotificationType.ChangePassword,
ChangePasswordNotificationData
>;
interface AddLoginQueueMessage extends NotificationQueueMessage {
type: "add";
username: string;
password: string;
uri: string;
}
export type AddUnlockVaultQueueMessage = NotificationQueueMessage<
typeof NotificationType.UnlockVault,
UnlockVaultNotificationData
>;
interface AddUnlockVaultQueueMessage extends NotificationQueueMessage {
type: "unlock";
}
export type AtRiskPasswordQueueMessage = NotificationQueueMessage<
typeof NotificationType.AtRiskPassword,
AtRiskPasswordNotificationData
>;
interface AtRiskPasswordQueueMessage extends NotificationQueueMessage {
type: "at-risk-password";
organizationName: string;
passwordChangeUri?: string;
}
type NotificationQueueMessageItem =
export type NotificationQueueMessageItem =
| AddLoginQueueMessage
| AddChangePasswordNotificationQueueMessage
| AddUnlockVaultQueueMessage
| AtRiskPasswordQueueMessage;
type LockedVaultPendingNotificationsData = {
export type LockedVaultPendingNotificationsData = {
commandToRetry: {
message: {
command: string;
@@ -80,26 +80,26 @@ type LockedVaultPendingNotificationsData = {
target: string;
};
type AdjustNotificationBarMessageData = {
export type AdjustNotificationBarMessageData = {
height: number;
};
type AddLoginMessageData = {
export type AddLoginMessageData = {
username: string;
password: string;
url: string;
};
type UnlockVaultMessageData = {
export type UnlockVaultMessageData = {
skipNotification?: boolean;
};
/**
* @todo Extend generics to this type, see Standard_NotificationQueueMessage
* @todo Extend generics to this type, see NotificationQueueMessage
* - use new `data` types as generic
* - eliminate optional status of properties as needed per Notification Type
*/
type NotificationBackgroundExtensionMessage = {
export type NotificationBackgroundExtensionMessage = {
[key: string]: any;
command: string;
data?: Partial<LockedVaultPendingNotificationsData> &
@@ -119,7 +119,7 @@ type BackgroundMessageParam = { message: NotificationBackgroundExtensionMessage
type BackgroundSenderParam = { sender: chrome.runtime.MessageSender };
type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam;
type NotificationBackgroundExtensionMessageHandlers = {
export type NotificationBackgroundExtensionMessageHandlers = {
[key: string]: CallableFunction;
unlockCompleted: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgGetFolderData: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<FolderView[]>;
@@ -150,16 +150,3 @@ type NotificationBackgroundExtensionMessageHandlers = {
bgGetActiveUserServerConfig: () => Promise<ServerConfig | null>;
getWebVaultUrlForNotification: () => Promise<string>;
};
export {
AddChangePasswordNotificationQueueMessage,
AddLoginQueueMessage,
AddUnlockVaultQueueMessage,
NotificationQueueMessageItem,
LockedVaultPendingNotificationsData,
AdjustNotificationBarMessageData,
UnlockVaultMessageData,
AddLoginMessageData,
NotificationBackgroundExtensionMessage,
NotificationBackgroundExtensionMessageHandlers,
};

View File

@@ -126,9 +126,11 @@ describe("NotificationBackground", () => {
it("returns a cipher view when passed an `AddLoginQueueMessage`", () => {
const message: AddLoginQueueMessage = {
type: "add",
username: "test",
password: "password",
uri: "https://example.com",
data: {
username: "test",
password: "password",
uri: "https://example.com",
},
domain: "",
tab: createChromeTabMock(),
expires: new Date(),
@@ -140,13 +142,13 @@ describe("NotificationBackground", () => {
expect(cipherView.name).toEqual("example.com");
expect(cipherView.login).toEqual({
fido2Credentials: [],
password: message.password,
password: message.data.password,
uris: [
{
_uri: message.uri,
_uri: message.data.uri,
},
],
username: message.username,
username: message.data.username,
});
});
@@ -154,9 +156,11 @@ describe("NotificationBackground", () => {
const folderId = "folder-id";
const message: AddLoginQueueMessage = {
type: "add",
username: "test",
password: "password",
uri: "https://example.com",
data: {
username: "test",
password: "password",
uri: "https://example.com",
},
domain: "example.com",
tab: createChromeTabMock(),
expires: new Date(),
@@ -170,6 +174,44 @@ describe("NotificationBackground", () => {
expect(cipherView.folderId).toEqual(folderId);
});
it("removes 'www.' prefix from hostname when generating cipher name", () => {
const message: AddLoginQueueMessage = {
type: "add",
data: {
username: "test",
password: "password",
uri: "https://www.example.com",
},
domain: "www.example.com",
tab: createChromeTabMock(),
expires: new Date(),
wasVaultLocked: false,
launchTimestamp: 0,
};
const cipherView = notificationBackground["convertAddLoginQueueMessageToCipherView"](message);
expect(cipherView.name).toEqual("example.com");
});
it("uses domain as fallback when hostname cannot be extracted from uri", () => {
const message: AddLoginQueueMessage = {
type: "add",
data: {
username: "test",
password: "password",
uri: "",
},
domain: "fallback-domain.com",
tab: createChromeTabMock(),
expires: new Date(),
wasVaultLocked: false,
launchTimestamp: 0,
};
const cipherView = notificationBackground["convertAddLoginQueueMessageToCipherView"](message);
expect(cipherView.name).toEqual("fallback-domain.com");
});
});
describe("notification bar extension message handlers and triggers", () => {
@@ -2544,8 +2586,11 @@ describe("NotificationBackground", () => {
type: NotificationType.AddLogin,
tab,
domain: "example.com",
username: "test",
password: "updated-password",
data: {
username: "test",
password: "updated-password",
uri: "https://example.com",
},
wasVaultLocked: true,
});
notificationBackground["notificationQueue"] = [queueMessage];
@@ -2559,7 +2604,7 @@ describe("NotificationBackground", () => {
expect(updatePasswordSpy).toHaveBeenCalledWith(
cipherView,
queueMessage.password,
queueMessage.data.password,
message.edit,
sender.tab,
"testId",
@@ -2631,9 +2676,14 @@ describe("NotificationBackground", () => {
type: NotificationType.AddLogin,
tab,
domain: "example.com",
username: "test",
password: "password",
data: {
username: "test",
password: "password",
uri: "https://example.com",
},
wasVaultLocked: false,
launchTimestamp: Date.now(),
expires: new Date(Date.now() + 10000),
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>({
@@ -2670,9 +2720,14 @@ describe("NotificationBackground", () => {
type: NotificationType.AddLogin,
tab,
domain: "example.com",
username: "test",
password: "password",
data: {
username: "test",
password: "password",
uri: "https://example.com",
},
wasVaultLocked: false,
launchTimestamp: Date.now(),
expires: new Date(Date.now() + 10000),
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>({
@@ -2716,9 +2771,14 @@ describe("NotificationBackground", () => {
type: NotificationType.AddLogin,
tab,
domain: "example.com",
username: "test",
password: "password",
data: {
username: "test",
password: "password",
uri: "https://example.com",
},
wasVaultLocked: false,
launchTimestamp: Date.now(),
expires: new Date(Date.now() + 10000),
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>({

View File

@@ -68,6 +68,7 @@ import {
AddChangePasswordNotificationQueueMessage,
AddLoginQueueMessage,
AddLoginMessageData,
AtRiskPasswordQueueMessage,
NotificationQueueMessageItem,
LockedVaultPendingNotificationsData,
NotificationBackgroundExtensionMessage,
@@ -528,12 +529,14 @@ export default class NotificationBackground {
this.removeTabFromNotificationQueue(tab);
const launchTimestamp = new Date().getTime();
const queueMessage: NotificationQueueMessageItem = {
const queueMessage: AtRiskPasswordQueueMessage = {
domain,
wasVaultLocked,
type: NotificationType.AtRiskPassword,
passwordChangeUri,
organizationName: organization.name,
data: {
passwordChangeUri,
organizationName: organization.name,
},
tab: tab,
launchTimestamp,
expires: new Date(launchTimestamp + NOTIFICATION_BAR_LIFESPAN_MS),
@@ -612,10 +615,12 @@ export default class NotificationBackground {
const launchTimestamp = new Date().getTime();
const message: AddLoginQueueMessage = {
type: NotificationType.AddLogin,
username: loginInfo.username,
password: loginInfo.password,
data: {
username: loginInfo.username,
password: loginInfo.password,
uri: loginInfo.url,
},
domain: loginDomain,
uri: loginInfo.url,
tab: tab,
launchTimestamp,
expires: new Date(launchTimestamp + NOTIFICATION_BAR_LIFESPAN_MS),
@@ -1291,16 +1296,23 @@ export default class NotificationBackground {
// If the vault was locked, check if a cipher needs updating instead of creating a new one
if (queueMessage.wasVaultLocked) {
const allCiphers = await this.cipherService.getAllDecryptedForUrl(
queueMessage.uri,
queueMessage.data.uri,
activeUserId,
);
const existingCipher = allCiphers.find(
(c) =>
c.login.username != null && c.login.username.toLowerCase() === queueMessage.username,
c.login.username != null &&
c.login.username.toLowerCase() === queueMessage.data.username,
);
if (existingCipher != null) {
await this.updatePassword(existingCipher, queueMessage.password, edit, tab, activeUserId);
await this.updatePassword(
existingCipher,
queueMessage.data.password,
edit,
tab,
activeUserId,
);
return;
}
}
@@ -1721,15 +1733,15 @@ export default class NotificationBackground {
folderId?: string,
): CipherView {
const uriView = new LoginUriView();
uriView.uri = message.uri;
uriView.uri = message.data.uri;
const loginView = new LoginView();
loginView.uris = [uriView];
loginView.username = message.username;
loginView.password = message.password;
loginView.username = message.data.username;
loginView.password = message.data.password;
const cipherView = new CipherView();
cipherView.name = (Utils.getHostname(message.uri) || message.domain).replace(/^www\./, "");
cipherView.name = (Utils.getHostname(message.data.uri) || message.domain).replace(/^www\./, "");
cipherView.folderId = folderId;
cipherView.type = CipherType.Login;
cipherView.login = loginView;

View File

@@ -3,6 +3,7 @@ import { html, TemplateResult } from "lit";
import { Theme } from "@bitwarden/common/platform/enums";
import { EventSecurity } from "../../../utils/event-security";
import { border, themes, typography, spacing } from "../constants/styles";
import { Spinner } from "../icons";
@@ -26,7 +27,7 @@ export function ActionButton({
fullWidth = true,
}: ActionButtonProps) {
const handleButtonClick = (event: Event) => {
if (!disabled && !isLoading) {
if (EventSecurity.isEventTrusted(event) && !disabled && !isLoading) {
handleClick(event);
}
};

View File

@@ -3,6 +3,7 @@ import { html } from "lit";
import { Theme } from "@bitwarden/common/platform/enums";
import { EventSecurity } from "../../../utils/event-security";
import { border, themes, typography, spacing } from "../constants/styles";
export type BadgeButtonProps = {
@@ -23,7 +24,7 @@ export function BadgeButton({
username,
}: BadgeButtonProps) {
const handleButtonClick = (event: Event) => {
if (!disabled) {
if (EventSecurity.isEventTrusted(event) && !disabled) {
buttonAction(event);
}
};

View File

@@ -3,6 +3,7 @@ import { html } from "lit";
import { Theme } from "@bitwarden/common/platform/enums";
import { EventSecurity } from "../../../utils/event-security";
import { themes, typography, spacing } from "../constants/styles";
import { PencilSquare } from "../icons";
@@ -21,7 +22,7 @@ export function EditButton({ buttonAction, buttonText, disabled = false, theme }
aria-label=${buttonText}
class=${editButtonStyles({ disabled, theme })}
@click=${(event: Event) => {
if (!disabled) {
if (EventSecurity.isEventTrusted(event) && !disabled) {
buttonAction(event);
}
}}

View File

@@ -3,6 +3,7 @@ import { html, nothing } from "lit";
import { Theme } from "@bitwarden/common/platform/enums";
import { EventSecurity } from "../../../../utils/event-security";
import { spacing, themes, typography } from "../../constants/styles";
export type NotificationConfirmationMessageProps = {
@@ -127,7 +128,7 @@ const AdditionalMessageStyles = ({ theme }: { theme: Theme }) => css`
`;
function handleButtonKeyDown(event: KeyboardEvent, handleClick: () => void) {
if (event.key === "Enter" || event.key === " ") {
if (EventSecurity.isEventTrusted(event) && (event.key === "Enter" || event.key === " ")) {
event.preventDefault();
handleClick();
}

View File

@@ -3,6 +3,7 @@ import { html, nothing } from "lit";
import { Theme } from "@bitwarden/common/platform/enums";
import { EventSecurity } from "../../../utils/event-security";
import { IconProps, Option } from "../common-types";
import { themes, spacing } from "../constants/styles";
@@ -29,6 +30,13 @@ export function OptionItem({
handleSelection,
}: OptionItemProps) {
const handleSelectionKeyUpProxy = (event: KeyboardEvent) => {
/**
* Reject synthetic events (not originating from the user agent)
*/
if (!EventSecurity.isEventTrusted(event)) {
return;
}
const listenedForKeys = new Set(["Enter", "Space"]);
if (listenedForKeys.has(event.code) && event.target instanceof Element) {
handleSelection();
@@ -37,6 +45,17 @@ export function OptionItem({
return;
};
const handleSelectionClickProxy = (event: MouseEvent) => {
/**
* Reject synthetic events (not originating from the user agent)
*/
if (!EventSecurity.isEventTrusted(event)) {
return;
}
handleSelection();
};
const iconProps: IconProps = { color: themes[theme].text.main, theme };
const itemIcon = icon?.(iconProps);
const ariaLabel =
@@ -52,7 +71,7 @@ export function OptionItem({
title=${text}
role="option"
aria-label=${ariaLabel}
@click=${handleSelection}
@click=${handleSelectionClickProxy}
@keyup=${handleSelectionKeyUpProxy}
>
${itemIcon ? html`<div class=${optionItemIconContainerStyles}>${itemIcon}</div>` : nothing}

View File

@@ -3,6 +3,7 @@ import { html, nothing } from "lit";
import { Theme } from "@bitwarden/common/platform/enums";
import { EventSecurity } from "../../../utils/event-security";
import { Option } from "../common-types";
import { themes, typography, scrollbarStyles, spacing } from "../constants/styles";
@@ -57,6 +58,10 @@ export function OptionItems({
}
function handleMenuKeyUp(event: KeyboardEvent) {
if (!EventSecurity.isEventTrusted(event)) {
return;
}
const items = [
...(event.currentTarget as HTMLElement).querySelectorAll<HTMLElement>('[tabindex="0"]'),
];

View File

@@ -4,6 +4,7 @@ import { property, state } from "lit/decorators.js";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
import { EventSecurity } from "../../../utils/event-security";
import { OptionSelectionButton } from "../buttons/option-selection-button";
import { Option } from "../common-types";
@@ -54,7 +55,7 @@ export class OptionSelection extends LitElement {
private static currentOpenInstance: OptionSelection | null = null;
private handleButtonClick = async (event: Event) => {
if (!this.disabled) {
if (EventSecurity.isEventTrusted(event) && !this.disabled) {
const isOpening = !this.showMenu;
if (isOpening) {

View File

@@ -3,6 +3,7 @@ import { mock } from "jest-mock-extended";
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
import { postWindowMessage, sendMockExtensionMessage } from "../spec/testing-utils";
import { EventSecurity } from "../utils/event-security";
describe("ContentMessageHandler", () => {
const sendMessageSpy = jest.spyOn(chrome.runtime, "sendMessage");
@@ -19,6 +20,7 @@ describe("ContentMessageHandler", () => {
);
beforeEach(() => {
jest.spyOn(EventSecurity, "isEventTrusted").mockReturnValue(true);
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-require-imports
require("./content-message-handler");

View File

@@ -1,6 +1,8 @@
import { ExtensionPageUrls } from "@bitwarden/common/vault/enums";
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
import { EventSecurity } from "../utils/event-security";
import {
ContentMessageWindowData,
ContentMessageWindowEventHandlers,
@@ -92,7 +94,10 @@ function handleOpenBrowserExtensionToUrlMessage({ url }: { url?: ExtensionPageUr
*/
function handleWindowMessageEvent(event: MessageEvent) {
const { source, data, origin } = event;
if (source !== window || !data?.command) {
/**
* Reject synthetic events (not originating from the user agent)
*/
if (!EventSecurity.isEventTrusted(event) || source !== window || !data?.command) {
return;
}

View File

@@ -1,3 +1,5 @@
import { EventSecurity } from "../utils/event-security";
const inputTags = ["input", "textarea", "select"];
const labelTags = ["label", "span"];
const attributeKeys = ["id", "name", "label-aria", "placeholder"];
@@ -52,6 +54,12 @@ function isNullOrEmpty(s: string | null) {
// We only have access to the element that's been clicked when the context menu is first opened.
// Remember it for use later.
document.addEventListener("contextmenu", (event) => {
/**
* Reject synthetic events (not originating from the user agent)
*/
if (!EventSecurity.isEventTrusted(event)) {
return;
}
clickedElement = event.target as HTMLElement;
});

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
BehaviorSubject,
EmptyError,
@@ -79,7 +77,7 @@ export type BrowserFido2Message = { sessionId: string } & (
}
| {
type: typeof BrowserFido2MessageTypes.PickCredentialResponse;
cipherId?: string;
cipherId: string;
userVerified: boolean;
}
| {

View File

@@ -10,6 +10,7 @@ import {
createInitAutofillInlineMenuListMessageMock,
} from "../../../../spec/autofill-mocks";
import { flushPromises, postWindowMessage } from "../../../../spec/testing-utils";
import { EventSecurity } from "../../../../utils/event-security";
import { AutofillInlineMenuList } from "./autofill-inline-menu-list";
@@ -28,6 +29,7 @@ describe("AutofillInlineMenuList", () => {
const events: { eventName: any; callback: any }[] = [];
beforeEach(() => {
jest.spyOn(EventSecurity, "isEventTrusted").mockReturnValue(true);
const oldEv = globalThis.addEventListener;
globalThis.addEventListener = (eventName: any, callback: any) => {
events.push({ eventName, callback });

View File

@@ -10,6 +10,7 @@ import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
import { InlineMenuCipherData } from "../../../../background/abstractions/overlay.background";
import { InlineMenuFillType } from "../../../../enums/autofill-overlay.enum";
import { buildSvgDomElement, specialCharacterToKeyMap, throttle } from "../../../../utils";
import { EventSecurity } from "../../../../utils/event-security";
import {
creditCardIcon,
globeIcon,
@@ -203,7 +204,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
private handleSaveLoginInlineMenuKeyUp = (event: KeyboardEvent) => {
const listenedForKeys = new Set(["ArrowDown"]);
if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) {
if (
/**
* Reject synthetic events (not originating from the user agent)
*/
!EventSecurity.isEventTrusted(event) ||
!listenedForKeys.has(event.code) ||
!(event.target instanceof Element)
) {
return;
}
@@ -229,7 +237,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* Handles the click event for the unlock button.
* Sends a message to the parent window to unlock the vault.
*/
private handleUnlockButtonClick = () => {
private handleUnlockButtonClick = (event: MouseEvent) => {
/**
* Reject synthetic events (not originating from the user agent)
*/
if (!EventSecurity.isEventTrusted(event)) {
return;
}
this.postMessageToParent({ command: "unlockVault" });
};
@@ -352,7 +367,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* Handles the click event for the fill generated password button. Triggers
* a message to the background script to fill the generated password.
*/
private handleFillGeneratedPasswordClick = () => {
private handleFillGeneratedPasswordClick = (event?: MouseEvent) => {
/**
* Reject synthetic events (not originating from the user agent)
*/
if (event && !EventSecurity.isEventTrusted(event)) {
return;
}
this.postMessageToParent({ command: "fillGeneratedPassword" });
};
@@ -362,7 +384,16 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* @param event - The keyup event.
*/
private handleFillGeneratedPasswordKeyUp = (event: KeyboardEvent) => {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
/**
* Reject synthetic events (not originating from the user agent)
*/
if (
!EventSecurity.isEventTrusted(event) ||
event.ctrlKey ||
event.altKey ||
event.metaKey ||
event.shiftKey
) {
return;
}
@@ -388,6 +419,13 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* @param event - The click event.
*/
private handleRefreshGeneratedPasswordClick = (event?: MouseEvent) => {
/**
* Reject synthetic events (not originating from the user agent)
*/
if (event && !EventSecurity.isEventTrusted(event)) {
return;
}
if (event) {
(event.target as HTMLElement)
.closest(".password-generator-actions")
@@ -403,7 +441,16 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* @param event - The keyup event.
*/
private handleRefreshGeneratedPasswordKeyUp = (event: KeyboardEvent) => {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
/**
* Reject synthetic events (not originating from the user agent)
*/
if (
!EventSecurity.isEventTrusted(event) ||
event.ctrlKey ||
event.altKey ||
event.metaKey ||
event.shiftKey
) {
return;
}
@@ -620,7 +667,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* Handles the click event for the new item button.
* Sends a message to the parent window to add a new vault item.
*/
private handleNewLoginVaultItemAction = () => {
private handleNewLoginVaultItemAction = (event: MouseEvent) => {
/**
* Reject synthetic events (not originating from the user agent)
*/
if (!EventSecurity.isEventTrusted(event)) {
return;
}
let addNewCipherType = this.inlineMenuFillType;
if (this.showInlineMenuAccountCreation) {
@@ -958,7 +1012,16 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
private handleFillCipherClickEvent = (cipher: InlineMenuCipherData) => {
const usePasskey = !!cipher.login?.passkey;
return this.useEventHandlersMemo(
() => this.triggerFillCipherClickEvent(cipher, usePasskey),
(event: Event) => {
/**
* Reject synthetic events (not originating from the user agent)
*/
if (!EventSecurity.isEventTrusted(event)) {
return;
}
this.triggerFillCipherClickEvent(cipher, usePasskey);
},
`${cipher.id}-fill-cipher-button-click-handler-${usePasskey ? "passkey" : ""}`,
);
};
@@ -990,7 +1053,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
*/
private handleFillCipherKeyUpEvent = (event: KeyboardEvent) => {
const listenedForKeys = new Set(["ArrowDown", "ArrowUp", "ArrowRight"]);
if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) {
/**
* Reject synthetic events (not originating from the user agent)
*/
if (
!EventSecurity.isEventTrusted(event) ||
!listenedForKeys.has(event.code) ||
!(event.target instanceof Element)
) {
return;
}
@@ -1018,7 +1088,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
*/
private handleNewItemButtonKeyUpEvent = (event: KeyboardEvent) => {
const listenedForKeys = new Set(["ArrowDown", "ArrowUp"]);
if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) {
/**
* Reject synthetic events (not originating from the user agent)
*/
if (
!EventSecurity.isEventTrusted(event) ||
!listenedForKeys.has(event.code) ||
!(event.target instanceof Element)
) {
return;
}
@@ -1063,11 +1140,16 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* @param cipher - The cipher to view.
*/
private handleViewCipherClickEvent = (cipher: InlineMenuCipherData) => {
return this.useEventHandlersMemo(
() =>
this.postMessageToParent({ command: "viewSelectedCipher", inlineMenuCipherId: cipher.id }),
`${cipher.id}-view-cipher-button-click-handler`,
);
return this.useEventHandlersMemo((event: Event) => {
/**
* Reject synthetic events (not originating from the user agent)
*/
if (!EventSecurity.isEventTrusted(event)) {
return;
}
this.postMessageToParent({ command: "viewSelectedCipher", inlineMenuCipherId: cipher.id });
}, `${cipher.id}-view-cipher-button-click-handler`);
};
/**
@@ -1080,7 +1162,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
*/
private handleViewCipherKeyUpEvent = (event: KeyboardEvent) => {
const listenedForKeys = new Set(["ArrowDown", "ArrowUp", "ArrowLeft"]);
if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) {
/**
* Reject synthetic events (not originating from the user agent)
*/
if (
!EventSecurity.isEventTrusted(event) ||
!listenedForKeys.has(event.code) ||
!(event.target instanceof Element)
) {
return;
}

View File

@@ -1,6 +1,7 @@
import { EVENTS } from "@bitwarden/common/autofill/constants";
import { RedirectFocusDirection } from "../../../../enums/autofill-overlay.enum";
import { EventSecurity } from "../../../../utils/event-security";
import {
AutofillInlineMenuPageElementWindowMessage,
AutofillInlineMenuPageElementWindowMessageHandlers,
@@ -163,7 +164,10 @@ export class AutofillInlineMenuPageElement extends HTMLElement {
*/
private handleDocumentKeyDownEvent = (event: KeyboardEvent) => {
const listenedForKeys = new Set(["Tab", "Escape", "ArrowUp", "ArrowDown"]);
if (!listenedForKeys.has(event.code)) {
/**
* Reject synthetic events (not originating from the user agent)
*/
if (!EventSecurity.isEventTrusted(event) || !listenedForKeys.has(event.code)) {
return;
}

View File

@@ -7,11 +7,7 @@
<div class="tw-bg-background-alt">
<p>
{{
(accountSwitcherEnabled$ | async)
? ("excludedDomainsDescAlt" | i18n)
: ("excludedDomainsDesc" | i18n)
}}
{{ "excludedDomainsDesc" | i18n }}
</p>
<bit-section *ngIf="!isLoading">
<bit-section-header>

View File

@@ -15,7 +15,7 @@ import {
FormArray,
} from "@angular/forms";
import { RouterModule } from "@angular/router";
import { Observable, Subject, takeUntil } from "rxjs";
import { Subject, takeUntil } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
@@ -35,7 +35,6 @@ import {
TypographyModule,
} from "@bitwarden/components";
import { AccountSwitcherService } from "../../../auth/popup/account-switching/services/account-switcher.service";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
@@ -74,8 +73,6 @@ export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy {
@ViewChildren("uriInput") uriInputElements: QueryList<ElementRef<HTMLInputElement>> =
new QueryList();
readonly accountSwitcherEnabled$: Observable<boolean> =
this.accountSwitcherService.accountSwitchingEnabled$();
dataIsPristine = true;
isLoading = false;
excludedDomainsState: string[] = [];
@@ -96,7 +93,6 @@ export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy {
private toastService: ToastService,
private formBuilder: FormBuilder,
private popupRouterCacheService: PopupRouterCacheService,
private accountSwitcherService: AccountSwitcherService,
) {}
get domainForms() {

View File

@@ -23,6 +23,7 @@ import {
sendMockExtensionMessage,
} from "../spec/testing-utils";
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
import { EventSecurity } from "../utils/event-security";
import { AutoFillConstants } from "./autofill-constants";
import { AutofillOverlayContentService } from "./autofill-overlay-content.service";
@@ -55,6 +56,9 @@ describe("AutofillOverlayContentService", () => {
const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall();
beforeEach(async () => {
// Mock EventSecurity to allow synthetic events in tests
jest.spyOn(EventSecurity, "isEventTrusted").mockReturnValue(true);
inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
domQueryService = new DomQueryService();
domElementVisibilityService = new DomElementVisibilityService();
@@ -331,6 +335,8 @@ describe("AutofillOverlayContentService", () => {
pageDetailsMock,
);
jest.spyOn(globalThis.customElements, "define").mockImplementation();
// Mock EventSecurity to allow synthetic events in tests
jest.spyOn(EventSecurity, "isEventTrusted").mockReturnValue(true);
});
it("closes the autofill inline menu when the `Escape` key is pressed", () => {

View File

@@ -45,6 +45,7 @@ import {
sendExtensionMessage,
throttle,
} from "../utils";
import { EventSecurity } from "../utils/event-security";
import {
AutofillOverlayContentExtensionMessageHandlers,
@@ -618,6 +619,10 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
*/
private handleSubmitButtonInteraction = (event: PointerEvent) => {
if (
/**
* Reject synthetic events (not originating from the user agent)
*/
!EventSecurity.isEventTrusted(event) ||
!this.submitElements.has(event.target as HTMLElement) ||
(event.type === "keyup" &&
!["Enter", "Space"].includes((event as unknown as KeyboardEvent).code))
@@ -703,6 +708,13 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
* @param event - The keyup event.
*/
private handleFormFieldKeyupEvent = async (event: globalThis.KeyboardEvent) => {
/**
* Reject synthetic events (not originating from the user agent)
*/
if (!EventSecurity.isEventTrusted(event)) {
return;
}
const eventCode = event.code;
if (eventCode === "Escape") {
void this.sendExtensionMessage("closeAutofillInlineMenu", {

View File

@@ -0,0 +1,26 @@
import { EventSecurity } from "./event-security";
describe("EventSecurity", () => {
describe("isEventTrusted", () => {
it("should call the event.isTrusted property", () => {
const testEvent = new KeyboardEvent("keyup", { code: "Escape" });
const result = EventSecurity.isEventTrusted(testEvent);
// In test environment, events are untrusted by default
expect(result).toBe(false);
expect(result).toBe(testEvent.isTrusted);
});
it("should be mockable with jest.spyOn", () => {
const testEvent = new KeyboardEvent("keyup", { code: "Escape" });
const spy = jest.spyOn(EventSecurity, "isEventTrusted").mockReturnValue(true);
const result = EventSecurity.isEventTrusted(testEvent);
expect(result).toBe(true);
expect(spy).toHaveBeenCalledWith(testEvent);
spy.mockRestore();
});
});
});

View File

@@ -0,0 +1,13 @@
/**
* Event security utilities for validating trusted events
*/
export class EventSecurity {
/**
* Validates that an event is trusted (originated from user agent)
* @param event - The event to validate
* @returns true if the event is trusted, false otherwise
*/
static isEventTrusted(event: Event): boolean {
return event.isTrusted;
}
}

View File

@@ -1,8 +1,6 @@
export type PhishingResource = {
name?: string;
remoteUrl: string;
/** Fallback URL to use if remoteUrl fails (e.g., due to SSL interception/cert issues) */
fallbackUrl: string;
primaryUrl: string;
checksumUrl: string;
todayUrl: string;
/** Matcher used to decide whether a given URL matches an entry from this resource */
@@ -20,8 +18,7 @@ export const PHISHING_RESOURCES: Record<PhishingResourceType, PhishingResource[]
[PhishingResourceType.Domains]: [
{
name: "Phishing.Database Domains",
remoteUrl: "https://phish.co.za/latest/phishing-domains-ACTIVE.txt",
fallbackUrl:
primaryUrl:
"https://raw.githubusercontent.com/Phishing-Database/Phishing.Database/refs/heads/master/phishing-domains-ACTIVE.txt",
checksumUrl:
"https://raw.githubusercontent.com/Phishing-Database/checksums/refs/heads/master/phishing-domains-ACTIVE.txt.md5",
@@ -49,9 +46,7 @@ export const PHISHING_RESOURCES: Record<PhishingResourceType, PhishingResource[]
[PhishingResourceType.Links]: [
{
name: "Phishing.Database Links",
remoteUrl: "https://phish.co.za/latest/phishing-links-ACTIVE.txt",
fallbackUrl:
"https://raw.githubusercontent.com/Phishing-Database/Phishing.Database/refs/heads/master/phishing-links-ACTIVE.txt",
primaryUrl: "https://assets.bitwarden.com/security/v1/link-blocklist.txt",
checksumUrl:
"https://raw.githubusercontent.com/Phishing-Database/checksums/refs/heads/master/phishing-links-ACTIVE.txt.md5",
todayUrl:

View File

@@ -304,12 +304,12 @@ export class PhishingDataService {
private _updateFullDataSet() {
const resource = getPhishingResources(this.resourceType);
if (!resource?.remoteUrl) {
if (!resource?.primaryUrl) {
return throwError(() => new Error("Invalid resource URL"));
}
this.logService.info(`[PhishingDataService] Starting FULL update using ${resource.remoteUrl}`);
return from(this.apiService.nativeFetch(new Request(resource.remoteUrl))).pipe(
this.logService.info(`[PhishingDataService] Starting FULL update using ${resource.primaryUrl}`);
return from(this.apiService.nativeFetch(new Request(resource.primaryUrl))).pipe(
switchMap((response) => {
if (!response.ok || !response.body) {
return throwError(
@@ -322,33 +322,6 @@ export class PhishingDataService {
return from(this.indexedDbService.saveUrlsFromStream(response.body));
}),
catchError((err: unknown) => {
this.logService.error(
`[PhishingDataService] Full dataset update failed using primary source ${err}`,
);
this.logService.warning(
`[PhishingDataService] Falling back to: ${resource.fallbackUrl} (Note: Fallback data may be less up-to-date)`,
);
// Try fallback URL
return from(this.apiService.nativeFetch(new Request(resource.fallbackUrl))).pipe(
switchMap((fallbackResponse) => {
if (!fallbackResponse.ok || !fallbackResponse.body) {
return throwError(
() =>
new Error(
`[PhishingDataService] Fallback fetch failed: ${fallbackResponse.status}, ${fallbackResponse.statusText}`,
),
);
}
return from(this.indexedDbService.saveUrlsFromStream(fallbackResponse.body));
}),
catchError((fallbackError: unknown) => {
this.logService.error(`[PhishingDataService] Fallback source failed`);
return throwError(() => fallbackError);
}),
);
}),
);
}

View File

@@ -27,8 +27,8 @@
{{ button.label | i18n }}
</span>
</button>
<div *ngIf="button.showBerry" class="tw-absolute tw-top-1.5 tw-left-[calc(50%+5px)]">
<div class="tw-bg-notification-600 tw-size-2.5 tw-rounded-full"></div>
<div *ngIf="button.showBerry" class="tw-absolute tw-top-0 tw-left-[calc(50%+5px)]">
<bit-berry type="status" variant="danger"></bit-berry>
</div>
</li>
</ul>

View File

@@ -5,7 +5,7 @@ import { RouterModule } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { BitSvg } from "@bitwarden/assets/svg";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SvgModule, LinkModule } from "@bitwarden/components";
import { SvgModule, LinkModule, BerryComponent } from "@bitwarden/components";
export type NavButton = {
label: string;
@@ -20,7 +20,7 @@ export type NavButton = {
@Component({
selector: "popup-tab-navigation",
templateUrl: "popup-tab-navigation.component.html",
imports: [CommonModule, LinkModule, RouterModule, JslibModule, SvgModule],
imports: [CommonModule, LinkModule, RouterModule, JslibModule, SvgModule, BerryComponent],
host: {
class: "tw-block tw-size-full tw-flex tw-flex-col",
},

View File

@@ -78,13 +78,13 @@ import { ExportBrowserV2Component } from "../tools/popup/settings/export/export-
import { ImportBrowserV2Component } from "../tools/popup/settings/import/import-browser-v2.component";
import { SettingsV2Component } from "../tools/popup/settings/settings-v2.component";
import { AtRiskPasswordsComponent } from "../vault/popup/components/at-risk-passwords/at-risk-passwords.component";
import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component";
import { AssignCollections } from "../vault/popup/components/vault-v2/assign-collections/assign-collections.component";
import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component";
import { IntroCarouselComponent } from "../vault/popup/components/vault-v2/intro-carousel/intro-carousel.component";
import { PasswordHistoryV2Component } from "../vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component";
import { VaultV2Component } from "../vault/popup/components/vault-v2/vault-v2.component";
import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component";
import { AddEditComponent } from "../vault/popup/components/vault/add-edit/add-edit.component";
import { AssignCollections } from "../vault/popup/components/vault/assign-collections/assign-collections.component";
import { AttachmentsComponent } from "../vault/popup/components/vault/attachments/attachments.component";
import { IntroCarouselComponent } from "../vault/popup/components/vault/intro-carousel/intro-carousel.component";
import { PasswordHistoryComponent } from "../vault/popup/components/vault/vault-password-history/vault-password-history.component";
import { VaultComponent } from "../vault/popup/components/vault/vault.component";
import { ViewComponent } from "../vault/popup/components/vault/view/view.component";
import {
atRiskPasswordAuthGuard,
canAccessAtRiskPasswords,
@@ -93,13 +93,13 @@ import {
import { clearVaultStateGuard } from "../vault/popup/guards/clear-vault-state.guard";
import { IntroCarouselGuard } from "../vault/popup/guards/intro-carousel.guard";
import { AdminSettingsComponent } from "../vault/popup/settings/admin-settings.component";
import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component";
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
import { ArchiveComponent } from "../vault/popup/settings/archive.component";
import { DownloadBitwardenComponent } from "../vault/popup/settings/download-bitwarden.component";
import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component";
import { MoreFromBitwardenPageV2Component } from "../vault/popup/settings/more-from-bitwarden-page-v2.component";
import { FoldersComponent } from "../vault/popup/settings/folders.component";
import { MoreFromBitwardenPageComponent } from "../vault/popup/settings/more-from-bitwarden-page.component";
import { TrashComponent } from "../vault/popup/settings/trash.component";
import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component";
import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component";
import { RouteElevation } from "./app-routing.animations";
import {
@@ -214,7 +214,7 @@ const routes: Routes = [
},
{
path: "view-cipher",
component: ViewV2Component,
component: ViewComponent,
canActivate: [authGuard],
data: {
// Above "trash"
@@ -223,20 +223,20 @@ const routes: Routes = [
},
{
path: "cipher-password-history",
component: PasswordHistoryV2Component,
component: PasswordHistoryComponent,
canActivate: [authGuard],
data: { elevation: 4 } satisfies RouteDataProperties,
},
{
path: "add-cipher",
component: AddEditV2Component,
component: AddEditComponent,
canActivate: [authGuard, debounceNavigationGuard()],
data: { elevation: 1, resetRouterCacheOnTabChange: true } satisfies RouteDataProperties,
runGuardsAndResolvers: "always",
},
{
path: "edit-cipher",
component: AddEditV2Component,
component: AddEditComponent,
canActivate: [authGuard, debounceNavigationGuard()],
data: {
// Above "trash"
@@ -247,7 +247,7 @@ const routes: Routes = [
},
{
path: "attachments",
component: AttachmentsV2Component,
component: AttachmentsComponent,
canActivate: [authGuard, filePickerPopoutGuard()],
data: { elevation: 4 } satisfies RouteDataProperties,
},
@@ -301,13 +301,13 @@ const routes: Routes = [
},
{
path: "vault-settings",
component: VaultSettingsV2Component,
component: VaultSettingsComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
{
path: "folders",
component: FoldersV2Component,
component: FoldersComponent,
canActivate: [authGuard],
data: { elevation: 2 } satisfies RouteDataProperties,
},
@@ -331,7 +331,7 @@ const routes: Routes = [
},
{
path: "appearance",
component: AppearanceV2Component,
component: AppearanceComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
@@ -343,7 +343,7 @@ const routes: Routes = [
},
{
path: "clone-cipher",
component: AddEditV2Component,
component: AddEditComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
@@ -635,7 +635,7 @@ const routes: Routes = [
},
{
path: "more-from-bitwarden",
component: MoreFromBitwardenPageV2Component,
component: MoreFromBitwardenPageComponent,
canActivate: [authGuard],
data: { elevation: 2 } satisfies RouteDataProperties,
},
@@ -696,7 +696,7 @@ const routes: Routes = [
},
{
path: "vault",
component: VaultV2Component,
component: VaultComponent,
canActivate: [authGuard],
canDeactivate: [clearVaultStateGuard],
data: { elevation: 0 } satisfies RouteDataProperties,

View File

@@ -1,19 +1,21 @@
<popup-page>
<bit-spotlight *ngIf="!(hasPremium$ | async)" persistent>
<span class="tw-text-xs"
>{{ "unlockFeaturesWithPremium" | i18n }}
<button
bitLink
buttonType="primary"
class="tw-text-xs"
type="button"
(click)="openUpgradeDialog()"
[title]="'upgradeNow' | i18n"
>
{{ "upgradeNow" | i18n }}
</button>
</span>
</bit-spotlight>
@if (!(hasPremium$ | async)) {
<bit-spotlight persistent>
<span class="tw-text-xs"
>{{ "unlockFeaturesWithPremium" | i18n }}
<button
bitLink
buttonType="primary"
class="tw-text-xs"
type="button"
(click)="openUpgradeDialog()"
[title]="'upgradeNow' | i18n"
>
{{ "upgradeNow" | i18n }}
</button>
</span>
</bit-spotlight>
}
<popup-header slot="header" pageTitle="{{ 'settings' | i18n }}">
<ng-container slot="end">
<app-pop-out></app-pop-out>
@@ -23,14 +25,14 @@
<bit-item-group>
<bit-item>
<a bit-item-content routerLink="/account-security">
<a bit-item-content routerLink="/account-security" [truncate]="false">
<i slot="start" class="bwi bwi-lock" aria-hidden="true"></i>
{{ "accountSecurity" | i18n }}
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
<bit-item>
<a bit-item-content routerLink="/autofill">
<a bit-item-content routerLink="/autofill" [truncate]="false">
<i slot="start" class="bwi bwi-check-circle" aria-hidden="true"></i>
<div class="tw-flex tw-items-center tw-justify-center">
<p class="tw-pr-2">{{ "autofill" | i18n }}</p>
@@ -44,7 +46,7 @@
</a>
</bit-item>
<bit-item>
<a bit-item-content routerLink="/notifications">
<a bit-item-content routerLink="/notifications" [truncate]="false">
<i slot="start" class="bwi bwi-file-text" aria-hidden="true"></i>
{{ "notifications" | i18n }}
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
@@ -55,6 +57,7 @@
bit-item-content
routerLink="/vault-settings"
(click)="dismissBadge(NudgeType.EmptyVaultNudge)"
[truncate]="false"
>
<i slot="start" class="bwi bwi-vault" aria-hidden="true"></i>
<div class="tw-flex tw-items-center tw-justify-center">
@@ -63,20 +66,18 @@
Currently can be only 1 item for notification.
Will make this dynamic when more nudges are added
-->
<span
*ngIf="showVaultBadge$ | async"
bitBadge
variant="notification"
[attr.aria-label]="'nudgeBadgeAria' | i18n"
>1</span
>
@if (showVaultBadge$ | async) {
<span bitBadge variant="notification" [attr.aria-label]="'nudgeBadgeAria' | i18n"
>1</span
>
}
</div>
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
<bit-item>
<a bit-item-content routerLink="/appearance">
<a bit-item-content routerLink="/appearance" [truncate]="false">
<i slot="start" class="bwi bwi-brush" aria-hidden="true"></i>
{{ "appearance" | i18n }}
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
@@ -85,7 +86,7 @@
@if (showAdminSettingsLink$ | async) {
<bit-item>
<a bit-item-content routerLink="/admin">
<a bit-item-content routerLink="/admin" [truncate]="false">
<i slot="start" class="bwi bwi-business" aria-hidden="true"></i>
<div class="tw-flex tw-items-center tw-justify-center">
<p class="tw-pr-2">{{ "admin" | i18n }}</p>
@@ -101,30 +102,28 @@
}
<bit-item>
<a bit-item-content routerLink="/about">
<a bit-item-content routerLink="/about" [truncate]="false">
<i slot="start" class="bwi bwi-info-circle" aria-hidden="true"></i>
{{ "about" | i18n }}
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
<bit-item>
<a bit-item-content routerLink="/download-bitwarden">
<a bit-item-content routerLink="/download-bitwarden" [truncate]="false">
<i slot="start" class="bwi bwi-mobile" aria-hidden="true"></i>
<div class="tw-flex tw-items-center">
<p class="tw-pr-2">{{ "downloadBitwardenOnAllDevices" | i18n }}</p>
<span
*ngIf="showDownloadBitwardenNudge$ | async"
bitBadge
variant="notification"
[attr.aria-label]="'nudgeBadgeAria' | i18n"
>1
</span>
<p class="tw-pr-2">{{ "downloadBitwardenApps" | i18n }}</p>
@if (showDownloadBitwardenNudge$ | async) {
<span bitBadge variant="notification" [attr.aria-label]="'nudgeBadgeAria' | i18n"
>1
</span>
}
</div>
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
<bit-item>
<a bit-item-content routerLink="/more-from-bitwarden">
<a bit-item-content routerLink="/more-from-bitwarden" [truncate]="false">
<i slot="start" class="bwi bwi-filter" aria-hidden="true"></i>
{{ "moreFromBitwarden" | i18n }}
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>

View File

@@ -41,24 +41,6 @@
</button>
<ng-container slot="end">
@if (isEditMode) {
@if ((archiveFlagEnabled$ | async) && isCipherArchived) {
<button
type="button"
[bitAction]="unarchive"
bitIconButton="bwi-unarchive"
[label]="'unarchive' | i18n"
></button>
}
@if ((userCanArchive$ | async) && canCipherBeArchived) {
<button
type="button"
[bitAction]="archive"
bitIconButton="bwi-archive"
[label]="'archiveVerb' | i18n"
></button>
}
}
@if (canDeleteCipher$ | async) {
<button
[bitAction]="delete"

View File

@@ -40,16 +40,16 @@ import BrowserPopupUtils from "../../../../../platform/browser/browser-popup-uti
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
import { PopupCloseWarningService } from "../../../../../popup/services/popup-close-warning.service";
import { AddEditV2Component } from "./add-edit-v2.component";
import { AddEditComponent } from "./add-edit.component";
// 'qrcode-parser' is used by `BrowserTotpCaptureService` but is an es6 module that jest can't compile.
// Mock the entire module here to prevent jest from throwing an error. I wasn't able to find a way to mock the
// `BrowserTotpCaptureService` where jest would not load the file in the first place.
jest.mock("qrcode-parser", () => {});
describe("AddEditV2Component", () => {
let component: AddEditV2Component;
let fixture: ComponentFixture<AddEditV2Component>;
describe("AddEditComponent", () => {
let component: AddEditComponent;
let fixture: ComponentFixture<AddEditComponent>;
let addEditCipherInfo$: BehaviorSubject<AddEditCipherInfo | null>;
let cipherServiceMock: MockProxy<CipherService>;
@@ -85,7 +85,7 @@ describe("AddEditV2Component", () => {
});
await TestBed.configureTestingModule({
imports: [AddEditV2Component],
imports: [AddEditComponent],
providers: [
provideNoopAnimations(),
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
@@ -143,7 +143,7 @@ describe("AddEditV2Component", () => {
})
.compileComponents();
fixture = TestBed.createComponent(AddEditV2Component);
fixture = TestBed.createComponent(AddEditComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -443,111 +443,6 @@ describe("AddEditV2Component", () => {
}));
});
describe("archive", () => {
it("calls archiveCipherUtilsService service to archive the cipher", async () => {
buildConfigResponse.originalCipher = { id: "222-333-444-5555", edit: true } as Cipher;
queryParams$.next({ cipherId: "222-333-444-5555" });
await fixture.whenStable();
await component.archive();
expect(component["archiveCipherUtilsService"].archiveCipher).toHaveBeenCalledWith(
expect.objectContaining({ id: "222-333-444-5555" }),
true,
);
});
});
describe("unarchive", () => {
it("calls archiveCipherUtilsService service to unarchive the cipher", async () => {
buildConfigResponse.originalCipher = {
id: "222-333-444-5555",
archivedDate: new Date(),
edit: true,
} as Cipher;
queryParams$.next({ cipherId: "222-333-444-5555" });
await component.unarchive();
expect(component["archiveCipherUtilsService"].unarchiveCipher).toHaveBeenCalledWith(
expect.objectContaining({ id: "222-333-444-5555" }),
);
});
});
describe("archive button", () => {
beforeEach(() => {
// prevent form from rendering
jest.spyOn(component as any, "loading", "get").mockReturnValue(true);
buildConfigResponse.originalCipher = { archivedDate: undefined, edit: true } as Cipher;
});
it("shows the archive button when the user can archive and the cipher can be archived", fakeAsync(() => {
cipherArchiveService.userCanArchive$.mockReturnValue(of(true));
queryParams$.next({ cipherId: "222-333-444-5555" });
tick();
fixture.detectChanges();
const archiveBtn = fixture.debugElement.query(By.css("button[biticonbutton='bwi-archive']"));
expect(archiveBtn).toBeTruthy();
}));
it("does not show the archive button when the user cannot archive", fakeAsync(() => {
cipherArchiveService.userCanArchive$.mockReturnValue(of(false));
queryParams$.next({ cipherId: "222-333-444-5555" });
tick();
fixture.detectChanges();
const archiveBtn = fixture.debugElement.query(By.css("button[biticonbutton='bwi-archive']"));
expect(archiveBtn).toBeFalsy();
}));
it("does not show the archive button when the cipher cannot be archived", fakeAsync(() => {
cipherArchiveService.userCanArchive$.mockReturnValue(of(true));
buildConfigResponse.originalCipher = { archivedDate: new Date(), edit: true } as Cipher;
queryParams$.next({ cipherId: "222-333-444-5555" });
tick();
fixture.detectChanges();
const archiveBtn = fixture.debugElement.query(By.css("button[biticonbutton='bwi-archive']"));
expect(archiveBtn).toBeFalsy();
}));
});
describe("unarchive button", () => {
beforeEach(() => {
// prevent form from rendering
jest.spyOn(component as any, "loading", "get").mockReturnValue(true);
buildConfigResponse.originalCipher = { edit: true } as Cipher;
});
it("shows the unarchive button when the cipher is archived", fakeAsync(() => {
buildConfigResponse.originalCipher = { archivedDate: new Date(), edit: true } as Cipher;
tick();
fixture.detectChanges();
const unarchiveBtn = fixture.debugElement.query(
By.css("button[biticonbutton='bwi-unarchive']"),
);
expect(unarchiveBtn).toBeTruthy();
}));
it("does not show the unarchive button when the cipher is not archived", fakeAsync(() => {
queryParams$.next({ cipherId: "222-333-444-5555" });
tick();
fixture.detectChanges();
const unarchiveBtn = fixture.debugElement.query(
By.css("button[biticonbutton='bwi-unarchive']"),
);
expect(unarchiveBtn).toBeFalsy();
}));
});
describe("delete", () => {
it("dialogService openSimpleDialog called when deleteBtn is hit", async () => {
const dialogSpy = jest

View File

@@ -157,8 +157,8 @@ export type AddEditQueryParams = Partial<Record<keyof QueryParams, string>>;
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "app-add-edit-v2",
templateUrl: "add-edit-v2.component.html",
selector: "app-add-edit",
templateUrl: "add-edit.component.html",
providers: [
{ provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService },
{ provide: TotpCaptureService, useClass: BrowserTotpCaptureService },
@@ -182,7 +182,7 @@ export type AddEditQueryParams = Partial<Record<keyof QueryParams, string>>;
BadgeModule,
],
})
export class AddEditV2Component implements OnInit, OnDestroy {
export class AddEditComponent implements OnInit, OnDestroy {
readonly cipherFormComponent = viewChild(CipherFormComponent);
headerText: string;
config: CipherFormConfig;
@@ -201,14 +201,6 @@ export class AddEditV2Component implements OnInit, OnDestroy {
return new CipherView(this.config?.originalCipher);
}
get canCipherBeArchived(): boolean {
return this.cipher?.canBeArchived;
}
get isCipherArchived(): boolean {
return this.cipher?.isArchived;
}
private fido2PopoutSessionData$ = fido2PopoutSessionData$();
private fido2PopoutSessionData: Fido2SessionData;
@@ -370,10 +362,6 @@ export class AddEditV2Component implements OnInit, OnDestroy {
await BrowserApi.sendMessage("addEditCipherSubmitted");
}
get isEditMode(): boolean {
return ["edit", "partial-edit"].includes(this.config?.mode);
}
subscribeToParams(): void {
this.route.queryParams
.pipe(
@@ -487,40 +475,6 @@ export class AddEditV2Component implements OnInit, OnDestroy {
return this.i18nService.t(translation[type]);
}
/**
* Update the cipher in the form after archiving/unarchiving.
* @param revisionDate The new revision date.
* @param archivedDate The new archived date (null if unarchived).
**/
updateCipherFromArchive = (revisionDate: Date, archivedDate: Date | null) => {
this.cipherFormComponent().patchCipher((current) => {
current.revisionDate = revisionDate;
current.archivedDate = archivedDate;
return current;
});
};
archive = async () => {
const cipherResponse = await this.archiveCipherUtilsService.archiveCipher(this.cipher, true);
if (!cipherResponse) {
return;
}
this.updateCipherFromArchive(
new Date(cipherResponse.revisionDate),
new Date(cipherResponse.archivedDate),
);
};
unarchive = async () => {
const cipherResponse = await this.archiveCipherUtilsService.unarchiveCipher(this.cipher);
if (!cipherResponse) {
return;
}
this.updateCipherFromArchive(new Date(cipherResponse.revisionDate), null);
};
delete = async () => {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "deleteItem" },

View File

@@ -23,7 +23,7 @@ import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup
import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component";
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
import { AttachmentsV2Component } from "./attachments-v2.component";
import { AttachmentsComponent } from "./attachments.component";
@Component({
selector: "popup-header",
@@ -44,9 +44,9 @@ class MockPopupFooterComponent {
readonly pageTitle = input<string>();
}
describe("AttachmentsV2Component", () => {
let component: AttachmentsV2Component;
let fixture: ComponentFixture<AttachmentsV2Component>;
describe("AttachmentsComponent", () => {
let component: AttachmentsComponent;
let fixture: ComponentFixture<AttachmentsComponent>;
const queryParams = new BehaviorSubject<{ cipherId: string }>({ cipherId: "5555-444-3333" });
let cipherAttachment: CipherAttachmentsComponent;
const navigate = jest.fn();
@@ -60,7 +60,7 @@ describe("AttachmentsV2Component", () => {
navigate.mockClear();
await TestBed.configureTestingModule({
imports: [AttachmentsV2Component],
imports: [AttachmentsComponent],
providers: [
{ provide: LogService, useValue: mock<LogService>() },
{ provide: ConfigService, useValue: mock<ConfigService>() },
@@ -83,7 +83,7 @@ describe("AttachmentsV2Component", () => {
{ provide: OrganizationService, useValue: mock<OrganizationService>() },
],
})
.overrideComponent(AttachmentsV2Component, {
.overrideComponent(AttachmentsComponent, {
remove: {
imports: [PopupHeaderComponent, PopupFooterComponent],
},
@@ -95,7 +95,7 @@ describe("AttachmentsV2Component", () => {
});
beforeEach(() => {
fixture = TestBed.createComponent(AttachmentsV2Component);
fixture = TestBed.createComponent(AttachmentsComponent);
component = fixture.componentInstance;
fixture.detectChanges();

View File

@@ -20,8 +20,8 @@ import { PopupRouterCacheService } from "../../../../../platform/popup/view-cach
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "app-attachments-v2",
templateUrl: "./attachments-v2.component.html",
selector: "app-attachments",
templateUrl: "./attachments.component.html",
imports: [
CommonModule,
ButtonModule,
@@ -33,7 +33,7 @@ import { PopupRouterCacheService } from "../../../../../platform/popup/view-cach
PopOutComponent,
],
})
export class AttachmentsV2Component {
export class AttachmentsComponent {
/** The `id` tied to the underlying HTMLFormElement */
attachmentFormId = CipherAttachmentsComponent.attachmentFormID;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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";
@@ -35,7 +35,7 @@ import { PasswordRepromptService } from "@bitwarden/vault";
import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service";
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
import { AddEditQueryParams } from "../add-edit/add-edit.component";
import {
AutofillConfirmationDialogComponent,
AutofillConfirmationDialogResult,
@@ -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$;

View File

@@ -21,13 +21,13 @@ import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitward
import { BrowserApi } from "../../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../../platform/browser/browser-popup-utils";
import { NewItemDropdownV2Component, NewItemInitialValues } from "./new-item-dropdown-v2.component";
import { NewItemDropdownComponent, NewItemInitialValues } from "./new-item-dropdown.component";
describe("NewItemDropdownV2Component", () => {
let component: NewItemDropdownV2Component;
let fixture: ComponentFixture<NewItemDropdownV2Component>;
describe("NewItemDropdownComponent", () => {
let component: NewItemDropdownComponent;
let fixture: ComponentFixture<NewItemDropdownComponent>;
let dialogServiceMock: jest.Mocked<DialogService>;
let browserApiMock: jest.Mocked<typeof BrowserApi>;
const browserApiMock: jest.Mocked<typeof BrowserApi> = mock<typeof BrowserApi>();
let restrictedItemTypesServiceMock: jest.Mocked<RestrictedItemTypesService>;
const mockTab = { url: "https://example.com" };
@@ -62,7 +62,7 @@ describe("NewItemDropdownV2Component", () => {
ButtonModule,
MenuModule,
NoItemsModule,
NewItemDropdownV2Component,
NewItemDropdownComponent,
],
providers: [
{ provide: I18nService, useValue: { t: (key: string) => key } },
@@ -80,7 +80,7 @@ describe("NewItemDropdownV2Component", () => {
});
beforeEach(() => {
fixture = TestBed.createComponent(NewItemDropdownV2Component);
fixture = TestBed.createComponent(NewItemDropdownComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -15,7 +15,7 @@ import { AddEditFolderDialogComponent } from "@bitwarden/vault";
import { BrowserApi } from "../../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../../platform/browser/browser-popup-utils";
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
import { AddEditQueryParams } from "../add-edit/add-edit.component";
export interface NewItemInitialValues {
folderId?: string;
@@ -27,10 +27,10 @@ export interface NewItemInitialValues {
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "app-new-item-dropdown",
templateUrl: "new-item-dropdown-v2.component.html",
templateUrl: "new-item-dropdown.component.html",
imports: [NoItemsModule, JslibModule, CommonModule, ButtonModule, RouterLink, MenuModule],
})
export class NewItemDropdownV2Component implements OnInit {
export class NewItemDropdownComponent implements OnInit {
cipherType = CipherType;
private tab?: chrome.tabs.Tab;
/**

View File

@@ -1,6 +1,6 @@
<div class="tw-flex tw-gap-1 tw-items-center">
<div class="tw-flex-1">
<app-vault-v2-search></app-vault-v2-search>
<app-vault-search></app-vault-search>
</div>
<div class="tw-relative">
<button

View File

@@ -24,18 +24,18 @@ import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault
import { PasswordRepromptService } from "@bitwarden/vault";
import { AutofillService } from "../../../../../autofill/services/abstractions/autofill.service";
import { VaultPopupItemsService } from "../../../../../vault/popup/services/vault-popup-items.service";
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
import {
PopupListFilter,
VaultPopupListFiltersService,
} from "../../../../../vault/popup/services/vault-popup-list-filters.service";
} from "../../../services/vault-popup-list-filters.service";
import { VaultPopupLoadingService } from "../../../services/vault-popup-loading.service";
import { VaultHeaderV2Component } from "./vault-header-v2.component";
import { VaultHeaderComponent } from "./vault-header.component";
describe("VaultHeaderV2Component", () => {
let component: VaultHeaderV2Component;
let fixture: ComponentFixture<VaultHeaderV2Component>;
describe("VaultHeaderComponent", () => {
let component: VaultHeaderComponent;
let fixture: ComponentFixture<VaultHeaderComponent>;
const emptyForm: PopupListFilter = {
organization: null,
@@ -57,7 +57,7 @@ describe("VaultHeaderV2Component", () => {
update.mockClear();
await TestBed.configureTestingModule({
imports: [VaultHeaderV2Component, CommonModule],
imports: [VaultHeaderComponent, CommonModule],
providers: [
{
provide: CipherService,
@@ -112,7 +112,7 @@ describe("VaultHeaderV2Component", () => {
],
}).compileComponents();
fixture = TestBed.createComponent(VaultHeaderV2Component);
fixture = TestBed.createComponent(VaultHeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -13,17 +13,17 @@ import {
} from "@bitwarden/components";
import { runInsideAngular } from "../../../../../platform/browser/run-inside-angular.operator";
import { VaultPopupListFiltersService } from "../../../../../vault/popup/services/vault-popup-list-filters.service";
import { VaultPopupListFiltersService } from "../../../services/vault-popup-list-filters.service";
import { VaultListFiltersComponent } from "../vault-list-filters/vault-list-filters.component";
import { VaultV2SearchComponent } from "../vault-search/vault-v2-search.component";
import { VaultSearchComponent } from "../vault-search/vault-search.component";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "app-vault-header-v2",
templateUrl: "vault-header-v2.component.html",
selector: "app-vault-header",
templateUrl: "vault-header.component.html",
imports: [
VaultV2SearchComponent,
VaultSearchComponent,
VaultListFiltersComponent,
DisclosureComponent,
IconButtonModule,
@@ -32,7 +32,7 @@ import { VaultV2SearchComponent } from "../vault-search/vault-v2-search.componen
JslibModule,
],
})
export class VaultHeaderV2Component {
export class VaultHeaderComponent {
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@ViewChild(DisclosureComponent) disclosure: DisclosureComponent;

View File

@@ -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>

View File

@@ -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");
});
});
});

View File

@@ -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);
}
/**

View File

@@ -16,10 +16,10 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
import { PasswordHistoryV2Component } from "./vault-password-history-v2.component";
import { PasswordHistoryComponent } from "./vault-password-history.component";
describe("PasswordHistoryV2Component", () => {
let fixture: ComponentFixture<PasswordHistoryV2Component>;
describe("PasswordHistoryComponent", () => {
let fixture: ComponentFixture<PasswordHistoryComponent>;
const params$ = new Subject();
const mockUserId = "acct-1" as UserId;
@@ -40,7 +40,7 @@ describe("PasswordHistoryV2Component", () => {
getCipher.mockClear();
await TestBed.configureTestingModule({
imports: [PasswordHistoryV2Component],
imports: [PasswordHistoryComponent],
providers: [
{ provide: WINDOW, useValue: window },
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
@@ -56,7 +56,7 @@ describe("PasswordHistoryV2Component", () => {
],
}).compileComponents();
fixture = TestBed.createComponent(PasswordHistoryV2Component);
fixture = TestBed.createComponent(PasswordHistoryComponent);
fixture.detectChanges();
});

View File

@@ -21,8 +21,8 @@ import { PopupRouterCacheService } from "../../../../../platform/popup/view-cach
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "vault-password-history-v2",
templateUrl: "vault-password-history-v2.component.html",
selector: "vault-password-history",
templateUrl: "vault-password-history.component.html",
imports: [
JslibModule,
PopupPageComponent,
@@ -32,7 +32,7 @@ import { PopupRouterCacheService } from "../../../../../platform/popup/view-cach
NgIf,
],
})
export class PasswordHistoryV2Component implements OnInit {
export class PasswordHistoryComponent implements OnInit {
protected cipher: CipherView;
constructor(

View File

@@ -11,18 +11,18 @@ import { SearchModule } from "@bitwarden/components";
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
import { VaultPopupLoadingService } from "../../../services/vault-popup-loading.service";
import { VaultV2SearchComponent } from "./vault-v2-search.component";
import { VaultSearchComponent } from "./vault-search.component";
describe("VaultV2SearchComponent", () => {
let component: VaultV2SearchComponent;
let fixture: ComponentFixture<VaultV2SearchComponent>;
describe("VaultSearchComponent", () => {
let component: VaultSearchComponent;
let fixture: ComponentFixture<VaultSearchComponent>;
const searchText$ = new BehaviorSubject("");
const loading$ = new BehaviorSubject(false);
const applyFilter = jest.fn();
const createComponent = () => {
fixture = TestBed.createComponent(VaultV2SearchComponent);
fixture = TestBed.createComponent(VaultSearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
};
@@ -31,7 +31,7 @@ describe("VaultV2SearchComponent", () => {
applyFilter.mockClear();
await TestBed.configureTestingModule({
imports: [VaultV2SearchComponent, CommonModule, SearchModule, JslibModule, FormsModule],
imports: [VaultSearchComponent, CommonModule, SearchModule, JslibModule, FormsModule],
providers: [
{
provide: VaultPopupItemsService,

View File

@@ -24,10 +24,10 @@ import { VaultPopupLoadingService } from "../../../services/vault-popup-loading.
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
imports: [CommonModule, SearchModule, JslibModule, FormsModule],
selector: "app-vault-v2-search",
templateUrl: "vault-v2-search.component.html",
selector: "app-vault-search",
templateUrl: "vault-search.component.html",
})
export class VaultV2SearchComponent {
export class VaultSearchComponent {
searchText: string = "";
private searchText$ = new Subject<string>();

View File

@@ -74,7 +74,7 @@
</ul>
</bit-spotlight>
</div>
<app-vault-header-v2></app-vault-header-v2>
<app-vault-header></app-vault-header>
</ng-container>
</ng-container>

View File

@@ -50,10 +50,10 @@ import { AtRiskPasswordCalloutComponent } from "../at-risk-callout/at-risk-passw
import { AutofillVaultListItemsComponent } from "./autofill-vault-list-items/autofill-vault-list-items.component";
import { BlockedInjectionBanner } from "./blocked-injection-banner/blocked-injection-banner.component";
import { NewItemDropdownV2Component } from "./new-item-dropdown/new-item-dropdown-v2.component";
import { VaultHeaderV2Component } from "./vault-header/vault-header-v2.component";
import { NewItemDropdownComponent } from "./new-item-dropdown/new-item-dropdown.component";
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
import { VaultListItemsContainerComponent } from "./vault-list-items-container/vault-list-items-container.component";
import { VaultV2Component } from "./vault-v2.component";
import { VaultComponent } from "./vault.component";
@Component({
selector: "popup-header",
@@ -66,12 +66,12 @@ export class PopupHeaderStubComponent {
}
@Component({
selector: "app-vault-header-v2",
selector: "app-vault-header",
standalone: true,
template: "",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class VaultHeaderV2StubComponent {}
export class VaultHeaderStubComponent {}
@Component({
selector: "app-current-account",
@@ -158,8 +158,8 @@ const autoConfirmDialogSpy = jest
jest.spyOn(BrowserApi, "isPopupOpen").mockResolvedValue(false);
jest.spyOn(BrowserPopupUtils, "openCurrentPagePopout").mockResolvedValue();
describe("VaultV2Component", () => {
let component: VaultV2Component;
describe("VaultComponent", () => {
let component: VaultComponent;
interface FakeAccount {
id: string;
@@ -242,7 +242,7 @@ describe("VaultV2Component", () => {
beforeEach(async () => {
jest.clearAllMocks();
await TestBed.configureTestingModule({
imports: [VaultV2Component, RouterTestingModule],
imports: [VaultComponent, RouterTestingModule],
providers: [
provideNoopAnimations(),
{ provide: VaultPopupItemsService, useValue: itemsSvc },
@@ -298,13 +298,13 @@ describe("VaultV2Component", () => {
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
TestBed.overrideComponent(VaultV2Component, {
TestBed.overrideComponent(VaultComponent, {
remove: {
imports: [
PopupHeaderComponent,
VaultHeaderV2Component,
VaultHeaderComponent,
CurrentAccountComponent,
NewItemDropdownV2Component,
NewItemDropdownComponent,
PopOutComponent,
BlockedInjectionBanner,
AtRiskPasswordCalloutComponent,
@@ -318,7 +318,7 @@ describe("VaultV2Component", () => {
add: {
imports: [
PopupHeaderStubComponent,
VaultHeaderV2StubComponent,
VaultHeaderStubComponent,
CurrentAccountStubComponent,
NewItemDropdownStubComponent,
PopOutStubComponent,
@@ -331,7 +331,7 @@ describe("VaultV2Component", () => {
},
});
const fixture = TestBed.createComponent(VaultV2Component);
const fixture = TestBed.createComponent(VaultComponent);
component = fixture.componentInstance;
});
@@ -393,7 +393,7 @@ describe("VaultV2Component", () => {
});
it("passes popup-page scroll region element to scroll position service", fakeAsync(() => {
const fixture = TestBed.createComponent(VaultV2Component);
const fixture = TestBed.createComponent(VaultComponent);
const component = fixture.componentInstance;
const readySubject$ = component["readySubject"] as unknown as BehaviorSubject<boolean>;
@@ -491,7 +491,7 @@ describe("VaultV2Component", () => {
of(type === NudgeType.PremiumUpgrade),
);
const fixture = TestBed.createComponent(VaultV2Component);
const fixture = TestBed.createComponent(VaultComponent);
const component = fixture.componentInstance;
void component.ngOnInit();
@@ -524,7 +524,7 @@ describe("VaultV2Component", () => {
return of(type === NudgeType.EmptyVaultNudge);
});
const fixture = TestBed.createComponent(VaultV2Component);
const fixture = TestBed.createComponent(VaultComponent);
fixture.detectChanges();
tick();
@@ -541,7 +541,7 @@ describe("VaultV2Component", () => {
return of(type === NudgeType.HasVaultItems);
});
const fixture = TestBed.createComponent(VaultV2Component);
const fixture = TestBed.createComponent(VaultComponent);
fixture.detectChanges();
tick();
@@ -559,7 +559,7 @@ describe("VaultV2Component", () => {
return of(type === NudgeType.PremiumUpgrade);
});
const fixture = TestBed.createComponent(VaultV2Component);
const fixture = TestBed.createComponent(VaultComponent);
fixture.detectChanges();
tick();
@@ -575,7 +575,7 @@ describe("VaultV2Component", () => {
return of(type === NudgeType.PremiumUpgrade);
});
const fixture = TestBed.createComponent(VaultV2Component);
const fixture = TestBed.createComponent(VaultComponent);
fixture.detectChanges();
tick();
@@ -591,7 +591,7 @@ describe("VaultV2Component", () => {
return of(type === NudgeType.PremiumUpgrade);
});
const fixture = TestBed.createComponent(VaultV2Component);
const fixture = TestBed.createComponent(VaultComponent);
fixture.detectChanges();
tick();
@@ -602,7 +602,7 @@ describe("VaultV2Component", () => {
it("does not render app-autofill-vault-list-items or favorites item container when hasSearchText$ is true", () => {
itemsSvc.hasSearchText$.next(true);
const fixture = TestBed.createComponent(VaultV2Component);
const fixture = TestBed.createComponent(VaultComponent);
component = fixture.componentInstance;
const readySubject$ = component["readySubject"];
@@ -628,7 +628,7 @@ describe("VaultV2Component", () => {
itemsSvc.hasSearchText$.next(false);
loadingSvc.loading$.next(false);
const fixture = TestBed.createComponent(VaultV2Component);
const fixture = TestBed.createComponent(VaultComponent);
component = fixture.componentInstance;
const readySubject$ = component["readySubject"];
@@ -655,7 +655,7 @@ describe("VaultV2Component", () => {
filtersSvc.numberOfAppliedFilters$.next(0);
loadingSvc.loading$.next(false);
const fixture = TestBed.createComponent(VaultV2Component);
const fixture = TestBed.createComponent(VaultComponent);
component = fixture.componentInstance;
const readySubject$ = component["readySubject"];
@@ -679,7 +679,7 @@ describe("VaultV2Component", () => {
itemsSvc.hasSearchText$.next(true);
loadingSvc.loading$.next(false);
const fixture = TestBed.createComponent(VaultV2Component);
const fixture = TestBed.createComponent(VaultComponent);
component = fixture.componentInstance;
const readySubject$ = component["readySubject"];
@@ -704,7 +704,7 @@ describe("VaultV2Component", () => {
filtersSvc.numberOfAppliedFilters$.next(1);
loadingSvc.loading$.next(false);
const fixture = TestBed.createComponent(VaultV2Component);
const fixture = TestBed.createComponent(VaultComponent);
component = fixture.componentInstance;
const readySubject$ = component["readySubject"];
@@ -735,7 +735,7 @@ describe("VaultV2Component", () => {
}),
);
const fixture = TestBed.createComponent(VaultV2Component);
const fixture = TestBed.createComponent(VaultComponent);
const component = fixture.componentInstance;
void component.ngOnInit();
@@ -754,7 +754,7 @@ describe("VaultV2Component", () => {
}),
);
const fixture = TestBed.createComponent(VaultV2Component);
const fixture = TestBed.createComponent(VaultComponent);
const component = fixture.componentInstance;
void component.ngOnInit();
@@ -773,7 +773,7 @@ describe("VaultV2Component", () => {
}),
);
const fixture = TestBed.createComponent(VaultV2Component);
const fixture = TestBed.createComponent(VaultComponent);
const component = fixture.componentInstance;
void component.ngOnInit();
@@ -792,7 +792,7 @@ describe("VaultV2Component", () => {
}),
);
const fixture = TestBed.createComponent(VaultV2Component);
const fixture = TestBed.createComponent(VaultComponent);
const component = fixture.componentInstance;
void component.ngOnInit();

View File

@@ -71,10 +71,10 @@ import { VaultLoadingSkeletonComponent } from "../vault-loading-skeleton/vault-l
import { BlockedInjectionBanner } from "./blocked-injection-banner/blocked-injection-banner.component";
import {
NewItemDropdownV2Component,
NewItemDropdownComponent,
NewItemInitialValues,
} from "./new-item-dropdown/new-item-dropdown-v2.component";
import { VaultHeaderV2Component } from "./vault-header/vault-header-v2.component";
} from "./new-item-dropdown/new-item-dropdown.component";
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from ".";
@@ -90,7 +90,7 @@ type VaultState = UnionOfValues<typeof VaultState>;
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "app-vault",
templateUrl: "vault-v2.component.html",
templateUrl: "vault.component.html",
imports: [
BlockedInjectionBanner,
PopupPageComponent,
@@ -103,9 +103,9 @@ type VaultState = UnionOfValues<typeof VaultState>;
AutofillVaultListItemsComponent,
VaultListItemsContainerComponent,
ButtonModule,
NewItemDropdownV2Component,
NewItemDropdownComponent,
ScrollingModule,
VaultHeaderV2Component,
VaultHeaderComponent,
AtRiskPasswordCalloutComponent,
SpotlightComponent,
RouterModule,
@@ -116,7 +116,7 @@ type VaultState = UnionOfValues<typeof VaultState>;
],
providers: [{ provide: VaultItemsTransferService, useClass: DefaultVaultItemsTransferService }],
})
export class VaultV2Component implements OnInit, OnDestroy {
export class VaultComponent implements OnInit, OnDestroy {
NudgeType = NudgeType;
cipherType = CipherType;
private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId);

View File

@@ -45,19 +45,19 @@ import {
import { BrowserApi } from "../../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../../platform/browser/browser-popup-utils";
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
import { VaultPopupScrollPositionService } from "../../../services/vault-popup-scroll-position.service";
import { VaultPopupAutofillService } from "./../../../services/vault-popup-autofill.service";
import { ViewV2Component } from "./view-v2.component";
import { ViewComponent } from "./view.component";
// 'qrcode-parser' is used by `BrowserTotpCaptureService` but is an es6 module that jest can't compile.
// Mock the entire module here to prevent jest from throwing an error. I wasn't able to find a way to mock the
// `BrowserTotpCaptureService` where jest would not load the file in the first place.
jest.mock("qrcode-parser", () => {});
describe("ViewV2Component", () => {
let component: ViewV2Component;
let fixture: ComponentFixture<ViewV2Component>;
describe("ViewComponent", () => {
let component: ViewComponent;
let fixture: ComponentFixture<ViewComponent>;
const params$ = new Subject();
const mockNavigate = jest.fn();
const collect = jest.fn().mockResolvedValue(null);
@@ -124,7 +124,7 @@ describe("ViewV2Component", () => {
cipherArchiveService.unarchiveWithServer.mockResolvedValue({ id: "122-333-444" } as CipherData);
await TestBed.configureTestingModule({
imports: [ViewV2Component],
imports: [ViewComponent],
providers: [
{ provide: Router, useValue: { navigate: mockNavigate } },
{ provide: CipherService, useValue: mockCipherService },
@@ -231,7 +231,7 @@ describe("ViewV2Component", () => {
})
.compileComponents();
fixture = TestBed.createComponent(ViewV2Component);
fixture = TestBed.createComponent(ViewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
(component as any).showFooter$ = of(true);

View File

@@ -56,17 +56,16 @@ import { sendExtensionMessage } from "../../../../../autofill/utils/index";
import { BrowserApi } from "../../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../../platform/browser/browser-popup-utils";
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component";
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service";
import { BrowserViewPasswordHistoryService } from "../../../services/browser-view-password-history.service";
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
import { VaultPopupScrollPositionService } from "../../../services/vault-popup-scroll-position.service";
import { closeViewVaultItemPopout, VaultPopoutType } from "../../../utils/vault-popout-window";
import { ROUTES_AFTER_EDIT_DELETION } from "../add-edit/add-edit-v2.component";
import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "./../../../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "./../../../../../platform/popup/layout/popup-page.component";
import { VaultPopupAutofillService } from "./../../../services/vault-popup-autofill.service";
import { ROUTES_AFTER_EDIT_DELETION } from "../add-edit/add-edit.component";
/**
* The types of actions that can be triggered when loading the view vault item popout via the
@@ -83,8 +82,8 @@ type LoadAction =
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "app-view-v2",
templateUrl: "view-v2.component.html",
selector: "app-view",
templateUrl: "view.component.html",
imports: [
CommonModule,
SearchModule,
@@ -107,7 +106,7 @@ type LoadAction =
{ provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService },
],
})
export class ViewV2Component {
export class ViewComponent {
private activeUserId: UserId;
headerText: string;

View File

@@ -1,7 +1,7 @@
import { TestBed } from "@angular/core/testing";
import { RouterStateSnapshot } from "@angular/router";
import { VaultV2Component } from "../components/vault-v2/vault-v2.component";
import { VaultComponent } from "../components/vault/vault.component";
import { VaultPopupItemsService } from "../services/vault-popup-items.service";
import { VaultPopupListFiltersService } from "../services/vault-popup-list-filters.service";
@@ -42,7 +42,7 @@ describe("clearVaultStateGuard", () => {
const nextState = { url } as RouterStateSnapshot;
const result = TestBed.runInInjectionContext(() =>
clearVaultStateGuard({} as VaultV2Component, null, null, nextState),
clearVaultStateGuard({} as VaultComponent, null, null, nextState),
);
expect(result).toBe(true);
@@ -56,7 +56,7 @@ describe("clearVaultStateGuard", () => {
const nextState = { url } as RouterStateSnapshot;
const result = TestBed.runInInjectionContext(() =>
clearVaultStateGuard({} as VaultV2Component, null, null, nextState),
clearVaultStateGuard({} as VaultComponent, null, null, nextState),
);
expect(result).toBe(true);
@@ -67,7 +67,7 @@ describe("clearVaultStateGuard", () => {
it("should not clear vault state when not changing states", () => {
const result = TestBed.runInInjectionContext(() =>
clearVaultStateGuard({} as VaultV2Component, null, null, null),
clearVaultStateGuard({} as VaultComponent, null, null, null),
);
expect(result).toBe(true);

View File

@@ -1,7 +1,7 @@
import { inject } from "@angular/core";
import { CanDeactivateFn } from "@angular/router";
import { VaultV2Component } from "../components/vault-v2/vault-v2.component";
import { VaultComponent } from "../components/vault/vault.component";
import { VaultPopupItemsService } from "../services/vault-popup-items.service";
import { VaultPopupListFiltersService } from "../services/vault-popup-list-filters.service";
@@ -10,8 +10,8 @@ import { VaultPopupListFiltersService } from "../services/vault-popup-list-filte
* This ensures the search and filter state is reset when navigating between different tabs,
* except viewing or editing a cipher.
*/
export const clearVaultStateGuard: CanDeactivateFn<VaultV2Component> = (
component: VaultV2Component,
export const clearVaultStateGuard: CanDeactivateFn<VaultComponent> = (
component: VaultComponent,
currentRoute,
currentState,
nextState,

View File

@@ -7,7 +7,7 @@ import { firstValueFrom } from "rxjs";
import { DialogService } from "@bitwarden/components";
import { CipherFormGenerationService } from "@bitwarden/vault";
import { VaultGeneratorDialogComponent } from "../components/vault-v2/vault-generator-dialog/vault-generator-dialog.component";
import { VaultGeneratorDialogComponent } from "../components/vault/vault-generator-dialog/vault-generator-dialog.component";
@Injectable()
export class BrowserCipherFormGenerationService implements CipherFormGenerationService {

View File

@@ -69,7 +69,7 @@ describe("VaultPopupItemsService", () => {
const accountServiceMock = mockAccountServiceWith(userId);
const configServiceMock = mock<ConfigService>();
const cipherArchiveServiceMock = mock<CipherArchiveService>();
cipherArchiveServiceMock.userCanArchive$.mockReturnValue(of(true));
cipherArchiveServiceMock.hasArchiveFlagEnabled$ = of(true);
const restrictedItemTypesService = {
restricted$: new BehaviorSubject<RestrictedCipherType[]>([]),

View File

@@ -135,24 +135,23 @@ export class VaultPopupItemsService {
shareReplay({ refCount: true, bufferSize: 1 }),
);
private userCanArchive$ = this.activeUserId$.pipe(
switchMap((userId) => {
return this.cipherArchiveService.userCanArchive$(userId);
}),
);
private _activeCipherList$: Observable<PopupCipherViewLike[]> = this._allDecryptedCiphers$.pipe(
switchMap((ciphers) =>
combineLatest([this.organizations$, this.decryptedCollections$, this.userCanArchive$]).pipe(
map(([organizations, collections, canArchive]) => {
combineLatest([
this.organizations$,
this.decryptedCollections$,
this.cipherArchiveService.hasArchiveFlagEnabled$,
]).pipe(
map(([organizations, collections, archiveFlag]) => {
const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org]));
const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col]));
return ciphers
.filter(
(c) =>
!CipherViewLikeUtils.isDeleted(c) &&
(!canArchive || !CipherViewLikeUtils.isArchived(c)),
(!archiveFlag || !CipherViewLikeUtils.isArchived(c)),
)
.map((cipher) => {
(cipher as PopupCipherViewLike).collections = cipher.collectionIds?.map(
(colId) => collectionMap[colId as CollectionId],

View File

@@ -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>

View File

@@ -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";
@@ -20,7 +22,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
import { PopupSizeService } from "../../../platform/popup/layout/popup-size.service";
import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-buttons.service";
import { AppearanceV2Component } from "./appearance-v2.component";
import { AppearanceComponent } from "./appearance.component";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@@ -49,9 +51,9 @@ class MockPopupPageComponent {
@Input() loading: boolean;
}
describe("AppearanceV2Component", () => {
let component: AppearanceV2Component;
let fixture: ComponentFixture<AppearanceV2Component>;
describe("AppearanceComponent", () => {
let component: AppearanceComponent;
let fixture: ComponentFixture<AppearanceComponent>;
const showFavicons$ = new BehaviorSubject<boolean>(true);
const enableBadgeCounter$ = new BehaviorSubject<boolean>(true);
@@ -59,7 +61,7 @@ describe("AppearanceV2Component", () => {
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("AppearanceV2Component", () => {
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: [AppearanceV2Component],
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,13 +125,13 @@ describe("AppearanceV2Component", () => {
{
provide: VaultSettingsService,
useValue: {
clickItemsToAutofillVaultView$,
clickItemsToAutofillVaultView$: of(false),
setClickItemsToAutofillVaultView,
},
},
],
})
.overrideComponent(AppearanceV2Component, {
.overrideComponent(AppearanceComponent, {
remove: {
imports: [PopupHeaderComponent, PopupPageComponent],
},
@@ -130,7 +141,7 @@ describe("AppearanceV2Component", () => {
})
.compileComponents();
fixture = TestBed.createComponent(AppearanceV2Component);
fixture = TestBed.createComponent(AppearanceComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -193,11 +204,40 @@ describe("AppearanceV2Component", () => {
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();
});
});
});
});

View File

@@ -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";
@@ -36,7 +38,7 @@ import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-butto
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
templateUrl: "./appearance-v2.component.html",
templateUrl: "./appearance.component.html",
imports: [
CommonModule,
JslibModule,
@@ -52,11 +54,18 @@ import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-butto
PermitCipherDetailsPopoverComponent,
],
})
export class AppearanceV2Component implements OnInit {
export class AppearanceComponent implements OnInit {
private compactModeService = inject(PopupCompactModeService);
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,

View File

@@ -42,7 +42,7 @@ import {
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
import { ROUTES_AFTER_EDIT_DELETION } from "../components/vault-v2/add-edit/add-edit-v2.component";
import { ROUTES_AFTER_EDIT_DELETION } from "../components/vault/add-edit/add-edit.component";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection

View File

@@ -19,7 +19,7 @@ import { AddEditFolderDialogComponent } from "@bitwarden/vault";
import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { FoldersV2Component } from "./folders-v2.component";
import { FoldersComponent } from "./folders.component";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@@ -48,9 +48,9 @@ class MockPopupFooterComponent {
@Input() pageTitle: string = "";
}
describe("FoldersV2Component", () => {
let component: FoldersV2Component;
let fixture: ComponentFixture<FoldersV2Component>;
describe("FoldersComponent", () => {
let component: FoldersComponent;
let fixture: ComponentFixture<FoldersComponent>;
const folderViews$ = new BehaviorSubject<FolderView[]>([]);
const open = jest.spyOn(AddEditFolderDialogComponent, "open");
const mockDialogService = { open: jest.fn() };
@@ -59,7 +59,7 @@ describe("FoldersV2Component", () => {
open.mockClear();
await TestBed.configureTestingModule({
imports: [FoldersV2Component],
imports: [FoldersComponent],
providers: [
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
{ provide: ConfigService, useValue: mock<ConfigService>() },
@@ -69,7 +69,7 @@ describe("FoldersV2Component", () => {
{ provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) },
],
})
.overrideComponent(FoldersV2Component, {
.overrideComponent(FoldersComponent, {
remove: {
imports: [PopupHeaderComponent, PopupFooterComponent],
},
@@ -80,7 +80,7 @@ describe("FoldersV2Component", () => {
.overrideProvider(DialogService, { useValue: mockDialogService })
.compileComponents();
fixture = TestBed.createComponent(FoldersV2Component);
fixture = TestBed.createComponent(FoldersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -25,7 +25,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
templateUrl: "./folders-v2.component.html",
templateUrl: "./folders.component.html",
imports: [
CommonModule,
JslibModule,
@@ -39,7 +39,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
AsyncActionsModule,
],
})
export class FoldersV2Component {
export class FoldersComponent {
folders$: Observable<FolderView[]>;
NoFoldersIcon = NoFolders;

View File

@@ -19,7 +19,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
templateUrl: "more-from-bitwarden-page-v2.component.html",
templateUrl: "more-from-bitwarden-page.component.html",
imports: [
CommonModule,
JslibModule,
@@ -30,7 +30,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
ItemModule,
],
})
export class MoreFromBitwardenPageV2Component {
export class MoreFromBitwardenPageComponent {
protected familySponsorshipAvailable$: Observable<boolean>;
protected isFreeFamilyPolicyEnabled$: Observable<boolean>;
protected hasSingleEnterpriseOrg$: Observable<boolean>;

Some files were not shown because too many files have changed in this diff Show More