1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-13 06:54:07 +00:00
Files
browser/libs/angular/src/vault/components/vault-items.component.ts

209 lines
6.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, 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<C extends CipherViewLike> 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<C>();
// 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<C>();
// 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<CipherType | undefined>();
// 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<void>();
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);
}
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;
});
}
}