From 4943e7096554ac5fb0d3a5b4aa38a92d5ee78707 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Fri, 25 Apr 2025 11:16:00 -0400 Subject: [PATCH] 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 --- .../abstractions/notification.background.ts | 11 +++- .../notification.background.spec.ts | 3 + .../background/notification.background.ts | 57 +++++++++++++++++-- .../content/components/common-types.ts | 6 ++ .../components/notification/button-row.ts | 38 ++++++++++++- .../components/notification/container.ts | 5 +- .../content/components/notification/footer.ts | 5 +- .../option-selection/option-selection.ts | 5 +- .../content/components/rows/button-row.ts | 4 +- .../components/signals/selected-collection.ts | 3 + .../components/signals/selected-folder.ts | 3 + .../components/signals/selected-vault.ts | 3 + .../abstractions/notification-bar.ts | 7 ++- apps/browser/src/autofill/notification/bar.ts | 38 +++++++++++-- .../browser/src/background/main.background.ts | 1 + 15 files changed, 169 insertions(+), 20 deletions(-) create mode 100644 apps/browser/src/autofill/content/components/signals/selected-collection.ts create mode 100644 apps/browser/src/autofill/content/components/signals/selected-folder.ts create mode 100644 apps/browser/src/autofill/content/components/signals/selected-vault.ts diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index c93fd9a3acf..db110319d20 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -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; bgGetFolderData: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; + bgGetCollectionData: ({ + message, + sender, + }: BackgroundOnMessageHandlerParams) => Promise; bgCloseNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgOpenAtRisksPasswords: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgAdjustNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; @@ -101,11 +107,14 @@ type NotificationBackgroundExtensionMessageHandlers = { bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgRemoveTabFromNotificationQueue: ({ sender }: BackgroundSenderParam) => void; bgSaveCipher: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + bgOpenAddEditVaultItemPopout: ({ + message, + sender, + }: BackgroundOnMessageHandlerParams) => Promise; bgOpenViewVaultItemPopout: ({ message, sender, }: BackgroundOnMessageHandlerParams) => Promise; - bgOpenVault: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgNeverSave: ({ sender }: BackgroundSenderParam) => Promise; bgUnlockPopoutOpened: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgReopenUnlockPopout: ({ sender }: BackgroundSenderParam) => Promise; diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index bb993fcf94b..63ae1193737 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -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(); const cipherService = mock(); + const collectionService = mock(); let activeAccountStatusMock$: BehaviorSubject; let authService: MockProxy; const policyService = mock(); @@ -83,6 +85,7 @@ describe("NotificationBackground", () => { authService, autofillService, cipherService, + collectionService, configService, domainSettingsService, environmentService, diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 9083f15d4f2..4e2a99d4a7a 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -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 { + const collections = (await this.collectionService.getAllDecrypted()).reduce( + (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 { 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; } diff --git a/apps/browser/src/autofill/content/components/common-types.ts b/apps/browser/src/autofill/content/components/common-types.ts index df11e140d70..591c579bae5 100644 --- a/apps/browser/src/autofill/content/components/common-types.ts +++ b/apps/browser/src/autofill/content/components/common-types.ts @@ -26,3 +26,9 @@ export type OrgView = { name: string; productTierType?: ProductTierType; }; + +export type CollectionView = { + id: string; + name: string; + organizationId: string; +}; diff --git a/apps/browser/src/autofill/content/components/notification/button-row.ts b/apps/browser/src/autofill/content/components/notification/button-row.ts index 3834da4269d..e181a6096f9 100644 --- a/apps/browser/src/autofill/content/components/notification/button-row.ts +++ b/apps/browser/src/autofill/content/components/notification/button-row.ts @@ -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( + (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, }, ] : []), diff --git a/apps/browser/src/autofill/content/components/notification/container.ts b/apps/browser/src/autofill/content/components/notification/container.ts index 5d9399eab70..44264816fe7 100644 --- a/apps/browser/src/autofill/content/components/notification/container.ts +++ b/apps/browser/src/autofill/content/components/notification/container.ts @@ -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, diff --git a/apps/browser/src/autofill/content/components/notification/footer.ts b/apps/browser/src/autofill/content/components/notification/footer.ts index 58a87ebc678..40c3dcecf41 100644 --- a/apps/browser/src/autofill/content/components/notification/footer.ts +++ b/apps/browser/src/autofill/content/components/notification/footer.ts @@ -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({
${!isChangeNotification ? NotificationButtonRow({ + collections, folders, organizations, i18n, diff --git a/apps/browser/src/autofill/content/components/option-selection/option-selection.ts b/apps/browser/src/autofill/content/components/option-selection/option-selection.ts index 5f43e7a0256..49b51852a39 100644 --- a/apps/browser/src/autofill/content/components/option-selection/option-selection.ts +++ b/apps/browser/src/autofill/content/components/option-selection/option-selection.ts @@ -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); }; diff --git a/apps/browser/src/autofill/content/components/rows/button-row.ts b/apps/browser/src/autofill/content/components/rows/button-row.ts index 80dcd0de125..f6674da6b6e 100644 --- a/apps/browser/src/autofill/content/components/rows/button-row.ts +++ b/apps/browser/src/autofill/content/components/rows/button-row.ts @@ -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 })}
${selectButtons?.map( - ({ id, label, options, handleSelectionUpdate }) => + ({ id, label, options, handleSelectionUpdate, selectedSignal }) => html` ` || nothing, )} diff --git a/apps/browser/src/autofill/content/components/signals/selected-collection.ts b/apps/browser/src/autofill/content/components/signals/selected-collection.ts new file mode 100644 index 00000000000..7e6a8d69e3a --- /dev/null +++ b/apps/browser/src/autofill/content/components/signals/selected-collection.ts @@ -0,0 +1,3 @@ +import { signal } from "@lit-labs/signals"; + +export const selectedCollection = signal("0"); diff --git a/apps/browser/src/autofill/content/components/signals/selected-folder.ts b/apps/browser/src/autofill/content/components/signals/selected-folder.ts new file mode 100644 index 00000000000..4c9e30521d5 --- /dev/null +++ b/apps/browser/src/autofill/content/components/signals/selected-folder.ts @@ -0,0 +1,3 @@ +import { signal } from "@lit-labs/signals"; + +export const selectedFolder = signal("0"); diff --git a/apps/browser/src/autofill/content/components/signals/selected-vault.ts b/apps/browser/src/autofill/content/components/signals/selected-vault.ts new file mode 100644 index 00000000000..d74549b1c43 --- /dev/null +++ b/apps/browser/src/autofill/content/components/signals/selected-vault.ts @@ -0,0 +1,3 @@ +import { signal } from "@lit-labs/signals"; + +export const selectedVault = signal("0"); diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index 9dd02b64154..8256190ea55 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -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; diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index a70eb08be0e..14d9bcd6d0f 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -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((resolve) => sendPlatformMessage({ command: "bgGetOrgData" }, resolve), @@ -151,13 +154,18 @@ async function initNotificationBar(message: NotificationBarWindowMessage) { new Promise((resolve) => sendPlatformMessage({ command: "bgGetDecryptedCiphers" }, resolve), ), - ]).then(([organizations, folders, ciphers]) => { + new Promise((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", diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 3066ef5eef5..da47542ee6b 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1206,6 +1206,7 @@ export default class MainBackground { this.authService, this.autofillService, this.cipherService, + this.collectionService, this.configService, this.domainSettingsService, this.environmentService,