mirror of
https://github.com/bitwarden/browser
synced 2026-02-14 15:33:55 +00:00
[PM-12045] search service activeuserstate (#13035)
* removing activeuserstate from search service
This commit is contained in:
@@ -389,11 +389,14 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
protected async search() {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
this.hasSearched = true;
|
||||
const isSearchable = await this.searchService.isSearchable(this.searchText);
|
||||
const isSearchable = await this.searchService.isSearchable(userId, this.searchText);
|
||||
|
||||
if (isSearchable) {
|
||||
this.displayedCiphers = await this.searchService.searchCiphers(
|
||||
userId,
|
||||
this.searchText,
|
||||
null,
|
||||
this.ciphers,
|
||||
|
||||
@@ -87,7 +87,7 @@ describe("VaultPopupItemsService", () => {
|
||||
failedToDecryptCiphersSubject.asObservable(),
|
||||
);
|
||||
|
||||
searchService.searchCiphers.mockImplementation(async (_, __, ciphers) => ciphers);
|
||||
searchService.searchCiphers.mockImplementation(async (userId, _, __, ciphers) => ciphers);
|
||||
cipherServiceMock.filterCiphersForUrl.mockImplementation(async (ciphers) =>
|
||||
ciphers.filter((c) => ["0", "1"].includes(c.id)),
|
||||
);
|
||||
@@ -276,7 +276,7 @@ describe("VaultPopupItemsService", () => {
|
||||
it("should filter autoFillCiphers$ down to search term", (done) => {
|
||||
const searchText = "Login";
|
||||
|
||||
searchService.searchCiphers.mockImplementation(async (q, _, ciphers) => {
|
||||
searchService.searchCiphers.mockImplementation(async (userId, q, _, ciphers) => {
|
||||
return ciphers.filter((cipher) => {
|
||||
return cipher.name.includes(searchText);
|
||||
});
|
||||
@@ -472,7 +472,12 @@ describe("VaultPopupItemsService", () => {
|
||||
|
||||
service.applyFilter(searchText);
|
||||
service.favoriteCiphers$.subscribe(() => {
|
||||
expect(searchServiceSpy).toHaveBeenCalledWith(searchText, undefined, expect.anything());
|
||||
expect(searchServiceSpy).toHaveBeenCalledWith(
|
||||
"UserId",
|
||||
searchText,
|
||||
undefined,
|
||||
expect.anything(),
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -138,22 +139,29 @@ export class VaultPopupItemsService {
|
||||
* Observable that indicates whether there is search text present that is searchable.
|
||||
* @private
|
||||
*/
|
||||
private _hasSearchText = this.searchText$.pipe(
|
||||
switchMap((searchText) => this.searchService.isSearchable(searchText)),
|
||||
private _hasSearchText = combineLatest([
|
||||
this.searchText$,
|
||||
getUserId(this.accountService.activeAccount$),
|
||||
]).pipe(
|
||||
switchMap(([searchText, userId]) => {
|
||||
return this.searchService.isSearchable(userId, searchText);
|
||||
}),
|
||||
);
|
||||
|
||||
private _filteredCipherList$: Observable<PopupCipherView[]> = combineLatest([
|
||||
this._activeCipherList$,
|
||||
this.searchText$,
|
||||
this.vaultPopupListFiltersService.filterFunction$,
|
||||
getUserId(this.accountService.activeAccount$),
|
||||
]).pipe(
|
||||
map(([ciphers, searchText, filterFunction]): [CipherView[], string] => [
|
||||
map(([ciphers, searchText, filterFunction, userId]): [CipherView[], string, UserId] => [
|
||||
filterFunction(ciphers),
|
||||
searchText,
|
||||
userId,
|
||||
]),
|
||||
switchMap(
|
||||
([ciphers, searchText]) =>
|
||||
this.searchService.searchCiphers(searchText, undefined, ciphers) as Promise<
|
||||
([ciphers, searchText, userId]) =>
|
||||
this.searchService.searchCiphers(userId, searchText, undefined, ciphers) as Promise<
|
||||
PopupCipherView[]
|
||||
>,
|
||||
),
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -52,6 +53,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
|
||||
sendApiService: SendApiService,
|
||||
dialogService: DialogService,
|
||||
toastService: ToastService,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
sendService,
|
||||
@@ -65,6 +67,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
|
||||
sendApiService,
|
||||
dialogService,
|
||||
toastService,
|
||||
accountService,
|
||||
);
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.searchBarService.searchText$.subscribe((searchText) => {
|
||||
|
||||
@@ -1,425 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { FormControl } from "@angular/forms";
|
||||
import { firstValueFrom, concatMap, map, lastValueFrom, startWith, debounceTime } from "rxjs";
|
||||
|
||||
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
||||
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
|
||||
import {
|
||||
OrganizationUserStatusType,
|
||||
OrganizationUserType,
|
||||
ProviderUserStatusType,
|
||||
ProviderUserType,
|
||||
} from "@bitwarden/common/admin-console/enums";
|
||||
import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationUserView } from "../organizations/core/views/organization-user.view";
|
||||
import { UserConfirmComponent } from "../organizations/manage/user-confirm.component";
|
||||
|
||||
type StatusType = OrganizationUserStatusType | ProviderUserStatusType;
|
||||
|
||||
const MaxCheckedCount = 500;
|
||||
|
||||
@Directive()
|
||||
export abstract class BasePeopleComponent<
|
||||
UserType extends ProviderUserUserDetailsResponse | OrganizationUserView,
|
||||
> {
|
||||
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
|
||||
confirmModalRef: ViewContainerRef;
|
||||
|
||||
get allCount() {
|
||||
return this.activeUsers != null ? this.activeUsers.length : 0;
|
||||
}
|
||||
|
||||
get invitedCount() {
|
||||
return this.statusMap.has(this.userStatusType.Invited)
|
||||
? this.statusMap.get(this.userStatusType.Invited).length
|
||||
: 0;
|
||||
}
|
||||
|
||||
get acceptedCount() {
|
||||
return this.statusMap.has(this.userStatusType.Accepted)
|
||||
? this.statusMap.get(this.userStatusType.Accepted).length
|
||||
: 0;
|
||||
}
|
||||
|
||||
get confirmedCount() {
|
||||
return this.statusMap.has(this.userStatusType.Confirmed)
|
||||
? this.statusMap.get(this.userStatusType.Confirmed).length
|
||||
: 0;
|
||||
}
|
||||
|
||||
get revokedCount() {
|
||||
return this.statusMap.has(this.userStatusType.Revoked)
|
||||
? this.statusMap.get(this.userStatusType.Revoked).length
|
||||
: 0;
|
||||
}
|
||||
|
||||
get showConfirmUsers(): boolean {
|
||||
return (
|
||||
this.activeUsers != null &&
|
||||
this.statusMap != null &&
|
||||
this.activeUsers.length > 1 &&
|
||||
this.confirmedCount > 0 &&
|
||||
this.confirmedCount < 3 &&
|
||||
this.acceptedCount > 0
|
||||
);
|
||||
}
|
||||
|
||||
get showBulkConfirmUsers(): boolean {
|
||||
return this.acceptedCount > 0;
|
||||
}
|
||||
|
||||
abstract userType: typeof OrganizationUserType | typeof ProviderUserType;
|
||||
abstract userStatusType: typeof OrganizationUserStatusType | typeof ProviderUserStatusType;
|
||||
|
||||
loading = true;
|
||||
statusMap = new Map<StatusType, UserType[]>();
|
||||
status: StatusType;
|
||||
users: UserType[] = [];
|
||||
pagedUsers: UserType[] = [];
|
||||
actionPromise: Promise<void>;
|
||||
|
||||
protected allUsers: UserType[] = [];
|
||||
protected activeUsers: UserType[] = [];
|
||||
|
||||
protected didScroll = false;
|
||||
protected pageSize = 100;
|
||||
|
||||
protected searchControl = new FormControl("", { nonNullable: true });
|
||||
protected isSearching$ = this.searchControl.valueChanges.pipe(
|
||||
debounceTime(500),
|
||||
concatMap((searchText) => this.searchService.isSearchable(searchText)),
|
||||
startWith(false),
|
||||
);
|
||||
protected isPaging$ = this.isSearching$.pipe(
|
||||
map((isSearching) => {
|
||||
if (isSearching && this.didScroll) {
|
||||
this.resetPaging();
|
||||
}
|
||||
return !isSearching && this.users && this.users.length > this.pageSize;
|
||||
}),
|
||||
);
|
||||
|
||||
private pagedUsersCount = 0;
|
||||
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
private searchService: SearchService,
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected keyService: KeyService,
|
||||
protected validationService: ValidationService,
|
||||
private logService: LogService,
|
||||
private searchPipe: SearchPipe,
|
||||
protected userNamePipe: UserNamePipe,
|
||||
protected dialogService: DialogService,
|
||||
protected organizationManagementPreferencesService: OrganizationManagementPreferencesService,
|
||||
protected toastService: ToastService,
|
||||
) {}
|
||||
|
||||
abstract edit(user: UserType): void;
|
||||
abstract getUsers(): Promise<ListResponse<UserType> | UserType[]>;
|
||||
abstract deleteUser(id: string): Promise<void>;
|
||||
abstract revokeUser(id: string): Promise<void>;
|
||||
abstract restoreUser(id: string): Promise<void>;
|
||||
abstract reinviteUser(id: string): Promise<void>;
|
||||
abstract confirmUser(user: UserType, publicKey: Uint8Array): Promise<void>;
|
||||
|
||||
async load() {
|
||||
const response = await this.getUsers();
|
||||
this.statusMap.clear();
|
||||
this.activeUsers = [];
|
||||
for (const status of Utils.iterateEnum(this.userStatusType)) {
|
||||
this.statusMap.set(status, []);
|
||||
}
|
||||
|
||||
if (response instanceof ListResponse) {
|
||||
this.allUsers = response.data != null && response.data.length > 0 ? response.data : [];
|
||||
} else if (Array.isArray(response)) {
|
||||
this.allUsers = response;
|
||||
}
|
||||
|
||||
this.allUsers.sort(
|
||||
Utils.getSortFunction<ProviderUserUserDetailsResponse | OrganizationUserView>(
|
||||
this.i18nService,
|
||||
"email",
|
||||
),
|
||||
);
|
||||
this.allUsers.forEach((u) => {
|
||||
if (!this.statusMap.has(u.status)) {
|
||||
this.statusMap.set(u.status, [u]);
|
||||
} else {
|
||||
this.statusMap.get(u.status).push(u);
|
||||
}
|
||||
if (u.status !== this.userStatusType.Revoked) {
|
||||
this.activeUsers.push(u);
|
||||
}
|
||||
});
|
||||
this.filter(this.status);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
filter(status: StatusType) {
|
||||
this.status = status;
|
||||
if (this.status != null) {
|
||||
this.users = this.statusMap.get(this.status);
|
||||
} else {
|
||||
this.users = this.activeUsers;
|
||||
}
|
||||
// Reset checkbox selecton
|
||||
this.selectAll(false);
|
||||
this.resetPaging();
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (!this.users || this.users.length <= this.pageSize) {
|
||||
return;
|
||||
}
|
||||
const pagedLength = this.pagedUsers.length;
|
||||
let pagedSize = this.pageSize;
|
||||
if (pagedLength === 0 && this.pagedUsersCount > this.pageSize) {
|
||||
pagedSize = this.pagedUsersCount;
|
||||
}
|
||||
if (this.users.length > pagedLength) {
|
||||
this.pagedUsers = this.pagedUsers.concat(
|
||||
this.users.slice(pagedLength, pagedLength + pagedSize),
|
||||
);
|
||||
}
|
||||
this.pagedUsersCount = this.pagedUsers.length;
|
||||
this.didScroll = this.pagedUsers.length > this.pageSize;
|
||||
}
|
||||
|
||||
checkUser(user: UserType, select?: boolean) {
|
||||
(user as any).checked = select == null ? !(user as any).checked : select;
|
||||
}
|
||||
|
||||
selectAll(select: boolean) {
|
||||
if (select) {
|
||||
this.selectAll(false);
|
||||
}
|
||||
|
||||
const filteredUsers = this.searchPipe.transform(
|
||||
this.users,
|
||||
this.searchControl.value,
|
||||
"name",
|
||||
"email",
|
||||
"id",
|
||||
);
|
||||
|
||||
const selectCount =
|
||||
select && filteredUsers.length > MaxCheckedCount ? MaxCheckedCount : filteredUsers.length;
|
||||
for (let i = 0; i < selectCount; i++) {
|
||||
this.checkUser(filteredUsers[i], select);
|
||||
}
|
||||
}
|
||||
|
||||
resetPaging() {
|
||||
this.pagedUsers = [];
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
invite() {
|
||||
this.edit(null);
|
||||
}
|
||||
|
||||
protected async removeUserConfirmationDialog(user: UserType) {
|
||||
return this.dialogService.openSimpleDialog({
|
||||
title: this.userNamePipe.transform(user),
|
||||
content: { key: "removeUserConfirmation" },
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
|
||||
async remove(user: UserType) {
|
||||
const confirmed = await this.removeUserConfirmationDialog(user);
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.actionPromise = this.deleteUser(user.id);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("removedUserId", this.userNamePipe.transform(user)),
|
||||
});
|
||||
this.removeUser(user);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
protected async revokeUserConfirmationDialog(user: UserType) {
|
||||
return this.dialogService.openSimpleDialog({
|
||||
title: { key: "revokeAccess", placeholders: [this.userNamePipe.transform(user)] },
|
||||
content: this.revokeWarningMessage(),
|
||||
acceptButtonText: { key: "revokeAccess" },
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
|
||||
async revoke(user: UserType) {
|
||||
const confirmed = await this.revokeUserConfirmationDialog(user);
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.actionPromise = this.revokeUser(user.id);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("revokedUserId", this.userNamePipe.transform(user)),
|
||||
});
|
||||
await this.load();
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
async restore(user: UserType) {
|
||||
this.actionPromise = this.restoreUser(user.id);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("restoredUserId", this.userNamePipe.transform(user)),
|
||||
});
|
||||
await this.load();
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
async reinvite(user: UserType) {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionPromise = this.reinviteUser(user.id);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("hasBeenReinvited", this.userNamePipe.transform(user)),
|
||||
});
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
async confirm(user: UserType) {
|
||||
function updateUser(self: BasePeopleComponent<UserType>) {
|
||||
user.status = self.userStatusType.Confirmed;
|
||||
const mapIndex = self.statusMap.get(self.userStatusType.Accepted).indexOf(user);
|
||||
if (mapIndex > -1) {
|
||||
self.statusMap.get(self.userStatusType.Accepted).splice(mapIndex, 1);
|
||||
self.statusMap.get(self.userStatusType.Confirmed).push(user);
|
||||
}
|
||||
}
|
||||
|
||||
const confirmUser = async (publicKey: Uint8Array) => {
|
||||
try {
|
||||
this.actionPromise = this.confirmUser(user, publicKey);
|
||||
await this.actionPromise;
|
||||
updateUser(this);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(user)),
|
||||
});
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
throw e;
|
||||
} finally {
|
||||
this.actionPromise = null;
|
||||
}
|
||||
};
|
||||
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
|
||||
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||
|
||||
const autoConfirm = await firstValueFrom(
|
||||
this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$,
|
||||
);
|
||||
if (autoConfirm == null || !autoConfirm) {
|
||||
const dialogRef = UserConfirmComponent.open(this.dialogService, {
|
||||
data: {
|
||||
name: this.userNamePipe.transform(user),
|
||||
userId: user != null ? user.userId : null,
|
||||
publicKey: publicKey,
|
||||
confirmUser: () => confirmUser(publicKey),
|
||||
},
|
||||
});
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fingerprint = await this.keyService.getFingerprint(user.userId, publicKey);
|
||||
this.logService.info(`User's fingerprint: ${fingerprint.join("-")}`);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
await confirmUser(publicKey);
|
||||
} catch (e) {
|
||||
this.logService.error(`Handled exception: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
protected revokeWarningMessage(): string {
|
||||
return this.i18nService.t("revokeUserConfirmation");
|
||||
}
|
||||
|
||||
protected getCheckedUsers() {
|
||||
return this.users.filter((u) => (u as any).checked);
|
||||
}
|
||||
|
||||
protected removeUser(user: UserType) {
|
||||
let index = this.users.indexOf(user);
|
||||
if (index > -1) {
|
||||
this.users.splice(index, 1);
|
||||
this.resetPaging();
|
||||
}
|
||||
|
||||
index = this.allUsers.indexOf(user);
|
||||
if (index > -1) {
|
||||
this.allUsers.splice(index, 1);
|
||||
}
|
||||
|
||||
if (this.statusMap.has(user.status)) {
|
||||
index = this.statusMap.get(user.status).indexOf(user);
|
||||
if (index > -1) {
|
||||
this.statusMap.get(user.status).splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,6 +178,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
protected freeTrial$: Observable<FreeTrial>;
|
||||
protected resellerWarning$: Observable<ResellerWarning | null>;
|
||||
protected prevCipherId: string | null = null;
|
||||
protected userId: UserId;
|
||||
/**
|
||||
* A list of collections that the user can assign items to and edit those items within.
|
||||
* @protected
|
||||
@@ -258,6 +259,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
this.resellerManagedOrgAlert = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.ResellerManagedOrgAlert,
|
||||
);
|
||||
@@ -401,7 +404,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
ciphers = await this.cipherService.getManyFromApiForOrganization(organization.id);
|
||||
}
|
||||
|
||||
await this.searchService.indexCiphers(ciphers, organization.id);
|
||||
await this.searchService.indexCiphers(this.userId, ciphers, organization.id);
|
||||
return ciphers;
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
@@ -445,7 +448,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? [];
|
||||
}
|
||||
|
||||
if (await this.searchService.isSearchable(searchText)) {
|
||||
if (await this.searchService.isSearchable(this.userId, searchText)) {
|
||||
collectionsToReturn = this.searchPipe.transform(
|
||||
collectionsToReturn,
|
||||
searchText,
|
||||
@@ -519,8 +522,13 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
const filterFunction = createFilterFunction(filter);
|
||||
|
||||
if (await this.searchService.isSearchable(searchText)) {
|
||||
return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers);
|
||||
if (await this.searchService.isSearchable(this.userId, searchText)) {
|
||||
return await this.searchService.searchCiphers(
|
||||
this.userId,
|
||||
searchText,
|
||||
[filterFunction],
|
||||
ciphers,
|
||||
);
|
||||
}
|
||||
|
||||
return ciphers.filter(filterFunction);
|
||||
|
||||
@@ -309,7 +309,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
|
||||
await this.stateEventRunnerService.handleEvent("logout", userId);
|
||||
|
||||
await this.searchService.clearIndex();
|
||||
await this.searchService.clearIndex(userId);
|
||||
this.authService.logOut(async () => {
|
||||
await this.stateService.clean({ userId: userId });
|
||||
await this.accountService.clean(userId);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { lastValueFrom } from "rxjs";
|
||||
import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -74,6 +75,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
|
||||
dialogService: DialogService,
|
||||
toastService: ToastService,
|
||||
private addEditFormConfigService: DefaultSendFormConfigService,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
sendService,
|
||||
@@ -87,6 +89,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
|
||||
sendApiService,
|
||||
dialogService,
|
||||
toastService,
|
||||
accountService,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -348,8 +348,13 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
// Append any failed to decrypt ciphers to the top of the cipher list
|
||||
const allCiphers = [...failedCiphers, ...ciphers];
|
||||
|
||||
if (await this.searchService.isSearchable(searchText)) {
|
||||
return await this.searchService.searchCiphers(searchText, [filterFunction], allCiphers);
|
||||
if (await this.searchService.isSearchable(activeUserId, searchText)) {
|
||||
return await this.searchService.searchCiphers(
|
||||
activeUserId,
|
||||
searchText,
|
||||
[filterFunction],
|
||||
allCiphers,
|
||||
);
|
||||
}
|
||||
|
||||
return allCiphers.filter(filterFunction);
|
||||
@@ -378,7 +383,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? [];
|
||||
}
|
||||
|
||||
if (await this.searchService.isSearchable(searchText)) {
|
||||
if (await this.searchService.isSearchable(activeUserId, searchText)) {
|
||||
collectionsToReturn = this.searchPipe.transform(
|
||||
collectionsToReturn,
|
||||
searchText,
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { SelectionModel } from "@angular/cdk/collections";
|
||||
import { Directive, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { BehaviorSubject, from, Subject, switchMap } from "rxjs";
|
||||
import { first, takeUntil } from "rxjs/operators";
|
||||
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { DialogService, TableDataSource, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { WebProviderService } from "../services/web-provider.service";
|
||||
|
||||
@Directive()
|
||||
export abstract class BaseClientsComponent implements OnInit, OnDestroy {
|
||||
protected destroy$ = new Subject<void>();
|
||||
|
||||
private searchText$ = new BehaviorSubject<string>("");
|
||||
|
||||
get searchText() {
|
||||
return this.searchText$.value;
|
||||
}
|
||||
|
||||
set searchText(value: string) {
|
||||
this.searchText$.next(value);
|
||||
this.selection.clear();
|
||||
this.dataSource.filter = value;
|
||||
}
|
||||
|
||||
private searching = false;
|
||||
protected scrolled = false;
|
||||
protected pageSize = 100;
|
||||
private pagedClientsCount = 0;
|
||||
protected selection = new SelectionModel<string>(true, []);
|
||||
|
||||
protected clients: ProviderOrganizationOrganizationDetailsResponse[];
|
||||
protected pagedClients: ProviderOrganizationOrganizationDetailsResponse[];
|
||||
protected dataSource = new TableDataSource<ProviderOrganizationOrganizationDetailsResponse>();
|
||||
|
||||
abstract providerId: string;
|
||||
|
||||
protected constructor(
|
||||
protected activatedRoute: ActivatedRoute,
|
||||
protected dialogService: DialogService,
|
||||
private i18nService: I18nService,
|
||||
private searchService: SearchService,
|
||||
private toastService: ToastService,
|
||||
private validationService: ValidationService,
|
||||
private webProviderService: WebProviderService,
|
||||
) {}
|
||||
|
||||
abstract load(): Promise<void>;
|
||||
|
||||
ngOnInit() {
|
||||
this.activatedRoute.queryParams
|
||||
.pipe(first(), takeUntil(this.destroy$))
|
||||
.subscribe((queryParams) => {
|
||||
this.searchText = queryParams.search;
|
||||
});
|
||||
|
||||
this.searchText$
|
||||
.pipe(
|
||||
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((isSearchable) => {
|
||||
this.searching = isSearchable;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
isPaging() {
|
||||
if (this.searching && this.scrolled) {
|
||||
this.resetPaging();
|
||||
}
|
||||
return !this.searching && this.clients && this.clients.length > this.pageSize;
|
||||
}
|
||||
|
||||
resetPaging() {
|
||||
this.pagedClients = [];
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (!this.clients || this.clients.length <= this.pageSize) {
|
||||
return;
|
||||
}
|
||||
const pagedLength = this.pagedClients.length;
|
||||
let pagedSize = this.pageSize;
|
||||
if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) {
|
||||
pagedSize = this.pagedClientsCount;
|
||||
}
|
||||
if (this.clients.length > pagedLength) {
|
||||
this.pagedClients = this.pagedClients.concat(
|
||||
this.clients.slice(pagedLength, pagedLength + pagedSize),
|
||||
);
|
||||
}
|
||||
this.pagedClientsCount = this.pagedClients.length;
|
||||
this.scrolled = this.pagedClients.length > this.pageSize;
|
||||
}
|
||||
|
||||
async remove(organization: ProviderOrganizationOrganizationDetailsResponse) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: organization.organizationName,
|
||||
content: { key: "detachOrganizationConfirmation" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.webProviderService.detachOrganization(this.providerId, organization.id);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("detachedOrganization", organization.organizationName),
|
||||
});
|
||||
await this.load();
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -79,9 +81,12 @@ export class SendComponent implements OnInit, OnDestroy {
|
||||
protected sendApiService: SendApiService,
|
||||
protected dialogService: DialogService,
|
||||
protected toastService: ToastService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
this.policyService
|
||||
.policyAppliesToActiveUser$(PolicyType.DisableSend)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
@@ -91,7 +96,7 @@ export class SendComponent implements OnInit, OnDestroy {
|
||||
|
||||
this._searchText$
|
||||
.pipe(
|
||||
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
|
||||
switchMap((searchText) => from(this.searchService.isSearchable(userId, searchText))),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((isSearchable) => {
|
||||
|
||||
@@ -27,6 +27,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected searchPending = false;
|
||||
|
||||
private userId: UserId;
|
||||
private destroy$ = new Subject<void>();
|
||||
private searchTimeout: any = null;
|
||||
private isSearchable: boolean = false;
|
||||
@@ -44,10 +45,12 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
protected accountService: AccountService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
async ngOnInit() {
|
||||
this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
this._searchText$
|
||||
.pipe(
|
||||
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
|
||||
switchMap((searchText) => from(this.searchService.isSearchable(this.userId, searchText))),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((isSearchable) => {
|
||||
@@ -133,6 +136,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
this.ciphers = await this.searchService.searchCiphers(
|
||||
this.userId,
|
||||
this.searchText,
|
||||
[this.filter, this.deletedFilter],
|
||||
indexedCiphers,
|
||||
|
||||
@@ -3,16 +3,21 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { SendView } from "../tools/send/models/view/send.view";
|
||||
import { IndexedEntityId } from "../types/guid";
|
||||
import { IndexedEntityId, UserId } from "../types/guid";
|
||||
import { CipherView } from "../vault/models/view/cipher.view";
|
||||
|
||||
export abstract class SearchService {
|
||||
indexedEntityId$: Observable<IndexedEntityId | null>;
|
||||
indexedEntityId$: (userId: UserId) => Observable<IndexedEntityId | null>;
|
||||
|
||||
clearIndex: () => Promise<void>;
|
||||
isSearchable: (query: string) => Promise<boolean>;
|
||||
indexCiphers: (ciphersToIndex: CipherView[], indexedEntityGuid?: string) => Promise<void>;
|
||||
clearIndex: (userId: UserId) => Promise<void>;
|
||||
isSearchable: (userId: UserId, query: string) => Promise<boolean>;
|
||||
indexCiphers: (
|
||||
userId: UserId,
|
||||
ciphersToIndex: CipherView[],
|
||||
indexedEntityGuid?: string,
|
||||
) => Promise<void>;
|
||||
searchCiphers: (
|
||||
userId: UserId,
|
||||
query: string,
|
||||
filter?: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[],
|
||||
ciphers?: CipherView[],
|
||||
|
||||
@@ -138,10 +138,11 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
||||
);
|
||||
|
||||
if (userId == null || userId === currentUserId) {
|
||||
await this.searchService.clearIndex();
|
||||
await this.collectionService.clearActiveUserCache();
|
||||
}
|
||||
|
||||
await this.searchService.clearIndex(lockingUserId);
|
||||
|
||||
await this.folderService.clearDecryptedFolderState(lockingUserId);
|
||||
await this.masterPasswordService.clearMasterKey(lockingUserId);
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@ import { UriMatchStrategy } from "../models/domain/domain-service";
|
||||
import { I18nService } from "../platform/abstractions/i18n.service";
|
||||
import { LogService } from "../platform/abstractions/log.service";
|
||||
import {
|
||||
ActiveUserState,
|
||||
SingleUserState,
|
||||
StateProvider,
|
||||
UserKeyDefinition,
|
||||
VAULT_SEARCH_MEMORY,
|
||||
} from "../platform/state";
|
||||
import { SendView } from "../tools/send/models/view/send.view";
|
||||
import { IndexedEntityId } from "../types/guid";
|
||||
import { IndexedEntityId, UserId } from "../types/guid";
|
||||
import { FieldType } from "../vault/enums";
|
||||
import { CipherType } from "../vault/enums/cipher-type";
|
||||
import { CipherView } from "../vault/models/view/cipher.view";
|
||||
@@ -70,24 +70,6 @@ export const LUNR_SEARCH_INDEXING = new UserKeyDefinition<boolean>(
|
||||
export class SearchService implements SearchServiceAbstraction {
|
||||
private static registeredPipeline = false;
|
||||
|
||||
private searchIndexState: ActiveUserState<SerializedLunrIndex> =
|
||||
this.stateProvider.getActive(LUNR_SEARCH_INDEX);
|
||||
private readonly index$: Observable<lunr.Index | null> = this.searchIndexState.state$.pipe(
|
||||
map((searchIndex) => (searchIndex ? lunr.Index.load(searchIndex) : null)),
|
||||
);
|
||||
|
||||
private searchIndexEntityIdState: ActiveUserState<IndexedEntityId> = this.stateProvider.getActive(
|
||||
LUNR_SEARCH_INDEXED_ENTITY_ID,
|
||||
);
|
||||
readonly indexedEntityId$: Observable<IndexedEntityId | null> =
|
||||
this.searchIndexEntityIdState.state$.pipe(map((id) => id));
|
||||
|
||||
private searchIsIndexingState: ActiveUserState<boolean> =
|
||||
this.stateProvider.getActive(LUNR_SEARCH_INDEXING);
|
||||
private readonly searchIsIndexing$: Observable<boolean> = this.searchIsIndexingState.state$.pipe(
|
||||
map((indexing) => indexing ?? false),
|
||||
);
|
||||
|
||||
private readonly immediateSearchLocales: string[] = ["zh-CN", "zh-TW", "ja", "ko", "vi"];
|
||||
private readonly defaultSearchableMinLength: number = 2;
|
||||
private searchableMinLength: number = this.defaultSearchableMinLength;
|
||||
@@ -114,15 +96,41 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
async clearIndex(): Promise<void> {
|
||||
await this.searchIndexEntityIdState.update(() => null);
|
||||
await this.searchIndexState.update(() => null);
|
||||
await this.searchIsIndexingState.update(() => null);
|
||||
private searchIndexState(userId: UserId): SingleUserState<SerializedLunrIndex> {
|
||||
return this.stateProvider.getUser(userId, LUNR_SEARCH_INDEX);
|
||||
}
|
||||
|
||||
async isSearchable(query: string): Promise<boolean> {
|
||||
private index$(userId: UserId): Observable<lunr.Index | null> {
|
||||
return this.searchIndexState(userId).state$.pipe(
|
||||
map((searchIndex) => (searchIndex ? lunr.Index.load(searchIndex) : null)),
|
||||
);
|
||||
}
|
||||
|
||||
private searchIndexEntityIdState(userId: UserId): SingleUserState<IndexedEntityId | null> {
|
||||
return this.stateProvider.getUser(userId, LUNR_SEARCH_INDEXED_ENTITY_ID);
|
||||
}
|
||||
|
||||
indexedEntityId$(userId: UserId): Observable<IndexedEntityId | null> {
|
||||
return this.searchIndexEntityIdState(userId).state$.pipe(map((id) => id));
|
||||
}
|
||||
|
||||
private searchIsIndexingState(userId: UserId): SingleUserState<boolean> {
|
||||
return this.stateProvider.getUser(userId, LUNR_SEARCH_INDEXING);
|
||||
}
|
||||
|
||||
private searchIsIndexing$(userId: UserId): Observable<boolean> {
|
||||
return this.searchIsIndexingState(userId).state$.pipe(map((indexing) => indexing ?? false));
|
||||
}
|
||||
|
||||
async clearIndex(userId: UserId): Promise<void> {
|
||||
await this.searchIndexEntityIdState(userId).update(() => null);
|
||||
await this.searchIndexState(userId).update(() => null);
|
||||
await this.searchIsIndexingState(userId).update(() => null);
|
||||
}
|
||||
|
||||
async isSearchable(userId: UserId, query: string): Promise<boolean> {
|
||||
query = SearchService.normalizeSearchQuery(query);
|
||||
const index = await this.getIndexForSearch();
|
||||
const index = await this.getIndexForSearch(userId);
|
||||
const notSearchable =
|
||||
query == null ||
|
||||
(index == null && query.length < this.searchableMinLength) ||
|
||||
@@ -130,13 +138,17 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
return !notSearchable;
|
||||
}
|
||||
|
||||
async indexCiphers(ciphers: CipherView[], indexedEntityId?: string): Promise<void> {
|
||||
if (await this.getIsIndexing()) {
|
||||
async indexCiphers(
|
||||
userId: UserId,
|
||||
ciphers: CipherView[],
|
||||
indexedEntityId?: string,
|
||||
): Promise<void> {
|
||||
if (await this.getIsIndexing(userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.setIsIndexing(true);
|
||||
await this.setIndexedEntityIdForSearch(indexedEntityId as IndexedEntityId);
|
||||
await this.setIsIndexing(userId, true);
|
||||
await this.setIndexedEntityIdForSearch(userId, indexedEntityId as IndexedEntityId);
|
||||
const builder = new lunr.Builder();
|
||||
builder.pipeline.add(this.normalizeAccentsPipelineFunction);
|
||||
builder.ref("id");
|
||||
@@ -172,14 +184,15 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
ciphers.forEach((c) => builder.add(c));
|
||||
const index = builder.build();
|
||||
|
||||
await this.setIndexForSearch(index.toJSON() as SerializedLunrIndex);
|
||||
await this.setIndexForSearch(userId, index.toJSON() as SerializedLunrIndex);
|
||||
|
||||
await this.setIsIndexing(false);
|
||||
await this.setIsIndexing(userId, false);
|
||||
|
||||
this.logService.info("Finished search indexing");
|
||||
}
|
||||
|
||||
async searchCiphers(
|
||||
userId: UserId,
|
||||
query: string,
|
||||
filter: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[] = null,
|
||||
ciphers: CipherView[],
|
||||
@@ -202,18 +215,18 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean);
|
||||
}
|
||||
|
||||
if (!(await this.isSearchable(query))) {
|
||||
if (!(await this.isSearchable(userId, query))) {
|
||||
return ciphers;
|
||||
}
|
||||
|
||||
if (await this.getIsIndexing()) {
|
||||
if (await this.getIsIndexing(userId)) {
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
if (await this.getIsIndexing()) {
|
||||
if (await this.getIsIndexing(userId)) {
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
}
|
||||
|
||||
const index = await this.getIndexForSearch();
|
||||
const index = await this.getIndexForSearch(userId);
|
||||
if (index == null) {
|
||||
// Fall back to basic search if index is not available
|
||||
return this.searchCiphersBasic(ciphers, query);
|
||||
@@ -307,24 +320,27 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
return sendsMatched.concat(lowPriorityMatched);
|
||||
}
|
||||
|
||||
async getIndexForSearch(): Promise<lunr.Index | null> {
|
||||
return await firstValueFrom(this.index$);
|
||||
async getIndexForSearch(userId: UserId): Promise<lunr.Index | null> {
|
||||
return await firstValueFrom(this.index$(userId));
|
||||
}
|
||||
|
||||
private async setIndexForSearch(index: SerializedLunrIndex): Promise<void> {
|
||||
await this.searchIndexState.update(() => index);
|
||||
private async setIndexForSearch(userId: UserId, index: SerializedLunrIndex): Promise<void> {
|
||||
await this.searchIndexState(userId).update(() => index);
|
||||
}
|
||||
|
||||
private async setIndexedEntityIdForSearch(indexedEntityId: IndexedEntityId): Promise<void> {
|
||||
await this.searchIndexEntityIdState.update(() => indexedEntityId);
|
||||
private async setIndexedEntityIdForSearch(
|
||||
userId: UserId,
|
||||
indexedEntityId: IndexedEntityId,
|
||||
): Promise<void> {
|
||||
await this.searchIndexEntityIdState(userId).update(() => indexedEntityId);
|
||||
}
|
||||
|
||||
private async setIsIndexing(indexing: boolean): Promise<void> {
|
||||
await this.searchIsIndexingState.update(() => indexing);
|
||||
private async setIsIndexing(userId: UserId, indexing: boolean): Promise<void> {
|
||||
await this.searchIsIndexingState(userId).update(() => indexing);
|
||||
}
|
||||
|
||||
private async getIsIndexing(): Promise<boolean> {
|
||||
return await firstValueFrom(this.searchIsIndexing$);
|
||||
private async getIsIndexing(userId: UserId): Promise<boolean> {
|
||||
return await firstValueFrom(this.searchIsIndexing$(userId));
|
||||
}
|
||||
|
||||
private fieldExtractor(c: CipherView, joined: boolean) {
|
||||
|
||||
@@ -363,7 +363,8 @@ describe("Cipher Service", () => {
|
||||
configService.getFeatureFlag.mockResolvedValue(true);
|
||||
configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(true));
|
||||
|
||||
searchService.indexedEntityId$ = of(null);
|
||||
searchService.indexedEntityId$.mockReturnValue(of(null));
|
||||
|
||||
stateService.getUserId.mockResolvedValue(mockUserId);
|
||||
|
||||
const keys = {
|
||||
|
||||
@@ -165,9 +165,9 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
if (this.searchService != null) {
|
||||
if (value == null) {
|
||||
await this.searchService.clearIndex();
|
||||
await this.searchService.clearIndex(userId);
|
||||
} else {
|
||||
await this.searchService.indexCiphers(value);
|
||||
await this.searchService.indexCiphers(userId, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -480,9 +480,9 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
private async reindexCiphers(userId: UserId) {
|
||||
const reindexRequired =
|
||||
this.searchService != null &&
|
||||
((await firstValueFrom(this.searchService.indexedEntityId$)) ?? userId) !== userId;
|
||||
((await firstValueFrom(this.searchService.indexedEntityId$(userId))) ?? userId) !== userId;
|
||||
if (reindexRequired) {
|
||||
await this.searchService.indexCiphers(await this.getDecryptedCiphers(userId), userId);
|
||||
await this.searchService.indexCiphers(userId, await this.getDecryptedCiphers(userId), userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,12 @@ import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, first, Subject } from "rxjs";
|
||||
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { mockAccountServiceWith } from "../../../../../common/spec";
|
||||
|
||||
import { SendItemsService } from "./send-items.service";
|
||||
import { SendListFiltersService } from "./send-list-filters.service";
|
||||
@@ -30,6 +34,7 @@ describe("SendItemsService", () => {
|
||||
{ provide: SendService, useValue: sendServiceMock },
|
||||
{ provide: SendListFiltersService, useValue: sendListFiltersServiceMock },
|
||||
{ provide: SearchService, useValue: searchServiceMock },
|
||||
{ provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) },
|
||||
SendItemsService,
|
||||
],
|
||||
});
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
} from "rxjs";
|
||||
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
|
||||
@@ -71,9 +73,13 @@ export class SendItemsService {
|
||||
/**
|
||||
* Observable that indicates whether a filter is currently applied to the sends.
|
||||
*/
|
||||
hasFilterApplied$ = combineLatest([this._searchText$, this.sendListFiltersService.filters$]).pipe(
|
||||
switchMap(([searchText, filters]) => {
|
||||
return from(this.searchService.isSearchable(searchText)).pipe(
|
||||
hasFilterApplied$ = combineLatest([
|
||||
this._searchText$,
|
||||
this.sendListFiltersService.filters$,
|
||||
getUserId(this.accountService.activeAccount$),
|
||||
]).pipe(
|
||||
switchMap(([searchText, filters, activeAcctId]) => {
|
||||
return from(this.searchService.isSearchable(activeAcctId, searchText)).pipe(
|
||||
map(
|
||||
(isSearchable) =>
|
||||
isSearchable || Object.values(filters).some((filter) => filter !== null),
|
||||
@@ -98,6 +104,7 @@ export class SendItemsService {
|
||||
private sendService: SendService,
|
||||
private sendListFiltersService: SendListFiltersService,
|
||||
private searchService: SearchService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
applyFilter(newSearchText: string) {
|
||||
|
||||
Reference in New Issue
Block a user