1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 13:23:34 +00:00

[PM-21791] Nudge UI Bug Fixes (#15010)

* remove margin bottom from empty vault nudge

* update page title to vault options

* show badge on import of vault settings

* add margin between no items title and icon

* add mock to test

* add comment for destroying vault settings page

* fix logic for manage/create collection

* account for deleted ciphers when showing the import nudge

* refactor name of vault import nudge
This commit is contained in:
Nick Krantz
2025-06-04 08:07:44 -05:00
committed by GitHub
parent dcd6f7ada8
commit 65f4ff6909
8 changed files with 120 additions and 7 deletions

View File

@@ -46,7 +46,7 @@
[title]="'hasItemsVaultNudgeTitle' | i18n" [title]="'hasItemsVaultNudgeTitle' | i18n"
(onDismiss)="dismissVaultNudgeSpotlight(NudgeType.HasVaultItems)" (onDismiss)="dismissVaultNudgeSpotlight(NudgeType.HasVaultItems)"
> >
<ul class="tw-pl-4 tw-text-main" bitTypography="body2"> <ul class="tw-pl-4 tw-text-main tw-mb-0" bitTypography="body2">
<li>{{ "hasItemsVaultNudgeBodyOne" | i18n }}</li> <li>{{ "hasItemsVaultNudgeBodyOne" | i18n }}</li>
<li>{{ "hasItemsVaultNudgeBodyTwo" | i18n }}</li> <li>{{ "hasItemsVaultNudgeBodyTwo" | i18n }}</li>
<li>{{ "hasItemsVaultNudgeBodyThree" | i18n }}</li> <li>{{ "hasItemsVaultNudgeBodyThree" | i18n }}</li>

View File

@@ -1,5 +1,5 @@
<popup-page> <popup-page>
<popup-header slot="header" [pageTitle]="'vault' | i18n" showBackButton> <popup-header slot="header" [pageTitle]="'settingsVaultOptions' | i18n" showBackButton>
<ng-container slot="end"> <ng-container slot="end">
<app-pop-out></app-pop-out> <app-pop-out></app-pop-out>
</ng-container> </ng-container>
@@ -14,7 +14,17 @@
</bit-item> </bit-item>
<bit-item> <bit-item>
<button type="button" bit-item-content (click)="import()"> <button type="button" bit-item-content (click)="import()">
{{ "importItems" | i18n }} <div class="tw-flex tw-items-center tw-justify-center tw-gap-2">
<p>{{ "importItems" | i18n }}</p>
<span
*ngIf="emptyVaultImportBadge$ | async"
bitBadge
variant="notification"
[attr.aria-label]="'nudgeBadgeAria' | i18n"
>
1
</span>
</div>
<i slot="end" class="bwi bwi-popout" aria-hidden="true"></i> <i slot="end" class="bwi bwi-popout" aria-hidden="true"></i>
</button> </button>
</bit-item> </bit-item>

View File

@@ -1,11 +1,15 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { Router, RouterModule } from "@angular/router"; import { Router, RouterModule } from "@angular/router";
import { firstValueFrom, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { ItemModule, ToastOptions, ToastService } from "@bitwarden/components"; import { BadgeComponent, ItemModule, ToastOptions, ToastService } from "@bitwarden/components";
import { BrowserApi } from "../../../platform/browser/browser-api"; import { BrowserApi } from "../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
@@ -23,22 +27,38 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
PopupHeaderComponent, PopupHeaderComponent,
PopOutComponent, PopOutComponent,
ItemModule, ItemModule,
BadgeComponent,
], ],
}) })
export class VaultSettingsV2Component implements OnInit { export class VaultSettingsV2Component implements OnInit, OnDestroy {
lastSync = "--"; lastSync = "--";
protected emptyVaultImportBadge$ = this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) =>
this.nudgeService.showNudgeBadge$(NudgeType.VaultSettingsImportNudge, userId),
),
);
constructor( constructor(
private router: Router, private router: Router,
private syncService: SyncService, private syncService: SyncService,
private toastService: ToastService, private toastService: ToastService,
private i18nService: I18nService, private i18nService: I18nService,
private nudgeService: NudgesService,
private accountService: AccountService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
await this.setLastSync(); await this.setLastSync();
} }
async ngOnDestroy(): Promise<void> {
// When a user navigates away from the page, dismiss the empty vault import nudge
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.nudgeService.dismissNudge(NudgeType.VaultSettingsImportNudge, userId);
}
async import() { async import() {
await this.router.navigate(["/import"]); await this.router.navigate(["/import"]);
if (await BrowserApi.isPopupOpen()) { if (await BrowserApi.isPopupOpen()) {

View File

@@ -3,4 +3,5 @@ export * from "./account-security-nudge.service";
export * from "./has-items-nudge.service"; export * from "./has-items-nudge.service";
export * from "./download-bitwarden-nudge.service"; export * from "./download-bitwarden-nudge.service";
export * from "./empty-vault-nudge.service"; export * from "./empty-vault-nudge.service";
export * from "./vault-settings-import-nudge.service";
export * from "./new-item-nudge.service"; export * from "./new-item-nudge.service";

View File

@@ -0,0 +1,74 @@
import { inject, Injectable } from "@angular/core";
import { combineLatest, Observable, of, switchMap } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { CollectionService } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
import { NudgeStatus, NudgeType } from "../nudges.service";
/**
* Custom Nudge Service for the vault settings import badge.
*/
@Injectable({
providedIn: "root",
})
export class VaultSettingsImportNudgeService extends DefaultSingleNudgeService {
cipherService = inject(CipherService);
organizationService = inject(OrganizationService);
collectionService = inject(CollectionService);
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
return combineLatest([
this.getNudgeStatus$(nudgeType, userId),
this.cipherService.cipherViews$(userId),
this.organizationService.organizations$(userId),
this.collectionService.decryptedCollections$,
]).pipe(
switchMap(([nudgeStatus, ciphers, orgs, collections]) => {
const vaultHasMoreThanOneItem = (ciphers?.length ?? 0) > 1;
const { hasBadgeDismissed, hasSpotlightDismissed } = nudgeStatus;
// When the user has no organizations, return the nudge status directly
if ((orgs?.length ?? 0) === 0) {
return hasBadgeDismissed || hasSpotlightDismissed
? of(nudgeStatus)
: of({
hasSpotlightDismissed: vaultHasMoreThanOneItem,
hasBadgeDismissed: vaultHasMoreThanOneItem,
});
}
const orgIds = new Set(orgs.map((org) => org.id));
const canCreateCollections = orgs.some((org) => org.canCreateNewCollections);
const hasManageCollections = collections.some(
(c) => c.manage && orgIds.has(c.organizationId),
);
// When the user has dismissed the nudge or spotlight, return the nudge status directly
if (hasBadgeDismissed || hasSpotlightDismissed) {
return of(nudgeStatus);
}
// When the user belongs to an organization and cannot create collections or manage collections,
// hide the nudge and spotlight
if (!hasManageCollections && !canCreateCollections) {
return of({
hasSpotlightDismissed: true,
hasBadgeDismissed: true,
});
}
// Otherwise, return the nudge status based on the vault contents
return of({
hasSpotlightDismissed: vaultHasMoreThanOneItem,
hasBadgeDismissed: vaultHasMoreThanOneItem,
});
}),
);
}
}

View File

@@ -20,6 +20,7 @@ import {
HasItemsNudgeService, HasItemsNudgeService,
EmptyVaultNudgeService, EmptyVaultNudgeService,
DownloadBitwardenNudgeService, DownloadBitwardenNudgeService,
VaultSettingsImportNudgeService,
} from "./custom-nudges-services"; } from "./custom-nudges-services";
import { DefaultSingleNudgeService } from "./default-single-nudge.service"; import { DefaultSingleNudgeService } from "./default-single-nudge.service";
import { NudgesService, NudgeType } from "./nudges.service"; import { NudgesService, NudgeType } from "./nudges.service";
@@ -64,6 +65,10 @@ describe("Vault Nudges Service", () => {
provide: EmptyVaultNudgeService, provide: EmptyVaultNudgeService,
useValue: mock<EmptyVaultNudgeService>(), useValue: mock<EmptyVaultNudgeService>(),
}, },
{
provide: VaultSettingsImportNudgeService,
useValue: mock<VaultSettingsImportNudgeService>(),
},
{ {
provide: ApiService, provide: ApiService,
useValue: mock<ApiService>(), useValue: mock<ApiService>(),

View File

@@ -13,6 +13,7 @@ import {
DownloadBitwardenNudgeService, DownloadBitwardenNudgeService,
NewItemNudgeService, NewItemNudgeService,
AccountSecurityNudgeService, AccountSecurityNudgeService,
VaultSettingsImportNudgeService,
} from "./custom-nudges-services"; } from "./custom-nudges-services";
import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service"; import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service";
@@ -31,6 +32,7 @@ export enum NudgeType {
* Add future nudges here * Add future nudges here
*/ */
EmptyVaultNudge = "empty-vault-nudge", EmptyVaultNudge = "empty-vault-nudge",
VaultSettingsImportNudge = "vault-settings-import-nudge",
HasVaultItems = "has-vault-items", HasVaultItems = "has-vault-items",
AutofillNudge = "autofill-nudge", AutofillNudge = "autofill-nudge",
AccountSecurity = "account-security", AccountSecurity = "account-security",
@@ -64,6 +66,7 @@ export class NudgesService {
private customNudgeServices: Partial<Record<NudgeType, SingleNudgeService>> = { private customNudgeServices: Partial<Record<NudgeType, SingleNudgeService>> = {
[NudgeType.HasVaultItems]: inject(HasItemsNudgeService), [NudgeType.HasVaultItems]: inject(HasItemsNudgeService),
[NudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService), [NudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
[NudgeType.VaultSettingsImportNudge]: inject(VaultSettingsImportNudgeService),
[NudgeType.AccountSecurity]: inject(AccountSecurityNudgeService), [NudgeType.AccountSecurity]: inject(AccountSecurityNudgeService),
[NudgeType.AutofillNudge]: inject(AutofillNudgeService), [NudgeType.AutofillNudge]: inject(AutofillNudgeService),
[NudgeType.DownloadBitwarden]: inject(DownloadBitwardenNudgeService), [NudgeType.DownloadBitwarden]: inject(DownloadBitwardenNudgeService),

View File

@@ -1,7 +1,7 @@
<div class="tw-mx-auto tw-flex tw-flex-col tw-items-center tw-justify-center tw-pt-6"> <div class="tw-mx-auto tw-flex tw-flex-col tw-items-center tw-justify-center tw-pt-6">
<div class="tw-max-w-sm tw-flex tw-flex-col tw-items-center"> <div class="tw-max-w-sm tw-flex tw-flex-col tw-items-center">
<bit-icon [icon]="icon" aria-hidden="true"></bit-icon> <bit-icon [icon]="icon" aria-hidden="true"></bit-icon>
<h3 class="tw-font-semibold tw-text-center"> <h3 class="tw-font-semibold tw-text-center tw-mt-4">
<ng-content select="[slot=title]"></ng-content> <ng-content select="[slot=title]"></ng-content>
</h3> </h3>
<p class="tw-text-center"> <p class="tw-text-center">