// FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Directive, EventEmitter, Input, OnDestroy, Output } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { BehaviorSubject, Subject, combineLatest, filter, map, of, shareReplay, switchMap, take, } 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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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 implements OnDestroy { // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @Input() activeCipherId: string = null; // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onCipherClicked = new EventEmitter(); // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onCipherRightClicked = new EventEmitter(); // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onAddCipher = new EventEmitter(); // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onAddCipherOptions = new EventEmitter(); loaded = false; ciphers: C[] = []; deleted = false; archived = 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 }), ); /** 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(); private _searchText$ = new BehaviorSubject(""); 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); } private archiveFeatureEnabled = false; constructor( protected searchService: SearchService, protected cipherService: CipherService, protected accountService: AccountService, protected restrictedItemTypesService: RestrictedItemTypesService, protected configService: ConfigService, ) { this.subscribeToCiphers(); // Check if archive feature flag is enabled this.configService .getFeatureFlag$(FeatureFlag.PM19148_InnovationArchive) .pipe(takeUntilDestroyed(), take(1)) .subscribe((isEnabled) => { this.archiveFeatureEnabled = isEnabled; }); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } async load(filter: (cipher: C) => boolean = null, deleted = false, archived = false) { this.deleted = deleted ?? false; this.archived = archived; await this.applyFilter(filter); this.loaded = true; } async reload(filter: (cipher: C) => boolean = null, deleted = false, archived = false) { this.loaded = false; await this.load(filter, deleted, archived); } async refresh() { await this.reload(this.filter, this.deleted, this.archived); } 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(); } protected deletedFilter: (cipher: C) => boolean = (c) => CipherViewLikeUtils.isDeleted(c) === this.deleted; protected archivedFilter: (cipher: C) => boolean = (c) => { // When the archive feature is not enabled, // always return true to avoid filtering out any items. if (!this.archiveFeatureEnabled) { return true; } return CipherViewLikeUtils.isArchived(c) === this.archived; }; /** * 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, restrictedTypeFilter], allCiphers, ); }), takeUntilDestroyed(), ) .subscribe((ciphers) => { this.ciphers = ciphers; this.loaded = true; }); } }