1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 01:03:35 +00:00

[PM-19907] updated empty state messages for web (#16283)

* updated empty state icons and copy for web vault
This commit is contained in:
Holly
2025-09-30 13:55:07 -05:00
committed by GitHub
parent 2a0b564e93
commit 0bd098dd8f
6 changed files with 150 additions and 23 deletions

View File

@@ -208,15 +208,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
}
applyOrganizationFilter = async (orgNode: TreeNode<OrganizationFilter>): Promise<void> => {
if (!orgNode?.node.enabled) {
this.toastService.showToast({
variant: "error",
message: this.i18nService.t("disabledOrganizationFilterError"),
});
await firstValueFrom(
this.organizationWarningsService.showInactiveSubscriptionDialog$(orgNode.node),
);
}
const filter = this.activeFilter;
if (orgNode?.node.id === "AllVaults") {
filter.resetOrganization();

View File

@@ -68,19 +68,20 @@
class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start"
*ngIf="isEmpty && !performingInitialLoad"
>
<bit-no-items [icon]="noItemIcon">
<div slot="title" *ngIf="filter.type === 'archive'">{{ "noItemsInArchive" | i18n }}</div>
<p slot="description" class="tw-text-center tw-max-w-md" *ngIf="filter.type === 'archive'">
{{ "archivedItemsDescription" | i18n }}
<bit-no-items [icon]="(emptyState$ | async)?.icon">
<div slot="title">
{{ (emptyState$ | async)?.title | i18n }}
</div>
<p slot="description" bitTypography="body2" class="tw-max-w-md tw-text-center">
{{ (emptyState$ | async)?.description | i18n }}
</p>
<div slot="title" *ngIf="filter.type !== 'archive'">{{ "noItemsInList" | i18n }}</div>
<button
type="button"
buttonType="primary"
bitButton
(click)="addCipher()"
slot="button"
*ngIf="filter.type !== 'trash' && filter.type !== 'archive'"
*ngIf="showAddCipherBtn"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "newItem" | i18n }}

View File

@@ -32,7 +32,14 @@ import {
Unassigned,
} from "@bitwarden/admin-console/common";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { NoResults } from "@bitwarden/assets/svg";
import {
NoResults,
DeactivatedOrg,
EmptyTrash,
FavoritesIcon,
ItemTypes,
Icon,
} from "@bitwarden/assets/svg";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import {
@@ -134,6 +141,16 @@ import { VaultOnboardingComponent } from "./vault-onboarding/vault-onboarding.co
const BroadcasterSubscriptionId = "VaultComponent";
type EmptyStateType = "trash" | "favorites" | "archive";
type EmptyStateItem = {
title: string;
description: string;
icon: Icon;
};
type EmptyStateMap = Record<EmptyStateType, EmptyStateItem>;
@Component({
selector: "app-vault",
templateUrl: "vault.component.html",
@@ -160,7 +177,11 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
kdfIterations: number;
activeFilter: VaultFilter = new VaultFilter();
protected noItemIcon = NoResults;
protected deactivatedOrgIcon = DeactivatedOrg;
protected emptyTrashIcon = EmptyTrash;
protected favoritesIcon = FavoritesIcon;
protected itemTypesIcon = ItemTypes;
protected noResultsIcon = NoResults;
protected performingInitialLoad = true;
protected refreshing = false;
protected processingEvent = false;
@@ -174,12 +195,16 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
protected isEmpty: boolean;
protected selectedCollection: TreeNode<CollectionView> | undefined;
protected canCreateCollections = false;
protected currentSearchText$: Observable<string>;
protected currentSearchText$: Observable<string> = this.route.queryParams.pipe(
map((queryParams) => queryParams.search),
);
private searchText$ = new Subject<string>();
private refresh$ = new BehaviorSubject<void>(null);
private destroy$ = new Subject<void>();
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
protected showAddCipherBtn: boolean = false;
organizations$ = this.accountService.activeAccount$
.pipe(map((a) => a?.id))
.pipe(switchMap((id) => this.organizationService.organizations$(id)));
@@ -191,6 +216,64 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
}),
);
emptyState$ = combineLatest([
this.currentSearchText$,
this.routedVaultFilterService.filter$,
this.organizations$,
]).pipe(
map(([searchText, filter, organizations]) => {
const selectedOrg = organizations?.find((org) => org.id === filter.organizationId);
const isOrgDisabled = selectedOrg && !selectedOrg.enabled;
if (isOrgDisabled) {
this.showAddCipherBtn = false;
return {
title: "organizationIsSuspended",
description: "organizationIsSuspendedDesc",
icon: this.deactivatedOrgIcon,
};
}
if (searchText) {
return {
title: "noSearchResults",
description: "clearFiltersOrTryAnother",
icon: this.noResultsIcon,
};
}
const emptyStateMap: EmptyStateMap = {
trash: {
title: "noItemsInTrash",
description: "noItemsInTrashDesc",
icon: this.emptyTrashIcon,
},
favorites: {
title: "emptyFavorites",
description: "emptyFavoritesDesc",
icon: this.favoritesIcon,
},
archive: {
title: "noItemsInArchive",
description: "archivedItemsDescription",
icon: this.itemTypesIcon,
},
};
if (filter?.type && filter.type in emptyStateMap) {
this.showAddCipherBtn = false;
return emptyStateMap[filter.type as EmptyStateType];
}
this.showAddCipherBtn = true;
return {
title: "noItemsInVault",
description: "emptyVaultDescription",
icon: this.itemTypesIcon,
};
}),
);
constructor(
private syncService: SyncService,
private route: ActivatedRoute,
@@ -298,8 +381,6 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
}),
);
this.currentSearchText$ = this.route.queryParams.pipe(map((queryParams) => queryParams.search));
const _ciphers = this.cipherService
.cipherListViews$(activeUserId)
.pipe(filter((c) => c !== null));

View File

@@ -1508,6 +1508,30 @@
"noItemsInList": {
"message": "There are no items to list."
},
"noItemsInTrash": {
"message": "No items in trash"
},
"noItemsInTrashDesc": {
"message": "Items you delete will appear here and be permanently deleted after 30 days"
},
"noItemsInVault": {
"message": "No items in the vault"
},
"emptyVaultDescription": {
"message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here."
},
"emptyFavorites": {
"message": "You haven't favorited any items"
},
"emptyFavoritesDesc": {
"message": "Add frequently used items to favorites for quick access."
},
"noSearchResults": {
"message": "No search results returned"
},
"clearFiltersOrTryAnother": {
"message": "Clear filters or try another search term"
},
"noPermissionToViewAllCollectionItems": {
"message": "You do not have permission to view all items in this collection."
},
@@ -4804,6 +4828,12 @@
"organizationIsDisabled": {
"message": "Organization suspended"
},
"organizationIsSuspended": {
"message": "Organization is suspended"
},
"organizationIsSuspendedDesc": {
"message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance."
},
"secretsAccessSuspended": {
"message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance."
},
@@ -4816,9 +4846,6 @@
"serviceAccountsCannotCreate": {
"message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance."
},
"disabledOrganizationFilterError": {
"message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance."
},
"licenseIsExpired": {
"message": "License is expired."
},