1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-07 04:03:29 +00:00
Files
browser/libs/angular/src/vault/components/vault-items.component.ts
Nick Krantz b4120e0e3f [PM-22134] Migrate list views to CipherListView from the SDK (#15174)
* add `CipherViewLike` and utilities to handle `CipherView` and `CipherViewLike`

* migrate libs needed for web vault to support `CipherViewLike`

* migrate web vault components to support

* add  for CipherView.  will have to be later

* fetch full CipherView for copying a password

* have only the cipher service utilize SDK migration flag

- This keeps feature flag logic away from the component
- Also cuts down on what is needed for other platforms

* strongly type CipherView for AC vault

- Probably temporary before migration of the AC vault to `CipherListView` SDK

* fix build icon tests by being more gracious with the uri structure

* migrate desktop components to CipherListViews$

* consume card from sdk

* add browser implementation for `CipherListView`

* update copy message for single copiable items

* refactor `getCipherViewLikeLogin` to `getLogin`

* refactor `getCipherViewLikeCard` to `getCard`

* add `hasFido2Credentials` helper

* add decryption failure to cipher like utils

* add todo with ticket

* fix decryption failure typing

* fix copy card messages

* fix addition of organizations and collections for `PopupCipherViewLike`

- accessors were being lost

* refactor to getters to fix re-rendering bug

* fix decryption failure helper

* fix sorting functions for `CipherViewLike`

* formatting

* add `CipherViewLikeUtils` tests

* refactor "copiable" to "copyable" to match SDK

* use `hasOldAttachments` from cipherlistview

* fix typing

* update SDK version

* add feature flag for cipher list view work

* use `CipherViewLikeUtils` for copyable values rather than referring to the cipher directly

* update restricted item type to support CipherViewLike

* add cipher support to `CipherViewLikeUtils`

* update `isCipherListView` check

* refactor CipherLike to a separate type

* refactor `getFullCipherView` into the cipher service

* add optional chaining for `uriChecksum`

* set empty array for decrypted CipherListView

* migrate nudge service to use `cipherListViews`

* update web vault to not depend on `cipherViews$`

* update popup list filters to use `CipherListView`

* fix storybook

* fix tests

* accept undefined as a MY VAULT filter value for cipher list views

* use `LoginUriView` for uri logic (#15530)

* filter out null ciphers from the `_allDecryptedCiphers$` (#15539)

* use `launchUri` to avoid any unexpected behavior in URIs - this appends `http://` when missing
2025-07-17 14:55:32 -05:00

195 lines
5.9 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import {
BehaviorSubject,
Subject,
combineLatest,
filter,
from,
map,
of,
shareReplay,
switchMap,
takeUntil,
} from "rxjs";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
import {
CipherViewLike,
CipherViewLikeUtils,
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
@Directive()
export class VaultItemsComponent<C extends CipherViewLike> implements OnInit, OnDestroy {
@Input() activeCipherId: string = null;
@Output() onCipherClicked = new EventEmitter<C>();
@Output() onCipherRightClicked = new EventEmitter<C>();
@Output() onAddCipher = new EventEmitter<CipherType | undefined>();
@Output() onAddCipherOptions = new EventEmitter();
loaded = false;
ciphers: C[] = [];
deleted = false;
organization: Organization;
CipherType = CipherType;
protected itemTypes$ = this.restrictedItemTypesService.restricted$.pipe(
map((restrictedItemTypes) =>
// Filter out restricted item types
CIPHER_MENU_ITEMS.filter(
(itemType) =>
!restrictedItemTypes.some(
(restrictedType) => restrictedType.cipherType === itemType.type,
),
),
),
shareReplay({ bufferSize: 1, refCount: true }),
);
protected searchPending = false;
/** Construct filters as an observable so it can be appended to the cipher stream. */
private _filter$ = new BehaviorSubject<(cipher: C) => boolean | null>(null);
private destroy$ = new Subject<void>();
private isSearchable: boolean = false;
private _searchText$ = new BehaviorSubject<string>("");
get searchText() {
return this._searchText$.value;
}
set searchText(value: string) {
this._searchText$.next(value);
}
get filter() {
return this._filter$.value;
}
set filter(value: (cipher: C) => boolean | null) {
this._filter$.next(value);
}
constructor(
protected searchService: SearchService,
protected cipherService: CipherService,
protected accountService: AccountService,
protected restrictedItemTypesService: RestrictedItemTypesService,
) {
this.subscribeToCiphers();
}
async ngOnInit() {
combineLatest([getUserId(this.accountService.activeAccount$), this._searchText$])
.pipe(
switchMap(([userId, searchText]) =>
from(this.searchService.isSearchable(userId, searchText)),
),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {
this.isSearchable = isSearchable;
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
async load(filter: (cipher: C) => boolean = null, deleted = false) {
this.deleted = deleted ?? false;
await this.applyFilter(filter);
this.loaded = true;
}
async reload(filter: (cipher: C) => boolean = null, deleted = false) {
this.loaded = false;
await this.load(filter, deleted);
}
async refresh() {
await this.reload(this.filter, this.deleted);
}
async applyFilter(filter: (cipher: C) => boolean = null) {
this.filter = filter;
}
selectCipher(cipher: C) {
this.onCipherClicked.emit(cipher);
}
rightClickCipher(cipher: C) {
this.onCipherRightClicked.emit(cipher);
}
addCipher(type?: CipherType) {
this.onAddCipher.emit(type);
}
addCipherOptions() {
this.onAddCipherOptions.emit();
}
isSearching() {
return !this.searchPending && this.isSearchable;
}
protected deletedFilter: (cipher: C) => boolean = (c) =>
CipherViewLikeUtils.isDeleted(c) === this.deleted;
/**
* Creates stream of dependencies that results in the list of ciphers to display
* within the vault list.
*
* Note: This previously used promises but race conditions with how the ciphers were
* stored in electron. Using observables is more reliable as fresh values will always
* cascade through the components.
*/
private subscribeToCiphers() {
getUserId(this.accountService.activeAccount$)
.pipe(
switchMap((userId) =>
combineLatest([
this.cipherService.cipherListViews$(userId).pipe(filter((ciphers) => ciphers != null)),
this.cipherService.failedToDecryptCiphers$(userId),
this._searchText$,
this._filter$,
of(userId),
this.restrictedItemTypesService.restricted$,
]),
),
switchMap(([indexedCiphers, failedCiphers, searchText, filter, userId, restricted]) => {
let allCiphers = (indexedCiphers ?? []) as C[];
const _failedCiphers = failedCiphers ?? [];
allCiphers = [..._failedCiphers, ...allCiphers] as C[];
const restrictedTypeFilter = (cipher: CipherViewLike) =>
!this.restrictedItemTypesService.isCipherRestricted(cipher, restricted);
return this.searchService.searchCiphers(
userId,
searchText,
[filter, this.deletedFilter, restrictedTypeFilter],
allCiphers,
);
}),
takeUntilDestroyed(),
)
.subscribe((ciphers) => {
this.ciphers = ciphers;
this.loaded = true;
});
}
}