1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 00:03:56 +00:00

PM-20396 open view item vault pop out (#14342)

* PM-20396 open view item vault pop out

* add aria and clean up

* format json

* clean naming in messages

* revert feature-flag.enum.ts

* change username to item name

* return nullish operator removed in testing

* update tests to account for itemName

* revert to anchor tag
This commit is contained in:
Daniel Riera
2025-04-23 14:47:09 -04:00
committed by GitHub
parent b589951c90
commit 320d4f65fa
9 changed files with 99 additions and 46 deletions

View File

@@ -192,7 +192,7 @@
"message": "Copy", "message": "Copy",
"description": "Copy to clipboard" "description": "Copy to clipboard"
}, },
"fill":{ "fill": {
"message": "Fill", "message": "Fill",
"description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible."
}, },
@@ -1062,6 +1062,15 @@
"notificationAddSave": { "notificationAddSave": {
"message": "Save" "message": "Save"
}, },
"notificationViewAria": {
"message": "View $ITEMNAME$, opens in new window",
"placeholders": {
"itemName": {
"content": "$1"
}
},
"description": "Aria label for the view button in notification bar confirmation message"
},
"newNotification": { "newNotification": {
"message": "New notification" "message": "New notification"
}, },
@@ -1075,23 +1084,23 @@
} }
} }
}, },
"loginSaveSuccessDetails": { "loginSaveConfirmation": {
"message": "$USERNAME$ saved to Bitwarden.", "message": "$ITEMNAME$ saved to Bitwarden.",
"placeholders": { "placeholders": {
"username": { "itemName": {
"content": "$1" "content": "$1"
} }
}, },
"description": "Shown to user after login is saved." "description": "Shown to user after item is saved."
}, },
"loginUpdatedSuccessDetails": { "loginUpdatedConfirmation": {
"message": "$USERNAME$ updated in Bitwarden.", "message": "$ITEMNAME$ updated in Bitwarden.",
"placeholders": { "placeholders": {
"username": { "itemName": {
"content": "$1" "content": "$1"
} }
}, },
"description": "Shown to user after login is updated." "description": "Shown to user after item is updated."
}, },
"saveAsNewLoginAction": { "saveAsNewLoginAction": {
"message": "Save as new login", "message": "Save as new login",

View File

@@ -101,6 +101,10 @@ type NotificationBackgroundExtensionMessageHandlers = {
bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>; bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgRemoveTabFromNotificationQueue: ({ sender }: BackgroundSenderParam) => void; bgRemoveTabFromNotificationQueue: ({ sender }: BackgroundSenderParam) => void;
bgSaveCipher: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; bgSaveCipher: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
bgOpenViewVaultItemPopout: ({
message,
sender,
}: BackgroundOnMessageHandlerParams) => Promise<void>;
bgOpenVault: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>; bgOpenVault: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgNeverSave: ({ sender }: BackgroundSenderParam) => Promise<void>; bgNeverSave: ({ sender }: BackgroundSenderParam) => Promise<void>;
bgUnlockPopoutOpened: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>; bgUnlockPopoutOpened: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;

View File

@@ -823,6 +823,7 @@ describe("NotificationBackground", () => {
notificationBackground["notificationQueue"] = [queueMessage]; notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>({ const cipherView = mock<CipherView>({
id: "testId", id: "testId",
name: "testItemName",
login: { username: "testUser" }, login: { username: "testUser" },
}); });
getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView); getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView);
@@ -844,8 +845,9 @@ describe("NotificationBackground", () => {
sender.tab, sender.tab,
"saveCipherAttemptCompleted", "saveCipherAttemptCompleted",
{ {
username: cipherView.login.username, itemName: "testItemName",
cipherId: cipherView.id, cipherId: cipherView.id,
task: undefined,
}, },
); );
}); });
@@ -899,7 +901,7 @@ describe("NotificationBackground", () => {
const cipherView = mock<CipherView>({ const cipherView = mock<CipherView>({
id: mockCipherId, id: mockCipherId,
organizationId: mockOrgId, organizationId: mockOrgId,
login: { username: "testUser" }, name: "Test Item",
}); });
getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView); getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView);
@@ -921,11 +923,11 @@ describe("NotificationBackground", () => {
"saveCipherAttemptCompleted", "saveCipherAttemptCompleted",
{ {
cipherId: "testId", cipherId: "testId",
itemName: "Test Item",
task: { task: {
orgName: "Org Name, LLC", orgName: "Org Name, LLC",
remainingTasksCount: 1, remainingTasksCount: 1,
}, },
username: "testUser",
}, },
); );
}); });
@@ -1074,6 +1076,7 @@ describe("NotificationBackground", () => {
notificationBackground["notificationQueue"] = [queueMessage]; notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>({ const cipherView = mock<CipherView>({
id: "testId", id: "testId",
name: "testName",
login: { username: "test", password: "password" }, login: { username: "test", password: "password" },
}); });
folderExistsSpy.mockResolvedValueOnce(false); folderExistsSpy.mockResolvedValueOnce(false);
@@ -1097,8 +1100,8 @@ describe("NotificationBackground", () => {
sender.tab, sender.tab,
"saveCipherAttemptCompleted", "saveCipherAttemptCompleted",
{ {
username: cipherView.login.username,
cipherId: cipherView.id, cipherId: cipherView.id,
itemName: cipherView.name,
}, },
); );
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { command: "addedCipher" }); expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { command: "addedCipher" });

