From 116751d4caaa024424b508309fcf23cb82532619 Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Thu, 24 Apr 2025 15:34:29 -0400 Subject: [PATCH 1/3] add small button variant (#14326) * adds small button size variant * makes small icon button same size as small button * testing small button for extension header * remove extension changes * update popout layout story * revert change to small icon button padding * add whitespace to see if error resolves * default buttonType to primary * default buttonType to secondary * add comment around why nonNullButtonSize value exists * add comment to property about using the non null version * Update apps/browser/src/platform/popup/layout/popup-layout.stories.ts Co-authored-by: Oscar Hinton * updated input syntax when using static values * remove nonNull value coersion * allow changing of size input in Story --------- Co-authored-by: Oscar Hinton --- .../popup/layout/popup-layout.stories.ts | 2 +- .../components/src/button/button.component.ts | 20 ++++---- libs/components/src/button/button.stories.ts | 47 ++++++++++++++----- .../src/shared/button-like.abstraction.ts | 2 + 4 files changed, 50 insertions(+), 21 deletions(-) diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts index a4c6b894159..32c4f151a8f 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -80,7 +80,7 @@ class VaultComponent { @Component({ selector: "mock-add-button", template: ` - diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index 0b4ce3073c3..19618938c42 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -1,12 +1,10 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { NgClass } from "@angular/common"; -import { Input, HostBinding, Component, model, computed } from "@angular/core"; +import { Input, HostBinding, Component, model, computed, input } from "@angular/core"; import { toObservable, toSignal } from "@angular/core/rxjs-interop"; import { debounce, interval } from "rxjs"; -import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction"; +import { ButtonLikeAbstraction, ButtonType, ButtonSize } from "../shared/button-like.abstraction"; const focusRing = [ "focus-visible:tw-ring-2", @@ -15,6 +13,11 @@ const focusRing = [ "focus-visible:tw-z-10", ]; +const buttonSizeStyles: Record = { + small: ["tw-py-1", "tw-px-3", "tw-text-sm"], + default: ["tw-py-1.5", "tw-px-3"], +}; + const buttonStyles: Record = { primary: [ "tw-border-primary-600", @@ -59,8 +62,6 @@ export class ButtonComponent implements ButtonLikeAbstraction { @HostBinding("class") get classList() { return [ "tw-font-semibold", - "tw-py-1.5", - "tw-px-3", "tw-rounded-full", "tw-transition", "tw-border-2", @@ -85,7 +86,8 @@ export class ButtonComponent implements ButtonLikeAbstraction { "disabled:hover:tw-no-underline", ] : [], - ); + ) + .concat(buttonSizeStyles[this.size() || "default"]); } protected disabledAttr = computed(() => { @@ -105,7 +107,9 @@ export class ButtonComponent implements ButtonLikeAbstraction { return this.showLoadingStyle() || (this.disabledAttr() && this.loading() === false); }); - @Input() buttonType: ButtonType; + @Input() buttonType: ButtonType = "secondary"; + + size = input("default"); private _block = false; diff --git a/libs/components/src/button/button.stories.ts b/libs/components/src/button/button.stories.ts index 448e290cce8..759bd1a352c 100644 --- a/libs/components/src/button/button.stories.ts +++ b/libs/components/src/button/button.stories.ts @@ -9,6 +9,13 @@ export default { buttonType: "primary", disabled: false, loading: false, + size: "default", + }, + argTypes: { + size: { + options: ["small", "default"], + control: { type: "radio" }, + }, }, parameters: { design: { @@ -24,19 +31,19 @@ export const Primary: Story = { render: (args) => ({ props: args, template: /*html*/ ` -
- - - - - +
+ + + + +
-
- Anchor - Anchor:hover - Anchor:focus-visible - Anchor:hover:focus-visible - Anchor:active + `, }), @@ -59,6 +66,22 @@ export const Danger: Story = { }, }; +export const Small: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` +
+ + + +
+ `, + }), + args: { + size: "small", + }, +}; + export const Loading: Story = { render: (args) => ({ props: args, diff --git a/libs/components/src/shared/button-like.abstraction.ts b/libs/components/src/shared/button-like.abstraction.ts index 5ee9d272594..c7cb620bff0 100644 --- a/libs/components/src/shared/button-like.abstraction.ts +++ b/libs/components/src/shared/button-like.abstraction.ts @@ -4,6 +4,8 @@ import { ModelSignal } from "@angular/core"; // @ts-strict-ignore export type ButtonType = "primary" | "secondary" | "danger" | "unstyled"; +export type ButtonSize = "default" | "small"; + export abstract class ButtonLikeAbstraction { loading: ModelSignal; disabled: ModelSignal; From 4a01c8bb171da95746c9d37015098e1164370b0a Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Thu, 24 Apr 2025 16:15:27 -0400 Subject: [PATCH 2/3] PM-20391 UX: Saving new login when none exist (#14406) * PM-20391 UX: Saving new login when none exist * Update apps/browser/src/_locales/en/messages.json Co-authored-by: Jonathan Prusik * Update apps/browser/src/_locales/en/messages.json Co-authored-by: Jonathan Prusik * Update apps/browser/src/autofill/notification/bar.ts Co-authored-by: Jonathan Prusik * Update apps/browser/src/autofill/content/components/cipher/cipher-action.ts Co-authored-by: Jonathan Prusik --------- Co-authored-by: Jonathan Prusik --- apps/browser/src/_locales/en/messages.json | 46 ++++---- .../background/notification.background.ts | 100 +++++++++++++----- .../content/components/buttons/edit-button.ts | 1 + .../components/cipher/cipher-action.ts | 2 +- .../content/components/icons/pencil-square.ts | 2 +- .../components/notification/container.ts | 4 +- apps/browser/src/autofill/notification/bar.ts | 5 +- 7 files changed, 104 insertions(+), 56 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index d1d05a4e852..4f83b07506b 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationEditTooltip": { + "message": "Edit before saving", + "description": "Tooltip and Aria label for edit button on cipher item" + }, "newNotification": { "message": "New notification" }, @@ -1110,12 +1114,12 @@ "message": "Update login", "description": "Button text for updating an existing login entry." }, - "saveLoginPrompt": { - "message": "Save login?", + "saveLogin": { + "message": "Save login", "description": "Prompt asking the user if they want to save their login details." }, - "updateLoginPrompt": { - "message": "Update existing login?", + "updateLogin": { + "message": "Update existing login", "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { @@ -1128,24 +1132,24 @@ }, "loginUpdateTaskSuccess": { "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", - "placeholders": { - "organization": { - "content": "$1" - } - }, - "description": "Shown to user after login is updated." + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." }, "loginUpdateTaskSuccessAdditional": { "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", - "placeholders": { - "organization": { - "content": "$1" - }, - "task_count": { - "content": "$2" - } + "placeholders": { + "organization": { + "content": "$1" }, - "description": "Shown to user after login is updated." + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." }, "nextSecurityTaskAction": { "message": "Change next password", @@ -2518,8 +2522,8 @@ "example": "Acme Corp" }, "count": { - "content": "$2", - "example": "2" + "content": "$2", + "example": "2" } } }, @@ -5224,4 +5228,4 @@ "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." } -} +} \ No newline at end of file diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 0b9c6244987..9083f15d4f2 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -163,6 +163,7 @@ export default class NotificationBackground { * Gets the current active tab and retrieves the relevant decrypted cipher * for the tab's URL. It constructs and returns an array of `NotificationCipherData` objects or a singular object. * If no active tab or URL is found, it returns an empty array. + * If new login, returns a preview of the cipher. * * @returns {Promise} */ @@ -175,53 +176,94 @@ export default class NotificationBackground { firstValueFrom(this.accountService.activeAccount$.pipe(getOptionalUserId)), ]); + if (!currentTab?.url || !activeUserId) { + return []; + } + const [decryptedCiphers, organizations] = await Promise.all([ - this.cipherService.getAllDecryptedForUrl(currentTab?.url, activeUserId), + this.cipherService.getAllDecryptedForUrl(currentTab.url, activeUserId), firstValueFrom(this.organizationService.organizations$(activeUserId)), ]); const iconsServerUrl = env.getIconsUrl(); - const toNotificationData = (view: CipherView): NotificationCipherData => { - const { id, name, reprompt, favorite, login, organizationId } = view; + const getOrganizationType = (orgId?: string) => + organizations.find((org) => org.id === orgId)?.productTierType; - const type = organizations.find((org) => org.id === organizationId)?.productTierType; + const cipherQueueMessage = this.notificationQueue.find( + (message): message is AddChangePasswordQueueMessage | AddLoginQueueMessage => + message.type === NotificationQueueMessageType.ChangePassword || + message.type === NotificationQueueMessageType.AddLogin, + ); - const organizationCategories: OrganizationCategory[] = []; + if (cipherQueueMessage) { + const cipherView = + cipherQueueMessage.type === NotificationQueueMessageType.ChangePassword + ? await this.getDecryptedCipherById(cipherQueueMessage.cipherId, activeUserId) + : this.convertAddLoginQueueMessageToCipherView(cipherQueueMessage); + + const organizationType = getOrganizationType(cipherView.organizationId); + return [ + this.convertToNotificationCipherData( + cipherView, + iconsServerUrl, + showFavicons, + organizationType, + ), + ]; + } + + return decryptedCiphers.map((view) => + this.convertToNotificationCipherData( + view, + iconsServerUrl, + showFavicons, + getOrganizationType(view.organizationId), + ), + ); + } + + /** + * Converts a CipherView and organization type into a NotificationCipherData object + * for use in the notification bar. + * + * @returns A NotificationCipherData object containing the relevant cipher information. + */ + + convertToNotificationCipherData( + view: CipherView, + iconsServerUrl: string, + showFavicons: boolean, + organizationType?: ProductTierType, + ): NotificationCipherData { + const { id, name, reprompt, favorite, login } = view; + + const organizationCategories: OrganizationCategory[] = []; + + if (organizationType != null) { if ( [ProductTierType.Teams, ProductTierType.Enterprise, ProductTierType.TeamsStarter].includes( - type, + organizationType, ) ) { organizationCategories.push(OrganizationCategories.business); } - if ([ProductTierType.Families, ProductTierType.Free].includes(type)) { + + if ([ProductTierType.Families, ProductTierType.Free].includes(organizationType)) { organizationCategories.push(OrganizationCategories.family); } - - return { - id, - name, - type: CipherType.Login, - reprompt, - favorite, - ...(organizationCategories.length ? { organizationCategories } : {}), - icon: buildCipherIcon(iconsServerUrl, view, showFavicons), - login: login && { username: login.username }, - }; - }; - - const changeItem = this.notificationQueue.find( - (message): message is AddChangePasswordQueueMessage => - message.type === NotificationQueueMessageType.ChangePassword, - ); - - if (changeItem) { - const cipherView = await this.getDecryptedCipherById(changeItem.cipherId, activeUserId); - return [toNotificationData(cipherView)]; } - return decryptedCiphers.map(toNotificationData); + return { + id, + name, + type: CipherType.Login, + reprompt, + favorite, + ...(organizationCategories.length ? { organizationCategories } : {}), + icon: buildCipherIcon(iconsServerUrl, view, showFavicons), + login: login && { username: login.username }, + }; } /** diff --git a/apps/browser/src/autofill/content/components/buttons/edit-button.ts b/apps/browser/src/autofill/content/components/buttons/edit-button.ts index 67221f5be18..a0037146db2 100644 --- a/apps/browser/src/autofill/content/components/buttons/edit-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/edit-button.ts @@ -21,6 +21,7 @@ export function EditButton({