1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 13:10:17 +00:00
Files
browser/libs/angular/src/vault/components/vault-items.component.ts
Jordan Aasen b589951c90 [PM-18520] - Update desktop cipher forms to use the same UI as web app and extension - (#13992)
* WIP - cipher form refactor

* cipher clone

* cipher clone

* finalize item view and form changes

* fix tests

* hide changes behind feature flag

* set flag to false

* create vault items v2. add button selector

* revert change to flag and vault items

* add attachments

* revert change to tsconfig

* move module

* fix modules

* cleanup

* fix import

* fix import

* fix import

* remove showForm

* update feature flag

* wip - cleanup

* fix up services

* cleanup

* fix type errors

* fix lint errors

* add dialog component

* revert changes to menu

* revert changes to menu

* fix vault-items-v2

* set feature flag to FALSE

* add missing i18n keys. fix collection state

* remove generator. update modules. bug fix

* fix restricted imports

* mark method as deprecated. add uri arg back

* fix shared.module

* fix shared.module

* fix shared.module

* add uri

* check and prompt for premium when opening attachments dialog

* move VaultItemDialogResult back

* fix import in spec file

* update copy functions

* fix MP reprompt issue
2025-04-23 11:13:44 -07:00

169 lines
4.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,
of,
switchMap,
takeUntil,
} from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
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 { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@Directive()
export class VaultItemsComponent implements OnInit, OnDestroy {
@Input() activeCipherId: string = null;
@Output() onCipherClicked = new EventEmitter<CipherView>();
@Output() onCipherRightClicked = new EventEmitter<CipherView>();
@Output() onAddCipher = new EventEmitter<CipherType | undefined>();
@Output() onAddCipherOptions = new EventEmitter();
loaded = false;
ciphers: CipherView[] = [];
deleted = false;
organization: Organization;
CipherType = CipherType;
protected searchPending = false;
/** Construct filters as an observable so it can be appended to the cipher stream. */
private _filter$ = new BehaviorSubject<(cipher: CipherView) => 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: CipherView) => boolean | null) {
this._filter$.next(value);
}
constructor(
protected searchService: SearchService,
protected cipherService: CipherService,
protected accountService: AccountService,
) {
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: CipherView) => boolean = null, deleted = false) {
this.deleted = deleted ?? false;
await this.applyFilter(filter);
this.loaded = true;
}
async reload(filter: (cipher: CipherView) => 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: CipherView) => boolean = null) {
this.filter = filter;
}
selectCipher(cipher: CipherView) {
this.onCipherClicked.emit(cipher);
}
rightClickCipher(cipher: CipherView) {
this.onCipherRightClicked.emit(cipher);
}
addCipher(type?: CipherType) {
this.onAddCipher.emit(type);
}
addCipherOptions() {
this.onAddCipherOptions.emit();
}
isSearching() {
return !this.searchPending && this.isSearchable;
}
protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === 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.cipherViews$(userId).pipe(filter((ciphers) => ciphers != null)),
this.cipherService.failedToDecryptCiphers$(userId),
this._searchText$,
this._filter$,
of(userId),
]),
),
switchMap(([indexedCiphers, failedCiphers, searchText, filter, userId]) => {
let allCiphers = indexedCiphers ?? [];
const _failedCiphers = failedCiphers ?? [];
allCiphers = [..._failedCiphers, ...allCiphers];
return this.searchService.searchCiphers(
userId,
searchText,
[filter, this.deletedFilter],
allCiphers,
);
}),
takeUntilDestroyed(),
)
.subscribe((ciphers) => {
this.ciphers = ciphers;
this.loaded = true;
});
}
}