View File

@@ -41,7 +41,10 @@ import { SecurityTask } from "@bitwarden/common/vault/tasks/models/security-task
import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window";
import { BrowserApi } from "../../platform/browser/browser-api"; import { BrowserApi } from "../../platform/browser/browser-api";
import { openAddEditVaultItemPopout } from "../../vault/popup/utils/vault-popout-window"; import {
openAddEditVaultItemPopout,
openViewVaultItemPopout,
} from "../../vault/popup/utils/vault-popout-window";
import { import {
OrganizationCategory, OrganizationCategory,
OrganizationCategories, OrganizationCategories,
@@ -67,6 +70,7 @@ import { OverlayBackgroundExtensionMessage } from "./abstractions/overlay.backgr
export default class NotificationBackground { export default class NotificationBackground {
private openUnlockPopout = openUnlockPopout; private openUnlockPopout = openUnlockPopout;
private openAddEditVaultItemPopout = openAddEditVaultItemPopout; private openAddEditVaultItemPopout = openAddEditVaultItemPopout;
private openViewVaultItemPopout = openViewVaultItemPopout;
private notificationQueue: NotificationQueueMessageItem[] = []; private notificationQueue: NotificationQueueMessageItem[] = [];
private allowedRetryCommands: Set<ExtensionCommandType> = new Set([ private allowedRetryCommands: Set<ExtensionCommandType> = new Set([
ExtensionCommand.AutofillLogin, ExtensionCommand.AutofillLogin,
@@ -91,6 +95,7 @@ export default class NotificationBackground {
bgGetOrgData: () => this.getOrgData(), bgGetOrgData: () => this.getOrgData(),
bgNeverSave: ({ sender }) => this.saveNever(sender.tab), bgNeverSave: ({ sender }) => this.saveNever(sender.tab),
bgOpenVault: ({ message, sender }) => this.openVault(message, sender.tab), bgOpenVault: ({ message, sender }) => this.openVault(message, sender.tab),
bgOpenViewVaultItemPopout: ({ message, sender }) => this.viewItem(message, sender.tab),
bgRemoveTabFromNotificationQueue: ({ sender }) => bgRemoveTabFromNotificationQueue: ({ sender }) =>
this.removeTabFromNotificationQueue(sender.tab), this.removeTabFromNotificationQueue(sender.tab),
bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab), bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab),
@@ -638,8 +643,8 @@ export default class NotificationBackground {
try { try {
await this.cipherService.createWithServer(cipher); await this.cipherService.createWithServer(cipher);
await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", {
username: queueMessage?.username && String(queueMessage.username), itemName: newCipher?.name && String(newCipher?.name),
cipherId: cipher?.id && String(cipher.id), cipherId: cipher?.id && String(cipher?.id),
}); });
await BrowserApi.tabSendMessage(tab, { command: "addedCipher" }); await BrowserApi.tabSendMessage(tab, { command: "addedCipher" });
} catch (error) { } catch (error) {
@@ -701,7 +706,7 @@ export default class NotificationBackground {
await this.cipherService.updateWithServer(cipher); await this.cipherService.updateWithServer(cipher);
await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", {
username: cipherView?.login?.username && String(cipherView.login.username), itemName: cipherView?.name && String(cipherView?.name),
cipherId: cipherView?.id && String(cipherView.id), cipherId: cipherView?.id && String(cipherView.id),
task: taskData, task: taskData,
}); });
@@ -754,6 +759,21 @@ export default class NotificationBackground {
await this.openAddEditVaultItemPopout(senderTab, { cipherId: message.cipherId }); await this.openAddEditVaultItemPopout(senderTab, { cipherId: message.cipherId });
} }
private async viewItem(
message: NotificationBackgroundExtensionMessage,
senderTab: chrome.tabs.Tab,
) {
await Promise.all([
this.openViewVaultItemPopout(senderTab, {
cipherId: message.cipherId,
action: null,
}),
BrowserApi.tabSendMessageData(senderTab, "closeNotificationBar", {
fadeOutNotification: !!message.fadeOutNotification,
}),
]);
}
private async folderExists(folderId: string, userId: UserId) { private async folderExists(folderId: string, userId: UserId) {
if (Utils.isNullOrWhitespace(folderId) || folderId === "null") { if (Utils.isNullOrWhitespace(folderId) || folderId === "null") {
return false; return false;

View File

@@ -16,16 +16,18 @@ const { css } = createEmotion({
export type NotificationConfirmationBodyProps = { export type NotificationConfirmationBodyProps = {
buttonText: string; buttonText: string;
itemName: string;
confirmationMessage: string; confirmationMessage: string;
error?: string; error?: string;
messageDetails?: string; messageDetails?: string;
tasksAreComplete?: boolean; tasksAreComplete?: boolean;
theme: Theme; theme: Theme;
handleOpenVault: (e: Event) => void; handleOpenVault: () => void;
}; };
export function NotificationConfirmationBody({ export function NotificationConfirmationBody({
buttonText, buttonText,
itemName,
confirmationMessage, confirmationMessage,
error, error,
messageDetails, messageDetails,
@@ -43,6 +45,7 @@ export function NotificationConfirmationBody({
${showConfirmationMessage ${showConfirmationMessage
? NotificationConfirmationMessage({ ? NotificationConfirmationMessage({
buttonText, buttonText,
itemName,
message: confirmationMessage, message: confirmationMessage,
messageDetails, messageDetails,
theme, theme,

View File

@@ -20,14 +20,14 @@ import { NotificationConfirmationFooter } from "./footer";
export type NotificationConfirmationContainerProps = NotificationBarIframeInitData & { export type NotificationConfirmationContainerProps = NotificationBarIframeInitData & {
handleCloseNotification: (e: Event) => void; handleCloseNotification: (e: Event) => void;
handleOpenVault: (e: Event) => void; handleOpenVault: () => void;
handleOpenTasks: (e: Event) => void; handleOpenTasks: (e: Event) => void;
} & { } & {
error?: string; error?: string;
i18n: { [key: string]: string }; i18n: { [key: string]: string };
itemName: string;
task?: NotificationTaskInfo; task?: NotificationTaskInfo;
type: NotificationType; type: NotificationType;
username: string;
}; };
export function NotificationConfirmationContainer({ export function NotificationConfirmationContainer({
@@ -36,13 +36,13 @@ export function NotificationConfirmationContainer({
handleOpenVault, handleOpenVault,
handleOpenTasks, handleOpenTasks,
i18n, i18n,
itemName,
task, task,
theme = ThemeTypes.Light, theme = ThemeTypes.Light,
type, type,
username,
}: NotificationConfirmationContainerProps) { }: NotificationConfirmationContainerProps) {
const headerMessage = getHeaderMessage(i18n, type, error); const headerMessage = getHeaderMessage(i18n, type, error);
const confirmationMessage = getConfirmationMessage(i18n, username, type, error); const confirmationMessage = getConfirmationMessage(i18n, itemName, type, error);
const buttonText = error ? i18n.newItem : i18n.view; const buttonText = error ? i18n.newItem : i18n.view;
let messageDetails: string | undefined; let messageDetails: string | undefined;
@@ -71,6 +71,7 @@ export function NotificationConfirmationContainer({
})} })}
${NotificationConfirmationBody({ ${NotificationConfirmationBody({
buttonText, buttonText,
itemName,
confirmationMessage, confirmationMessage,
tasksAreComplete, tasksAreComplete,
messageDetails, messageDetails,
@@ -106,19 +107,17 @@ const notificationContainerStyles = (theme: Theme) => css`
function getConfirmationMessage( function getConfirmationMessage(
i18n: { [key: string]: string }, i18n: { [key: string]: string },
username: string, itemName: string,
type?: NotificationType, type?: NotificationType,
error?: string, error?: string,
) { ) {
const loginSaveSuccessDetails = chrome.i18n.getMessage("loginSaveSuccessDetails", [username]); const loginSaveConfirmation = chrome.i18n.getMessage("loginSaveConfirmation", [itemName]);
const loginUpdatedSuccessDetails = chrome.i18n.getMessage("loginUpdatedSuccessDetails", [ const loginUpdatedConfirmation = chrome.i18n.getMessage("loginUpdatedConfirmation", [itemName]);
username,
]);
if (error) { if (error) {
return i18n.saveFailureDetails; return i18n.saveFailureDetails;
} }
return type === "add" ? loginSaveSuccessDetails : loginUpdatedSuccessDetails; return type === "add" ? loginSaveConfirmation : loginUpdatedConfirmation;
} }
function getHeaderMessage( function getHeaderMessage(

View File

@@ -7,19 +7,23 @@ import { themes, typography } from "../../constants/styles";
export type NotificationConfirmationMessageProps = { export type NotificationConfirmationMessageProps = {
buttonText?: string; buttonText?: string;
itemName: string;
message?: string; message?: string;
messageDetails?: string; messageDetails?: string;
handleClick: (e: Event) => void; handleClick: () => void;
theme: Theme; theme: Theme;
}; };
export function NotificationConfirmationMessage({ export function NotificationConfirmationMessage({
buttonText, buttonText,
itemName,
message, message,
messageDetails, messageDetails,
handleClick, handleClick,
theme, theme,
}: NotificationConfirmationMessageProps) { }: NotificationConfirmationMessageProps) {
const buttonAria = chrome.i18n.getMessage("notificationViewAria", [itemName]);
return html` return html`
<div> <div>
${message || buttonText ${message || buttonText
@@ -35,6 +39,10 @@ export function NotificationConfirmationMessage({
title=${buttonText} title=${buttonText}
class=${notificationConfirmationButtonTextStyles(theme)} class=${notificationConfirmationButtonTextStyles(theme)}
@click=${handleClick} @click=${handleClick}
@keydown=${(e: KeyboardEvent) => handleButtonKeyDown(e, handleClick)}
aria-label=${buttonAria}
tabindex="0"
role="button"
> >
${buttonText} ${buttonText}
</a> </a>
@@ -81,3 +89,10 @@ const AdditionalMessageStyles = ({ theme }: { theme: Theme }) => css`
font-size: 14px; font-size: 14px;
color: ${themes[theme].text.muted}; color: ${themes[theme].text.muted};
`; `;
function handleButtonKeyDown(event: KeyboardEvent, handleClick: () => void) {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
handleClick();
}
}

View File

@@ -33,7 +33,7 @@ type NotificationBarWindowMessage = {
data?: { data?: {
cipherId?: string; cipherId?: string;
task?: NotificationTaskInfo; task?: NotificationTaskInfo;
username?: string; itemName?: string;
}; };
error?: string; error?: string;
initData?: NotificationBarIframeInitData; initData?: NotificationBarIframeInitData;

View File

@@ -56,9 +56,9 @@ function getI18n() {
collection: chrome.i18n.getMessage("collection"), collection: chrome.i18n.getMessage("collection"),
folder: chrome.i18n.getMessage("folder"), folder: chrome.i18n.getMessage("folder"),
loginSaveSuccess: chrome.i18n.getMessage("loginSaveSuccess"), loginSaveSuccess: chrome.i18n.getMessage("loginSaveSuccess"),
loginSaveSuccessDetails: chrome.i18n.getMessage("loginSaveSuccessDetails"), loginSaveConfirmation: chrome.i18n.getMessage("loginSaveConfirmation"),
loginUpdateSuccess: chrome.i18n.getMessage("loginUpdateSuccess"), loginUpdateSuccess: chrome.i18n.getMessage("loginUpdateSuccess"),
loginUpdateSuccessDetails: chrome.i18n.getMessage("loginUpdatedSuccessDetails"), loginUpdateConfirmation: chrome.i18n.getMessage("loginUpdatedConfirmation"),
loginUpdateTaskSuccess: chrome.i18n.getMessage("loginUpdateTaskSuccess"), loginUpdateTaskSuccess: chrome.i18n.getMessage("loginUpdateTaskSuccess"),
loginUpdateTaskSuccessAdditional: chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional"), loginUpdateTaskSuccessAdditional: chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional"),
nextSecurityTaskAction: chrome.i18n.getMessage("nextSecurityTaskAction"), nextSecurityTaskAction: chrome.i18n.getMessage("nextSecurityTaskAction"),
@@ -72,6 +72,7 @@ function getI18n() {
notificationEdit: chrome.i18n.getMessage("edit"), notificationEdit: chrome.i18n.getMessage("edit"),
notificationUnlock: chrome.i18n.getMessage("notificationUnlock"), notificationUnlock: chrome.i18n.getMessage("notificationUnlock"),
notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"), notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"),
notificationViewAria: chrome.i18n.getMessage("notificationViewAria"),
saveAction: chrome.i18n.getMessage("notificationAddSave"), saveAction: chrome.i18n.getMessage("notificationAddSave"),
saveAsNewLoginAction: chrome.i18n.getMessage("saveAsNewLoginAction"), saveAsNewLoginAction: chrome.i18n.getMessage("saveAsNewLoginAction"),
saveFailure: chrome.i18n.getMessage("saveFailure"), saveFailure: chrome.i18n.getMessage("saveFailure"),
@@ -349,10 +350,9 @@ function handleSaveCipherAttemptCompletedMessage(message: NotificationBarWindowM
); );
} }
function openViewVaultItemPopout(e: Event, cipherId: string) { function openViewVaultItemPopout(cipherId: string) {
e.preventDefault();
sendPlatformMessage({ sendPlatformMessage({
command: "bgOpenVault", command: "bgOpenViewVaultItemPopout",
cipherId, cipherId,
}); });
} }
@@ -360,7 +360,7 @@ function openViewVaultItemPopout(e: Event, cipherId: string) {
function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) { function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) {
const { theme, type } = notificationBarIframeInitData; const { theme, type } = notificationBarIframeInitData;
const { error, data } = message; const { error, data } = message;
const { username, cipherId, task } = data || {}; const { cipherId, task, itemName } = data || {};
const i18n = getI18n(); const i18n = getI18n();
const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light); const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light);
@@ -374,9 +374,9 @@ function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) {
handleCloseNotification, handleCloseNotification,
i18n, i18n,
error, error,
username: username ?? i18n.typeLogin, itemName: itemName ?? i18n.typeLogin,
task, task,
handleOpenVault: (e) => cipherId && openViewVaultItemPopout(e, cipherId), handleOpenVault: () => cipherId && openViewVaultItemPopout(cipherId),
handleOpenTasks: () => sendPlatformMessage({ command: "bgOpenAtRisksPasswords" }), handleOpenTasks: () => sendPlatformMessage({ command: "bgOpenAtRisksPasswords" }),
}), }),
document.body, document.body,