1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 05:43:41 +00:00

Pm 19366 handle selections in notification dropdown component to save ciphers appropriately (#14070)

* initial approach for folder selection via queryselect

* handle folder selection with signals

* custom signal when needed on option select to track individual select values

* add vault signal

* initial approach for collection data

* different calls for collections, add collection signal, alter approach

* add appropriate icon for collections dropdown

* populate vault with notification queue

* org id added to extension message type

* clean up naming for upcoming change

* use reduce in getCollections
This commit is contained in:
Daniel Riera
2025-04-25 11:16:00 -04:00
committed by GitHub
parent ab7016fd6b
commit 4943e70965
15 changed files with 169 additions and 20 deletions

View File

@@ -2,6 +2,7 @@ import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { CollectionView } from "../../content/components/common-types";
import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum";
import AutofillPageDetails from "../../models/autofill-page-details";
@@ -83,6 +84,7 @@ type NotificationBackgroundExtensionMessage = {
tab?: chrome.tabs.Tab;
sender?: string;
notificationType?: string;
organizationId?: string;
fadeOutNotification?: boolean;
};
@@ -94,6 +96,10 @@ type NotificationBackgroundExtensionMessageHandlers = {
[key: string]: CallableFunction;
unlockCompleted: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgGetFolderData: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<FolderView[]>;
bgGetCollectionData: ({
message,
sender,
}: BackgroundOnMessageHandlerParams) => Promise<CollectionView[]>;
bgCloseNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgOpenAtRisksPasswords: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgAdjustNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
@@ -101,11 +107,14 @@ type NotificationBackgroundExtensionMessageHandlers = {
bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgRemoveTabFromNotificationQueue: ({ sender }: BackgroundSenderParam) => void;
bgSaveCipher: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
bgOpenAddEditVaultItemPopout: ({
message,
sender,
}: BackgroundOnMessageHandlerParams) => Promise<void>;
bgOpenViewVaultItemPopout: ({
message,
sender,
}: BackgroundOnMessageHandlerParams) => Promise<void>;
bgOpenVault: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgNeverSave: ({ sender }: BackgroundSenderParam) => Promise<void>;
bgUnlockPopoutOpened: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgReopenUnlockPopout: ({ sender }: BackgroundSenderParam) => Promise<void>;

View File

@@ -1,6 +1,7 @@
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, of } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { DefaultPolicyService } from "@bitwarden/common/admin-console/services/policy/default-policy.service";
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -53,6 +54,7 @@ describe("NotificationBackground", () => {
let notificationBackground: NotificationBackground;
const autofillService = mock<AutofillService>();
const cipherService = mock<CipherService>();
const collectionService = mock<CollectionService>();
let activeAccountStatusMock$: BehaviorSubject<AuthenticationStatus>;
let authService: MockProxy<AuthService>;
const policyService = mock<DefaultPolicyService>();
@@ -83,6 +85,7 @@ describe("NotificationBackground", () => {
authService,
autofillService,
cipherService,
collectionService,
configService,
domainSettingsService,
environmentService,

View File

@@ -2,6 +2,7 @@
// @ts-strict-ignore
import { firstValueFrom, switchMap, map, of } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
@@ -50,6 +51,7 @@ import {
OrganizationCategories,
NotificationCipherData,
} from "../content/components/cipher/types";
import { CollectionView } from "../content/components/common-types";
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
import { AutofillService } from "../services/abstractions/autofill.service";
@@ -92,9 +94,11 @@ export default class NotificationBackground {
bgGetEnableAddedLoginPrompt: () => this.getEnableAddedLoginPrompt(),
bgGetExcludedDomains: () => this.getExcludedDomains(),
bgGetFolderData: () => this.getFolderData(),
bgGetCollectionData: ({ message }) => this.getCollectionData(message),
bgGetOrgData: () => this.getOrgData(),
bgNeverSave: ({ sender }) => this.saveNever(sender.tab),
bgOpenVault: ({ message, sender }) => this.openVault(message, sender.tab),
bgOpenAddEditVaultItemPopout: ({ message, sender }) =>
this.openAddEditVaultItem(message, sender.tab),
bgOpenViewVaultItemPopout: ({ message, sender }) => this.viewItem(message, sender.tab),
bgRemoveTabFromNotificationQueue: ({ sender }) =>
this.removeTabFromNotificationQueue(sender.tab),
@@ -114,6 +118,7 @@ export default class NotificationBackground {
private authService: AuthService,
private autofillService: AutofillService,
private cipherService: CipherService,
private collectionService: CollectionService,
private configService: ConfigService,
private domainSettingsService: DomainSettingsService,
private environmentService: EnvironmentService,
@@ -789,17 +794,36 @@ export default class NotificationBackground {
userId,
);
await this.openAddEditVaultItemPopout(senderTab, { cipherId: cipherView.id });
await this.openAddEditVaultItemPopout(senderTab, { cipherId: cipherView?.id });
}
private async openVault(
private async openAddEditVaultItem(
message: NotificationBackgroundExtensionMessage,
senderTab: chrome.tabs.Tab,
) {
if (!message.cipherId) {
await this.openAddEditVaultItemPopout(senderTab);
const { cipherId, organizationId } = message;
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getOptionalUserId));
if (cipherId) {
await this.openAddEditVaultItemPopout(senderTab, { cipherId });
return;
}
await this.openAddEditVaultItemPopout(senderTab, { cipherId: message.cipherId });
const queueItem = this.notificationQueue.find((item) => item.tab.id === senderTab.id);
if (queueItem?.type === NotificationQueueMessageType.AddLogin) {
const cipherView = this.convertAddLoginQueueMessageToCipherView(queueItem);
cipherView.organizationId = organizationId;
if (userId) {
await this.cipherService.setAddEditCipherInfo({ cipher: cipherView }, userId);
}
await this.openAddEditVaultItemPopout(senderTab);
this.removeTabFromNotificationQueue(senderTab);
return;
}
await this.openAddEditVaultItemPopout(senderTab);
}
private async viewItem(
@@ -898,6 +922,25 @@ export default class NotificationBackground {
return await firstValueFrom(this.folderService.folderViews$(activeUserId));
}
private async getCollectionData(
message: NotificationBackgroundExtensionMessage,
): Promise<CollectionView[]> {
const collections = (await this.collectionService.getAllDecrypted()).reduce<CollectionView[]>(
(acc, collection) => {
if (collection.organizationId === message?.orgId) {
acc.push({
id: collection.id,
name: collection.name,
organizationId: collection.organizationId,
});
}
return acc;
},
[],
);
return collections;
}
private async getWebVaultUrl(): Promise<string> {
const env = await firstValueFrom(this.environmentService.environment$);
return env.getWebVaultUrl();
@@ -924,6 +967,7 @@ export default class NotificationBackground {
const organizations = await firstValueFrom(
this.organizationService.organizations$(activeUserId),
);
return organizations.map((org) => {
const { id, name, productTierType } = org;
return {
@@ -1054,6 +1098,7 @@ export default class NotificationBackground {
cipherView.folderId = folderId;
cipherView.type = CipherType.Login;
cipherView.login = loginView;
cipherView.organizationId = null;
return cipherView;
}

View File

@@ -26,3 +26,9 @@ export type OrgView = {
name: string;
productTierType?: ProductTierType;
};
export type CollectionView = {
id: string;
name: string;
organizationId: string;
};

View File

@@ -3,9 +3,12 @@ import { html } from "lit";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { Theme } from "@bitwarden/common/platform/enums";
import { Option, OrgView, FolderView } from "../common-types";
import { Business, Family, Folder, User } from "../icons";
import { Option, OrgView, FolderView, CollectionView } from "../common-types";
import { Business, Family, Folder, User, CollectionShared } from "../icons";
import { ButtonRow } from "../rows/button-row";
import { selectedCollection as selectedCollectionSignal } from "../signals/selected-collection";
import { selectedFolder as selectedFolderSignal } from "../signals/selected-folder";
import { selectedVault as selectedVaultSignal } from "../signals/selected-vault";
function getVaultIconByProductTier(productTierType?: ProductTierType): Option["icon"] {
switch (productTierType) {
@@ -29,11 +32,13 @@ export type NotificationButtonRowProps = {
text: string;
handlePrimaryButtonClick: (args: any) => void;
};
collections?: CollectionView[];
theme: Theme;
};
export function NotificationButtonRow({
folders,
collections,
i18n,
organizations,
primaryButton,
@@ -77,6 +82,21 @@ export function NotificationButtonRow({
)
: [];
const collectionOptions: Option[] = collections?.length
? collections.reduce<Option[]>(
(options, { id, name }: any) => [
...options,
{
icon: CollectionShared,
text: name,
value: id === null ? "0" : id,
default: id === null,
},
],
[],
)
: [];
return html`
${ButtonRow({
theme,
@@ -88,15 +108,27 @@ export function NotificationButtonRow({
id: "organization",
label: i18n.vault,
options: organizationOptions,
selectedSignal: selectedVaultSignal,
},
]
: []),
...(folderOptions.length > 1
...(folderOptions.length > 1 && !collectionOptions.length
? [
{
id: "folder",
label: i18n.folder,
options: folderOptions,
selectedSignal: selectedFolderSignal,
},
]
: []),
...(collectionOptions.length > 1
? [
{
id: "collection",
label: "Collection", // @TODO localize
options: collectionOptions,
selectedSignal: selectedCollectionSignal,
},
]
: []),

View File

@@ -9,7 +9,7 @@ import {
NotificationType,
} from "../../../notification/abstractions/notification-bar";
import { NotificationCipherData } from "../cipher/types";
import { FolderView, OrgView } from "../common-types";
import { CollectionView, FolderView, OrgView } from "../common-types";
import { themes, spacing } from "../constants/styles";
import { NotificationBody, componentClassPrefix as notificationBodyClassPrefix } from "./body";
@@ -25,6 +25,7 @@ export type NotificationContainerProps = NotificationBarIframeInitData & {
handleEditOrUpdateAction: (e: Event) => void;
} & {
ciphers?: NotificationCipherData[];
collections?: CollectionView[];
folders?: FolderView[];
i18n: { [key: string]: string };
organizations?: OrgView[];
@@ -36,6 +37,7 @@ export function NotificationContainer({
handleEditOrUpdateAction,
handleSaveAction,
ciphers,
collections,
folders,
i18n,
organizations,
@@ -64,6 +66,7 @@ export function NotificationContainer({
: null}
${NotificationFooter({
handleSaveAction,
collections,
folders,
i18n,
notificationType: type,

View File

@@ -7,12 +7,13 @@ import {
NotificationType,
NotificationTypes,
} from "../../../notification/abstractions/notification-bar";
import { OrgView, FolderView } from "../common-types";
import { OrgView, FolderView, CollectionView } from "../common-types";
import { spacing, themes } from "../constants/styles";
import { NotificationButtonRow } from "./button-row";
export type NotificationFooterProps = {
collections?: CollectionView[];
folders?: FolderView[];
i18n: { [key: string]: string };
notificationType?: NotificationType;
@@ -22,6 +23,7 @@ export type NotificationFooterProps = {
};
export function NotificationFooter({
collections,
folders,
i18n,
notificationType,
@@ -36,6 +38,7 @@ export function NotificationFooter({
<div class=${notificationFooterStyles({ theme })}>
${!isChangeNotification
? NotificationButtonRow({
collections,
folders,
organizations,
i18n,

View File

@@ -32,6 +32,9 @@ export class OptionSelection extends LitElement {
@property({ type: (selectedOption: Option["value"]) => selectedOption })
handleSelectionUpdate?: (args: any) => void;
@property({ attribute: false })
selectedSignal?: { set: (value: any) => void };
@state()
private showMenu = false;
@@ -77,7 +80,7 @@ export class OptionSelection extends LitElement {
private handleOptionSelection = (selectedOption: Option) => {
this.showMenu = false;
this.selection = selectedOption;
this.selectedSignal?.set(selectedOption.value);
// Any side-effects that should occur from the selection
this.handleSelectionUpdate?.(selectedOption.value);
};

View File

@@ -19,6 +19,7 @@ export type ButtonRowProps = {
label?: string;
options: Option[];
handleSelectionUpdate?: (args: any) => void;
selectedSignal?: { set: (value: any) => void };
}[];
};
@@ -32,7 +33,7 @@ export function ButtonRow({ theme, primaryButton, selectButtons }: ButtonRowProp
})}
<div class=${optionSelectionsStyles}>
${selectButtons?.map(
({ id, label, options, handleSelectionUpdate }) =>
({ id, label, options, handleSelectionUpdate, selectedSignal }) =>
html`
<option-selection
key=${id}
@@ -40,6 +41,7 @@ export function ButtonRow({ theme, primaryButton, selectButtons }: ButtonRowProp
.label=${label}
.options=${options}
.handleSelectionUpdate=${handleSelectionUpdate}
.selectedSignal=${selectedSignal}
></option-selection>
` || nothing,
)}

View File

@@ -0,0 +1,3 @@
import { signal } from "@lit-labs/signals";
export const selectedCollection = signal<string>("0");

View File

@@ -0,0 +1,3 @@
import { signal } from "@lit-labs/signals";
export const selectedFolder = signal<string>("0");

View File

@@ -0,0 +1,3 @@
import { signal } from "@lit-labs/signals";
export const selectedVault = signal<string>("0");

View File

@@ -1,7 +1,11 @@
import { Theme } from "@bitwarden/common/platform/enums";
import { NotificationCipherData } from "../../../autofill/content/components/cipher/types";
import { FolderView, OrgView } from "../../../autofill/content/components/common-types";
import {
FolderView,
OrgView,
CollectionView,
} from "../../../autofill/content/components/common-types";
const NotificationTypes = {
Add: "add",
@@ -19,6 +23,7 @@ type NotificationTaskInfo = {
type NotificationBarIframeInitData = {
ciphers?: NotificationCipherData[];
folders?: FolderView[];
collections?: CollectionView[];
importType?: string;
isVaultLocked?: boolean;
launchTimestamp?: number;

View File

@@ -6,9 +6,11 @@ import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view
import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background";
import { NotificationCipherData } from "../content/components/cipher/types";
import { OrgView } from "../content/components/common-types";
import { CollectionView, OrgView } from "../content/components/common-types";
import { NotificationConfirmationContainer } from "../content/components/notification/confirmation/container";
import { NotificationContainer } from "../content/components/notification/container";
import { selectedFolder as selectedFolderSignal } from "../content/components/signals/selected-folder";
import { selectedVault as selectedVaultSignal } from "../content/components/signals/selected-vault";
import { buildSvgDomElement } from "../utils";
import { circleCheckIcon } from "../utils/svg-icons";
@@ -41,6 +43,7 @@ function load() {
applyNotificationBarStyle();
});
}
function applyNotificationBarStyle() {
if (!useComponentBar) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -140,7 +143,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
document.body.innerHTML = "";
// Current implementations utilize a require for scss files which creates the need to remove the node.
document.head.querySelectorAll('link[rel="stylesheet"]').forEach((node) => node.remove());
const orgId = selectedVaultSignal.get();
await Promise.all([
new Promise<OrgView[]>((resolve) =>
sendPlatformMessage({ command: "bgGetOrgData" }, resolve),
@@ -151,13 +154,18 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
new Promise<NotificationCipherData[]>((resolve) =>
sendPlatformMessage({ command: "bgGetDecryptedCiphers" }, resolve),
),
]).then(([organizations, folders, ciphers]) => {
new Promise<CollectionView[]>((resolve) =>
sendPlatformMessage({ command: "bgGetCollectionData", orgId }, resolve),
),
]).then(([organizations, folders, ciphers, collections]) => {
notificationBarIframeInitData = {
...notificationBarIframeInitData,
organizations,
folders,
ciphers,
organizations,
collections,
};
// @TODO use context to avoid prop drilling
return render(
NotificationContainer({
@@ -254,9 +262,18 @@ function handleCloseNotification(e: Event) {
}
function handleSaveAction(e: Event) {
const selectedVault = selectedVaultSignal.get();
if (selectedVault.length > 1) {
openAddEditVaultItemPopout(e, { organizationId: selectedVault });
handleCloseNotification(e);
return;
}
e.preventDefault();
sendSaveCipherMessage(removeIndividualVault());
const selectedFolder = selectedFolderSignal.get();
sendSaveCipherMessage(removeIndividualVault(), selectedFolder);
if (removeIndividualVault()) {
return;
}
@@ -351,6 +368,17 @@ function handleSaveCipherAttemptCompletedMessage(message: NotificationBarWindowM
);
}
function openAddEditVaultItemPopout(
e: Event,
options: { cipherId?: string; organizationId?: string },
) {
e.preventDefault();
sendPlatformMessage({
command: "bgOpenAddEditVaultItemPopout",
...options,
});
}
function openViewVaultItemPopout(cipherId: string) {
sendPlatformMessage({
command: "bgOpenViewVaultItemPopout",

View File

@@ -1206,6 +1206,7 @@ export default class MainBackground {
this.authService,
this.autofillService,
this.cipherService,
this.collectionService,
this.configService,
this.domainSettingsService,
this.environmentService,