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:
1
.github/workflows/build-desktop.yml
vendored
1
.github/workflows/build-desktop.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/release-cli.yml
vendored
2
.github/workflows/release-cli.yml
vendored
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"]'),
|
||||
];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
| {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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", {
|
||||
|
||||
26
apps/browser/src/autofill/utils/event-security.spec.ts
Normal file
26
apps/browser/src/autofill/utils/event-security.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
13
apps/browser/src/autofill/utils/event-security.ts
Normal file
13
apps/browser/src/autofill/utils/event-security.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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" },
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
[showRefresh]="showRefresh"
|
||||
(onRefresh)="refreshCurrentTab()"
|
||||
[description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : undefined"
|
||||
showAutofillButton
|
||||
isAutofillList
|
||||
[disableDescriptionMargin]="showEmptyAutofillTip$ | async"
|
||||
[primaryActionAutofill]="clickItemsToAutofillVaultView$ | async"
|
||||
[groupByType]="groupByType()"
|
||||
[showAutofillButton]="(clickItemsToAutofillVaultView$ | async) === false"
|
||||
[primaryActionAutofill]="clickItemsToAutofillVaultView$ | async"
|
||||
></app-vault-list-items-container>
|
||||
@@ -8,18 +8,18 @@
|
||||
></button>
|
||||
<bit-menu #moreOptions>
|
||||
@if (!decryptionFailure) {
|
||||
<ng-container *ngIf="canAutofill && !hideAutofillOptions">
|
||||
@if (canAutofill && showAutofill()) {
|
||||
<ng-container *ngIf="autofillAllowed$ | async">
|
||||
<button type="button" bitMenuItem (click)="doAutofill()">
|
||||
{{ "autofill" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showViewOption">
|
||||
}
|
||||
@if (showViewOption()) {
|
||||
<button type="button" bitMenuItem (click)="onView()">
|
||||
{{ "view" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
}
|
||||
<button type="button" bitMenuItem (click)="toggleFavorite()">
|
||||
{{ favoriteText | i18n }}
|
||||
</button>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { booleanAttribute, Component, Input } from "@angular/core";
|
||||
import { booleanAttribute, Component, input, Input } from "@angular/core";
|
||||
import { Router, RouterModule } from "@angular/router";
|
||||
import { BehaviorSubject, combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs";
|
||||
import { filter } from "rxjs/operators";
|
||||
@@ -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$;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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;
|
||||
/**
|
||||
@@ -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
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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;
|
||||
@@ -90,11 +90,11 @@
|
||||
</ng-container>
|
||||
|
||||
<cdk-virtual-scroll-viewport [itemSize]="itemHeight$ | async" bitScrollLayout>
|
||||
<bit-item *cdkVirtualFor="let cipher of group.ciphers">
|
||||
<bit-item *cdkVirtualFor="let cipher of group.ciphers" class="tw-group/vault-item">
|
||||
<button
|
||||
bit-item-content
|
||||
type="button"
|
||||
(click)="primaryActionOnSelect(cipher)"
|
||||
(click)="onCipherSelect(cipher)"
|
||||
(dblclick)="launchCipher(cipher)"
|
||||
[appA11yTitle]="
|
||||
cipherItemTitleKey()(cipher)
|
||||
@@ -125,32 +125,45 @@
|
||||
</button>
|
||||
|
||||
<ng-container slot="end">
|
||||
<bit-item-action *ngIf="!hideAutofillButton()">
|
||||
<button
|
||||
type="button"
|
||||
bitBadge
|
||||
variant="primary"
|
||||
(click)="doAutofill(cipher)"
|
||||
[title]="autofillShortcutTooltip() ?? ('autofillTitle' | i18n: cipher.name)"
|
||||
[attr.aria-label]="'autofillTitle' | i18n: cipher.name"
|
||||
>
|
||||
{{ "fill" | i18n }}
|
||||
</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action *ngIf="!showAutofillButton() && CipherViewLikeUtils.canLaunch(cipher)">
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-external-link"
|
||||
size="small"
|
||||
(click)="launchCipher(cipher)"
|
||||
[label]="'launchWebsiteName' | i18n: cipher.name"
|
||||
></button>
|
||||
</bit-item-action>
|
||||
@if (showFillTextOnHover()) {
|
||||
<bit-item-action>
|
||||
<span
|
||||
class="tw-opacity-0 tw-text-sm tw-text-primary-600 tw-px-2 group-hover/vault-item:tw-opacity-100 group-focus-within/vault-item:tw-opacity-100 tw-cursor-pointer"
|
||||
>
|
||||
{{ "fill" | i18n }}
|
||||
</span>
|
||||
</bit-item-action>
|
||||
}
|
||||
@if (showAutofillBadge()) {
|
||||
<bit-item-action>
|
||||
<button
|
||||
type="button"
|
||||
bitBadge
|
||||
variant="primary"
|
||||
(click)="doAutofill(cipher)"
|
||||
[title]="autofillShortcutTooltip() ?? ('autofillTitle' | i18n: cipher.name)"
|
||||
[attr.aria-label]="'autofillTitle' | i18n: cipher.name"
|
||||
>
|
||||
{{ "fill" | i18n }}
|
||||
</button>
|
||||
</bit-item-action>
|
||||
}
|
||||
@if (showLaunchButton() && CipherViewLikeUtils.canLaunch(cipher)) {
|
||||
<bit-item-action>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-external-link"
|
||||
size="small"
|
||||
(click)="launchCipher(cipher)"
|
||||
[label]="'launchWebsiteName' | i18n: cipher.name"
|
||||
></button>
|
||||
</bit-item-action>
|
||||
}
|
||||
<app-item-copy-actions [cipher]="cipher"></app-item-copy-actions>
|
||||
<app-item-more-options
|
||||
[cipher]="cipher"
|
||||
[hideAutofillOptions]="hideAutofillMenuOptions()"
|
||||
[showViewOption]="primaryActionAutofill()"
|
||||
[showAutofill]="showAutofillInMenu()"
|
||||
[showViewOption]="showViewInMenu()"
|
||||
></app-item-more-options>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
@@ -0,0 +1,332 @@
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { Router } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CompactModeService, DialogService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||
import { VaultPopupSectionService } from "../../../services/vault-popup-section.service";
|
||||
import { PopupCipherViewLike } from "../../../views/popup-cipher.view";
|
||||
|
||||
import { VaultListItemsContainerComponent } from "./vault-list-items-container.component";
|
||||
|
||||
describe("VaultListItemsContainerComponent", () => {
|
||||
let fixture: ComponentFixture<VaultListItemsContainerComponent>;
|
||||
let component: VaultListItemsContainerComponent;
|
||||
|
||||
const featureFlag$ = new BehaviorSubject<boolean>(false);
|
||||
const currentTabIsOnBlocklist$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
const mockCipher = {
|
||||
id: "cipher-1",
|
||||
name: "Test Login",
|
||||
type: CipherType.Login,
|
||||
login: {
|
||||
username: "user@example.com",
|
||||
uris: [{ uri: "https://example.com", match: null }],
|
||||
},
|
||||
favorite: false,
|
||||
reprompt: 0,
|
||||
organizationId: null,
|
||||
collectionIds: [],
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
} as any;
|
||||
|
||||
const configService = {
|
||||
getFeatureFlag$: jest.fn().mockImplementation((flag: FeatureFlag) => {
|
||||
if (flag === FeatureFlag.PM31039ItemActionInExtension) {
|
||||
return featureFlag$.asObservable();
|
||||
}
|
||||
return of(false);
|
||||
}),
|
||||
};
|
||||
|
||||
const vaultPopupAutofillService = {
|
||||
currentTabIsOnBlocklist$: currentTabIsOnBlocklist$.asObservable(),
|
||||
doAutofill: jest.fn(),
|
||||
};
|
||||
|
||||
const compactModeService = {
|
||||
enabled$: of(false),
|
||||
};
|
||||
|
||||
const vaultPopupSectionService = {
|
||||
getOpenDisplayStateForSection: jest.fn().mockReturnValue(() => true),
|
||||
updateSectionOpenStoredState: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
featureFlag$.next(false);
|
||||
currentTabIsOnBlocklist$.next(false);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [VaultListItemsContainerComponent, NoopAnimationsModule],
|
||||
providers: [
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{ provide: VaultPopupAutofillService, useValue: vaultPopupAutofillService },
|
||||
{ provide: CompactModeService, useValue: compactModeService },
|
||||
{ provide: VaultPopupSectionService, useValue: vaultPopupSectionService },
|
||||
{ provide: I18nService, useValue: { t: (k: string) => k } },
|
||||
{ provide: AccountService, useValue: { activeAccount$: of({ id: "UserId" }) } },
|
||||
{ provide: CipherService, useValue: mock<CipherService>() },
|
||||
{ provide: Router, useValue: { navigate: jest.fn() } },
|
||||
{ provide: PlatformUtilsService, useValue: { getAutofillKeyboardShortcut: () => "" } },
|
||||
{ provide: DialogService, useValue: mock<DialogService>() },
|
||||
{ provide: PasswordRepromptService, useValue: mock<PasswordRepromptService>() },
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(VaultListItemsContainerComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
describe("Updated item action feature flag", () => {
|
||||
describe("when feature flag is OFF", () => {
|
||||
beforeEach(() => {
|
||||
featureFlag$.next(false);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should not show fill text on hover", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showFillTextOnHover()).toBe(false);
|
||||
});
|
||||
|
||||
it("should show autofill badge when showAutofillButton is true and primaryActionAutofill is false", () => {
|
||||
fixture.componentRef.setInput("showAutofillButton", true);
|
||||
fixture.componentRef.setInput("primaryActionAutofill", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showAutofillBadge()).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide autofill badge when primaryActionAutofill is true", () => {
|
||||
fixture.componentRef.setInput("showAutofillButton", true);
|
||||
fixture.componentRef.setInput("primaryActionAutofill", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showAutofillBadge()).toBe(false);
|
||||
});
|
||||
|
||||
it("should show launch button when showAutofillButton is false", () => {
|
||||
fixture.componentRef.setInput("showAutofillButton", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showLaunchButton()).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide launch button when showAutofillButton is true", () => {
|
||||
fixture.componentRef.setInput("showAutofillButton", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showLaunchButton()).toBe(false);
|
||||
});
|
||||
|
||||
it("should show autofill in menu when showAutofillButton is false", () => {
|
||||
fixture.componentRef.setInput("showAutofillButton", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showAutofillInMenu()).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide autofill in menu when showAutofillButton is true", () => {
|
||||
fixture.componentRef.setInput("showAutofillButton", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showAutofillInMenu()).toBe(false);
|
||||
});
|
||||
|
||||
it("should show view in menu when primaryActionAutofill is true", () => {
|
||||
fixture.componentRef.setInput("primaryActionAutofill", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showViewInMenu()).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide view in menu when primaryActionAutofill is false", () => {
|
||||
fixture.componentRef.setInput("primaryActionAutofill", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showViewInMenu()).toBe(false);
|
||||
});
|
||||
|
||||
it("should autofill on select when primaryActionAutofill is true", () => {
|
||||
fixture.componentRef.setInput("primaryActionAutofill", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.canAutofill()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not autofill on select when primaryActionAutofill is false", () => {
|
||||
fixture.componentRef.setInput("primaryActionAutofill", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.canAutofill()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when feature flag is ON", () => {
|
||||
beforeEach(() => {
|
||||
featureFlag$.next(true);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should show fill text on hover for autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showFillTextOnHover()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not show fill text on hover for non-autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showFillTextOnHover()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not show autofill badge", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", true);
|
||||
fixture.componentRef.setInput("showAutofillButton", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showAutofillBadge()).toBe(false);
|
||||
});
|
||||
|
||||
it("should hide launch button for autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showLaunchButton()).toBe(false);
|
||||
});
|
||||
|
||||
it("should show launch button for non-autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showLaunchButton()).toBe(true);
|
||||
});
|
||||
|
||||
it("should show autofill in menu for non-autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showAutofillInMenu()).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide autofill in menu for autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showAutofillInMenu()).toBe(false);
|
||||
});
|
||||
|
||||
it("should show view in menu for autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showViewInMenu()).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide view in menu for non-autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showViewInMenu()).toBe(false);
|
||||
});
|
||||
|
||||
it("should autofill on select for autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.canAutofill()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not autofill on select for non-autofill list items", () => {
|
||||
fixture.componentRef.setInput("isAutofillList", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.canAutofill()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when current URI is blocked", () => {
|
||||
beforeEach(() => {
|
||||
currentTabIsOnBlocklist$.next(true);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should not autofill on select even when feature flag is ON and isAutofillList is true", () => {
|
||||
featureFlag$.next(true);
|
||||
fixture.componentRef.setInput("isAutofillList", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.canAutofill()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not autofill on select even when primaryActionAutofill is true", () => {
|
||||
featureFlag$.next(false);
|
||||
fixture.componentRef.setInput("primaryActionAutofill", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.canAutofill()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("cipherItemTitleKey", () => {
|
||||
it("should return autofillTitle when canAutofill is true", () => {
|
||||
featureFlag$.next(true);
|
||||
fixture.componentRef.setInput("isAutofillList", true);
|
||||
fixture.detectChanges();
|
||||
|
||||
const titleKeyFn = component.cipherItemTitleKey();
|
||||
const result = titleKeyFn(mockCipher);
|
||||
|
||||
expect(result).toBe("autofillTitleWithField");
|
||||
});
|
||||
|
||||
it("should return viewItemTitle when canAutofill is false", () => {
|
||||
featureFlag$.next(true);
|
||||
fixture.componentRef.setInput("isAutofillList", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
const titleKeyFn = component.cipherItemTitleKey();
|
||||
const result = titleKeyFn(mockCipher);
|
||||
|
||||
expect(result).toBe("viewItemTitleWithField");
|
||||
});
|
||||
|
||||
it("should return title without WithField when cipher has no username", () => {
|
||||
featureFlag$.next(true);
|
||||
fixture.componentRef.setInput("isAutofillList", false);
|
||||
fixture.detectChanges();
|
||||
|
||||
const cipherWithoutUsername = {
|
||||
...mockCipher,
|
||||
login: { ...mockCipher.login, username: null },
|
||||
} as PopupCipherViewLike;
|
||||
|
||||
const titleKeyFn = component.cipherItemTitleKey();
|
||||
const result = titleKeyFn(cipherWithoutUsername);
|
||||
|
||||
expect(result).toBe("viewItemTitle");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,8 @@ import { firstValueFrom, map } from "rxjs";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
@@ -88,8 +90,15 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
|
||||
export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
private compactModeService = inject(CompactModeService);
|
||||
private vaultPopupSectionService = inject(VaultPopupSectionService);
|
||||
private configService = inject(ConfigService);
|
||||
protected CipherViewLikeUtils = CipherViewLikeUtils;
|
||||
|
||||
/** Signal for the feature flag that controls simplified item action behavior */
|
||||
protected readonly simplifiedItemActionEnabled = toSignal(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM31039ItemActionInExtension),
|
||||
{ initialValue: false },
|
||||
);
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild(CdkVirtualScrollViewport, { static: false }) viewPort!: CdkVirtualScrollViewport;
|
||||
@@ -136,24 +145,18 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
*/
|
||||
private viewCipherTimeout?: number;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
ciphers = input<PopupCipherViewLike[]>([]);
|
||||
readonly ciphers = input<PopupCipherViewLike[]>([]);
|
||||
|
||||
/**
|
||||
* If true, we will group ciphers by type (Login, Card, Identity)
|
||||
* within subheadings in a single container, converted to a WritableSignal.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
groupByType = input<boolean | undefined>(false);
|
||||
readonly groupByType = input<boolean | undefined>(false);
|
||||
|
||||
/**
|
||||
* Computed signal for a grouped list of ciphers with an optional header
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
cipherGroups = computed<
|
||||
readonly cipherGroups = computed<
|
||||
{
|
||||
subHeaderKey?: string;
|
||||
ciphers: PopupCipherViewLike[];
|
||||
@@ -195,9 +198,7 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
/**
|
||||
* Title for the vault list item section.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
title = input<string | undefined>(undefined);
|
||||
readonly title = input<string | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Optionally allow the items to be collapsed.
|
||||
@@ -205,24 +206,20 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
* The key must be added to the state definition in `vault-popup-section.service.ts` since the
|
||||
* collapsed state is stored locally.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
collapsibleKey = input<keyof PopupSectionOpen | undefined>(undefined);
|
||||
readonly collapsibleKey = input<keyof PopupSectionOpen | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Optional description for the vault list item section. Will be shown below the title even when
|
||||
* no ciphers are available.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
description = input<string | undefined>(undefined);
|
||||
|
||||
readonly description = input<string | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Option to show a refresh button in the section header.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
showRefresh = input(false, { transform: booleanAttribute });
|
||||
|
||||
readonly showRefresh = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* Event emitted when the refresh button is clicked.
|
||||
@@ -235,71 +232,124 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
/**
|
||||
* Flag indicating that the current tab location is blocked
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
currentURIIsBlocked = toSignal(this.vaultPopupAutofillService.currentTabIsOnBlocklist$);
|
||||
readonly currentUriIsBlocked = toSignal(this.vaultPopupAutofillService.currentTabIsOnBlocklist$);
|
||||
|
||||
/**
|
||||
* Resolved i18n key to use for suggested cipher items
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
cipherItemTitleKey = computed(() => {
|
||||
readonly cipherItemTitleKey = computed(() => {
|
||||
return (cipher: CipherViewLike) => {
|
||||
const login = CipherViewLikeUtils.getLogin(cipher);
|
||||
const hasUsername = login?.username != null;
|
||||
const key =
|
||||
this.primaryActionAutofill() && !this.currentURIIsBlocked()
|
||||
? "autofillTitle"
|
||||
: "viewItemTitle";
|
||||
// Use autofill title when autofill is the primary action
|
||||
const key = this.canAutofill() ? "autofillTitle" : "viewItemTitle";
|
||||
return hasUsername ? `${key}WithField` : key;
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated - To be removed when PM31039ItemActionInExtension is fully rolled out
|
||||
* Option to show the autofill button for each item.
|
||||
* Used when feature flag is disabled.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
showAutofillButton = input(false, { transform: booleanAttribute });
|
||||
readonly showAutofillButton = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* Flag indicating whether the suggested cipher item autofill button should be shown or not
|
||||
* @deprecated - To be removed when PM31039ItemActionInExtension is fully rolled out
|
||||
* Whether to show the autofill badge button (old behavior).
|
||||
* Only shown when feature flag is disabled AND conditions are met.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
hideAutofillButton = computed(
|
||||
() => !this.showAutofillButton() || this.currentURIIsBlocked() || this.primaryActionAutofill(),
|
||||
readonly showAutofillBadge = computed(
|
||||
() => !this.simplifiedItemActionEnabled() && !this.hideAutofillButton(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Flag indicating whether the cipher item autofill menu options should be shown or not
|
||||
* @deprecated - To be removed when PM31039ItemActionInExtension is fully rolled out
|
||||
* Flag indicating whether the cipher item autofill menu options should be shown or not.
|
||||
* Used when feature flag is disabled.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
hideAutofillMenuOptions = computed(() => this.currentURIIsBlocked() || this.showAutofillButton());
|
||||
readonly hideAutofillMenuOptions = computed(
|
||||
() => this.currentUriIsBlocked() || this.showAutofillButton(),
|
||||
);
|
||||
|
||||
/**
|
||||
* @deprecated - To be removed when PM31039ItemActionInExtension is fully rolled out
|
||||
* Option to perform autofill operation as the primary action for autofill suggestions.
|
||||
* Used when feature flag is disabled.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
primaryActionAutofill = input(false, { transform: booleanAttribute });
|
||||
readonly primaryActionAutofill = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* @deprecated - To be removed when PM31039ItemActionInExtension is fully rolled out
|
||||
* Flag indicating whether the suggested cipher item autofill button should be shown or not.
|
||||
* Used when feature flag is disabled.
|
||||
*/
|
||||
readonly hideAutofillButton = computed(
|
||||
() => !this.showAutofillButton() || this.currentUriIsBlocked() || this.primaryActionAutofill(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Option to mark this container as an autofill list.
|
||||
*/
|
||||
readonly isAutofillList = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* Computed property whether the cipher action may perform autofill.
|
||||
* When feature flag is enabled, uses isAutofillList.
|
||||
* When feature flag is disabled, uses primaryActionAutofill.
|
||||
*/
|
||||
readonly canAutofill = computed(() => {
|
||||
if (this.currentUriIsBlocked()) {
|
||||
return false;
|
||||
}
|
||||
return this.isAutofillList()
|
||||
? this.simplifiedItemActionEnabled()
|
||||
: this.primaryActionAutofill();
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether to show the "Fill" text on hover.
|
||||
* Only shown when feature flag is enabled AND this is an autofill list.
|
||||
*/
|
||||
readonly showFillTextOnHover = computed(
|
||||
() => this.simplifiedItemActionEnabled() && this.canAutofill(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Whether to show the launch button.
|
||||
*/
|
||||
readonly showLaunchButton = computed(() =>
|
||||
this.simplifiedItemActionEnabled() ? !this.isAutofillList() : !this.showAutofillButton(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Whether to show the "Autofill" option in the more options menu.
|
||||
* New behavior: show for non-autofill list items.
|
||||
* Old behavior: show when not hidden by hideAutofillMenuOptions.
|
||||
*/
|
||||
readonly showAutofillInMenu = computed(() =>
|
||||
this.simplifiedItemActionEnabled() ? !this.canAutofill() : !this.hideAutofillMenuOptions(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Whether to show the "View" option in the more options menu.
|
||||
* New behavior: show for autofill list items (since click = autofill).
|
||||
* Old behavior: show when primary action is autofill.
|
||||
*/
|
||||
readonly showViewInMenu = computed(() =>
|
||||
this.simplifiedItemActionEnabled() ? this.isAutofillList() : this.primaryActionAutofill(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Remove the bottom margin from the bit-section in this component
|
||||
* (used for containers at the end of the page where bottom margin is not needed)
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
disableSectionMargin = input(false, { transform: booleanAttribute });
|
||||
readonly disableSectionMargin = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* Remove the description margin
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
disableDescriptionMargin = input(false, { transform: booleanAttribute });
|
||||
readonly disableDescriptionMargin = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* The tooltip text for the organization icon for ciphers that belong to an organization.
|
||||
@@ -313,9 +363,7 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
return collections[0]?.name;
|
||||
}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
protected autofillShortcutTooltip = signal<string | undefined>(undefined);
|
||||
protected readonly autofillShortcutTooltip = signal<string | undefined>(undefined);
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
@@ -340,10 +388,8 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
primaryActionOnSelect(cipher: PopupCipherViewLike) {
|
||||
return this.primaryActionAutofill() && !this.currentURIIsBlocked()
|
||||
? this.doAutofill(cipher)
|
||||
: this.onViewCipher(cipher);
|
||||
onCipherSelect(cipher: PopupCipherViewLike) {
|
||||
return this.canAutofill() ? this.doAutofill(cipher) : this.onViewCipher(cipher);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
@@ -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,
|
||||
@@ -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>();
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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[]>([]),
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -50,16 +50,18 @@
|
||||
<vault-permit-cipher-details-popover></vault-permit-cipher-details-popover>
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<bit-form-control [disableMargin]="simplifiedItemActionEnabled()">
|
||||
<input bitCheckbox formControlName="showQuickCopyActions" type="checkbox" />
|
||||
<bit-label>{{ "showQuickCopyActions" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control disableMargin>
|
||||
<input bitCheckbox formControlName="clickItemsToAutofillVaultView" type="checkbox" />
|
||||
<bit-label>
|
||||
{{ "clickToAutofill" | i18n }}
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
@if (!simplifiedItemActionEnabled()) {
|
||||
<bit-form-control disableMargin>
|
||||
<input bitCheckbox formControlName="clickItemsToAutofillVaultView" type="checkbox" />
|
||||
<bit-label>
|
||||
{{ "clickToAutofill" | i18n }}
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
}
|
||||
</bit-card>
|
||||
</form>
|
||||
</popup-page>
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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
Reference in New Issue
Block a user