mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-22 11:45:59 +01:00
[PM-6194] Refactor injection of services in browser services module (#8380)
* refactored injector of services on the browser service module * refactored the search and popup serach service to use state provider * renamed back to default * removed token service that was readded during merge conflict * Updated search service construction on the cli * updated to use user key definition * Reafctored all components that refernce issearchable * removed commented variable * added uncommited code to remove dependencies not needed anymore * added uncommited code to remove dependencies not needed anymore
This commit is contained in:
parent
560033cb88
commit
2bce6c538c
@ -514,7 +514,7 @@ export default class MainBackground {
|
|||||||
this.apiService,
|
this.apiService,
|
||||||
this.fileUploadService,
|
this.fileUploadService,
|
||||||
);
|
);
|
||||||
this.searchService = new SearchService(this.logService, this.i18nService);
|
this.searchService = new SearchService(this.logService, this.i18nService, this.stateProvider);
|
||||||
|
|
||||||
this.collectionService = new CollectionService(
|
this.collectionService = new CollectionService(
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
@ -1177,7 +1177,7 @@ export default class MainBackground {
|
|||||||
const newActiveUser = await this.stateService.clean({ userId: userId });
|
const newActiveUser = await this.stateService.clean({ userId: userId });
|
||||||
|
|
||||||
if (userId == null || userId === currentUserId) {
|
if (userId == null || userId === currentUserId) {
|
||||||
this.searchService.clearIndex();
|
await this.searchService.clearIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.stateEventRunnerService.handleEvent("logout", currentUserId as UserId);
|
await this.stateEventRunnerService.handleEvent("logout", currentUserId as UserId);
|
||||||
|
@ -14,12 +14,17 @@ import {
|
|||||||
logServiceFactory,
|
logServiceFactory,
|
||||||
LogServiceInitOptions,
|
LogServiceInitOptions,
|
||||||
} from "../../platform/background/service-factories/log-service.factory";
|
} from "../../platform/background/service-factories/log-service.factory";
|
||||||
|
import {
|
||||||
|
stateProviderFactory,
|
||||||
|
StateProviderInitOptions,
|
||||||
|
} from "../../platform/background/service-factories/state-provider.factory";
|
||||||
|
|
||||||
type SearchServiceFactoryOptions = FactoryOptions;
|
type SearchServiceFactoryOptions = FactoryOptions;
|
||||||
|
|
||||||
export type SearchServiceInitOptions = SearchServiceFactoryOptions &
|
export type SearchServiceInitOptions = SearchServiceFactoryOptions &
|
||||||
LogServiceInitOptions &
|
LogServiceInitOptions &
|
||||||
I18nServiceInitOptions;
|
I18nServiceInitOptions &
|
||||||
|
StateProviderInitOptions;
|
||||||
|
|
||||||
export function searchServiceFactory(
|
export function searchServiceFactory(
|
||||||
cache: { searchService?: AbstractSearchService } & CachedServices,
|
cache: { searchService?: AbstractSearchService } & CachedServices,
|
||||||
@ -33,6 +38,7 @@ export function searchServiceFactory(
|
|||||||
new SearchService(
|
new SearchService(
|
||||||
await logServiceFactory(cache, opts),
|
await logServiceFactory(cache, opts),
|
||||||
await i18nServiceFactory(cache, opts),
|
await i18nServiceFactory(cache, opts),
|
||||||
|
await stateProviderFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,14 @@
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
import { SearchService } from "@bitwarden/common/services/search.service";
|
import { SearchService } from "@bitwarden/common/services/search.service";
|
||||||
|
|
||||||
export class PopupSearchService extends SearchService {
|
export class PopupSearchService extends SearchService {
|
||||||
constructor(
|
constructor(logService: LogService, i18nService: I18nService, stateProvider: StateProvider) {
|
||||||
private mainSearchService: SearchService,
|
super(logService, i18nService, stateProvider);
|
||||||
logService: LogService,
|
|
||||||
i18nService: I18nService,
|
|
||||||
) {
|
|
||||||
super(logService, i18nService);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearIndex() {
|
clearIndex(): Promise<void> {
|
||||||
throw new Error("Not available.");
|
throw new Error("Not available.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,7 +16,7 @@ export class PopupSearchService extends SearchService {
|
|||||||
throw new Error("Not available.");
|
throw new Error("Not available.");
|
||||||
}
|
}
|
||||||
|
|
||||||
getIndexForSearch() {
|
async getIndexForSearch() {
|
||||||
return this.mainSearchService.getIndexForSearch();
|
return await super.getIndexForSearch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,17 +74,15 @@ import {
|
|||||||
GlobalStateProvider,
|
GlobalStateProvider,
|
||||||
StateProvider,
|
StateProvider,
|
||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
import { SearchService } from "@bitwarden/common/services/search.service";
|
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||||
import { CipherFileUploadService } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
|
|
||||||
import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||||
|
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
|
|
||||||
|
|
||||||
import { UnauthGuardService } from "../../auth/popup/services";
|
import { UnauthGuardService } from "../../auth/popup/services";
|
||||||
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
|
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
|
||||||
@ -187,19 +185,8 @@ const safeProviders: SafeProvider[] = [
|
|||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: SearchServiceAbstraction,
|
provide: SearchServiceAbstraction,
|
||||||
useFactory: (logService: LogService, i18nService: I18nServiceAbstraction) => {
|
useClass: PopupSearchService,
|
||||||
return new PopupSearchService(
|
deps: [LogService, I18nServiceAbstraction, StateProvider],
|
||||||
getBgService<SearchService>("searchService")(),
|
|
||||||
logService,
|
|
||||||
i18nService,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
deps: [LogService, I18nServiceAbstraction],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
|
||||||
provide: CipherFileUploadService,
|
|
||||||
useFactory: getBgService<CipherFileUploadService>("cipherFileUploadService"),
|
|
||||||
deps: [],
|
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: CipherService,
|
provide: CipherService,
|
||||||
@ -231,11 +218,6 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: BrowserEnvironmentService,
|
useClass: BrowserEnvironmentService,
|
||||||
deps: [LogService, StateProvider, AccountServiceAbstraction],
|
deps: [LogService, StateProvider, AccountServiceAbstraction],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
|
||||||
provide: TotpService,
|
|
||||||
useFactory: getBgService<TotpService>("totpService"),
|
|
||||||
deps: [],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: I18nServiceAbstraction,
|
provide: I18nServiceAbstraction,
|
||||||
useFactory: (globalStateProvider: GlobalStateProvider) => {
|
useFactory: (globalStateProvider: GlobalStateProvider) => {
|
||||||
@ -252,6 +234,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
},
|
},
|
||||||
deps: [EncryptService],
|
deps: [EncryptService],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: TotpServiceAbstraction,
|
||||||
|
useClass: TotpService,
|
||||||
|
deps: [CryptoFunctionService, LogService],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: AuthRequestServiceAbstraction,
|
provide: AuthRequestServiceAbstraction,
|
||||||
useFactory: getBgService<AuthRequestServiceAbstraction>("authRequestService"),
|
useFactory: getBgService<AuthRequestServiceAbstraction>("authRequestService"),
|
||||||
@ -333,11 +320,6 @@ const safeProviders: SafeProvider[] = [
|
|||||||
BillingAccountProfileStateService,
|
BillingAccountProfileStateService,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
|
||||||
provide: VaultExportServiceAbstraction,
|
|
||||||
useFactory: getBgService<VaultExportServiceAbstraction>("exportService"),
|
|
||||||
deps: [],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: KeyConnectorService,
|
provide: KeyConnectorService,
|
||||||
useFactory: getBgService<KeyConnectorService>("keyConnectorService"),
|
useFactory: getBgService<KeyConnectorService>("keyConnectorService"),
|
||||||
|
@ -171,9 +171,7 @@ export class SendGroupingsComponent extends BaseSendComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showSearching() {
|
showSearching() {
|
||||||
return (
|
return this.hasSearched || (!this.searchPending && this.isSearchable);
|
||||||
this.hasSearched || (!this.searchPending && this.searchService.isSearchable(this.searchText))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateTypeCounts() {
|
private calculateTypeCounts() {
|
||||||
|
@ -6,7 +6,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
|
|||||||
import { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
@ -31,7 +31,7 @@ export class ActionButtonsComponent implements OnInit, OnDestroy {
|
|||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private eventCollectionService: EventCollectionService,
|
private eventCollectionService: EventCollectionService,
|
||||||
private totpService: TotpService,
|
private totpService: TotpServiceAbstraction,
|
||||||
private passwordRepromptService: PasswordRepromptService,
|
private passwordRepromptService: PasswordRepromptService,
|
||||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
) {}
|
) {}
|
||||||
|
@ -311,7 +311,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async search() {
|
protected async search() {
|
||||||
this.hasSearched = this.searchService.isSearchable(this.searchText);
|
this.hasSearched = await this.searchService.isSearchable(this.searchText);
|
||||||
this.searchPending = true;
|
this.searchPending = true;
|
||||||
if (this.hasSearched) {
|
if (this.hasSearched) {
|
||||||
this.displayedCiphers = await this.searchService.searchCiphers(
|
this.displayedCiphers = await this.searchService.searchCiphers(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { Subject, firstValueFrom } from "rxjs";
|
import { Subject, firstValueFrom, from } from "rxjs";
|
||||||
import { debounceTime, takeUntil } from "rxjs/operators";
|
import { debounceTime, switchMap, takeUntil } from "rxjs/operators";
|
||||||
|
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
@ -120,8 +120,14 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.search$
|
this.search$
|
||||||
.pipe(debounceTime(500), takeUntil(this.destroy$))
|
.pipe(
|
||||||
.subscribe(() => this.searchVault());
|
debounceTime(500),
|
||||||
|
switchMap(() => {
|
||||||
|
return from(this.searchVault());
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
const autofillOnPageLoadOrgPolicy = await firstValueFrom(
|
const autofillOnPageLoadOrgPolicy = await firstValueFrom(
|
||||||
this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$,
|
this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$,
|
||||||
@ -232,14 +238,12 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
searchVault() {
|
async searchVault() {
|
||||||
if (!this.searchService.isSearchable(this.searchText)) {
|
if (!(await this.searchService.isSearchable(this.searchText))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
await this.router.navigate(["/tabs/vault"], { queryParams: { searchText: this.searchText } });
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate(["/tabs/vault"], { queryParams: { searchText: this.searchText } });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
closeOnEsc(e: KeyboardEvent) {
|
closeOnEsc(e: KeyboardEvent) {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Location } from "@angular/common";
|
import { Location } from "@angular/common";
|
||||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs";
|
||||||
import { first } from "rxjs/operators";
|
import { first, switchMap, takeUntil } from "rxjs/operators";
|
||||||
|
|
||||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
@ -53,7 +53,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
folderCounts = new Map<string, number>();
|
folderCounts = new Map<string, number>();
|
||||||
collectionCounts = new Map<string, number>();
|
collectionCounts = new Map<string, number>();
|
||||||
typeCounts = new Map<CipherType, number>();
|
typeCounts = new Map<CipherType, number>();
|
||||||
searchText: string;
|
|
||||||
state: BrowserGroupingsComponentState;
|
state: BrowserGroupingsComponentState;
|
||||||
showLeftHeader = true;
|
showLeftHeader = true;
|
||||||
searchPending = false;
|
searchPending = false;
|
||||||
@ -71,6 +70,16 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
private hasSearched = false;
|
private hasSearched = false;
|
||||||
private hasLoadedAllCiphers = false;
|
private hasLoadedAllCiphers = false;
|
||||||
private allCiphers: CipherView[] = null;
|
private allCiphers: CipherView[] = null;
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
private _searchText$ = new BehaviorSubject<string>("");
|
||||||
|
private isSearchable: boolean = false;
|
||||||
|
|
||||||
|
get searchText() {
|
||||||
|
return this._searchText$.value;
|
||||||
|
}
|
||||||
|
set searchText(value: string) {
|
||||||
|
this._searchText$.next(value);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
@ -148,6 +157,15 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY);
|
BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._searchText$
|
||||||
|
.pipe(
|
||||||
|
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe((isSearchable) => {
|
||||||
|
this.isSearchable = isSearchable;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@ -161,6 +179,8 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.saveState();
|
this.saveState();
|
||||||
this.broadcasterService.unsubscribe(ComponentId);
|
this.broadcasterService.unsubscribe(ComponentId);
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
@ -181,7 +201,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
async loadCiphers() {
|
async loadCiphers() {
|
||||||
this.allCiphers = await this.cipherService.getAllDecrypted();
|
this.allCiphers = await this.cipherService.getAllDecrypted();
|
||||||
if (!this.hasLoadedAllCiphers) {
|
if (!this.hasLoadedAllCiphers) {
|
||||||
this.hasLoadedAllCiphers = !this.searchService.isSearchable(this.searchText);
|
this.hasLoadedAllCiphers = !(await this.searchService.isSearchable(this.searchText));
|
||||||
}
|
}
|
||||||
await this.search(null);
|
await this.search(null);
|
||||||
this.getCounts();
|
this.getCounts();
|
||||||
@ -210,7 +230,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
const filterDeleted = (c: CipherView) => !c.isDeleted;
|
const filterDeleted = (c: CipherView) => !c.isDeleted;
|
||||||
if (timeout == null) {
|
if (timeout == null) {
|
||||||
this.hasSearched = this.searchService.isSearchable(this.searchText);
|
this.hasSearched = this.isSearchable;
|
||||||
this.ciphers = await this.searchService.searchCiphers(
|
this.ciphers = await this.searchService.searchCiphers(
|
||||||
this.searchText,
|
this.searchText,
|
||||||
filterDeleted,
|
filterDeleted,
|
||||||
@ -223,7 +243,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.searchPending = true;
|
this.searchPending = true;
|
||||||
this.searchTimeout = setTimeout(async () => {
|
this.searchTimeout = setTimeout(async () => {
|
||||||
this.hasSearched = this.searchService.isSearchable(this.searchText);
|
this.hasSearched = this.isSearchable;
|
||||||
if (!this.hasLoadedAllCiphers && !this.hasSearched) {
|
if (!this.hasLoadedAllCiphers && !this.hasSearched) {
|
||||||
await this.loadCiphers();
|
await this.loadCiphers();
|
||||||
} else {
|
} else {
|
||||||
@ -381,9 +401,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showSearching() {
|
showSearching() {
|
||||||
return (
|
return this.hasSearched || (!this.searchPending && this.isSearchable);
|
||||||
this.hasSearched || (!this.searchPending && this.searchService.isSearchable(this.searchText))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
closeOnEsc(e: KeyboardEvent) {
|
closeOnEsc(e: KeyboardEvent) {
|
||||||
|
@ -20,7 +20,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||||
@ -74,7 +74,7 @@ export class ViewComponent extends BaseViewComponent {
|
|||||||
constructor(
|
constructor(
|
||||||
cipherService: CipherService,
|
cipherService: CipherService,
|
||||||
folderService: FolderService,
|
folderService: FolderService,
|
||||||
totpService: TotpService,
|
totpService: TotpServiceAbstraction,
|
||||||
tokenService: TokenService,
|
tokenService: TokenService,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
cryptoService: CryptoService,
|
cryptoService: CryptoService,
|
||||||
|
@ -414,7 +414,7 @@ export class Main {
|
|||||||
this.sendService,
|
this.sendService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.searchService = new SearchService(this.logService, this.i18nService);
|
this.searchService = new SearchService(this.logService, this.i18nService, this.stateProvider);
|
||||||
|
|
||||||
this.broadcasterService = new BroadcasterService();
|
this.broadcasterService = new BroadcasterService();
|
||||||
|
|
||||||
|
@ -609,7 +609,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
// This must come last otherwise the logout will prematurely trigger
|
// This must come last otherwise the logout will prematurely trigger
|
||||||
// a process reload before all the state service user data can be cleaned up
|
// a process reload before all the state service user data can be cleaned up
|
||||||
if (userBeingLoggedOut === preLogoutActiveUserId) {
|
if (userBeingLoggedOut === preLogoutActiveUserId) {
|
||||||
this.searchService.clearIndex();
|
await this.searchService.clearIndex();
|
||||||
this.authService.logOut(async () => {
|
this.authService.logOut(async () => {
|
||||||
if (expired) {
|
if (expired) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Directive, ViewChild, ViewContainerRef } from "@angular/core";
|
import { Directive, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { BehaviorSubject, Subject, firstValueFrom, from, switchMap, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
||||||
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
|
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
|
||||||
@ -33,7 +33,9 @@ const MaxCheckedCount = 500;
|
|||||||
@Directive()
|
@Directive()
|
||||||
export abstract class BasePeopleComponent<
|
export abstract class BasePeopleComponent<
|
||||||
UserType extends ProviderUserUserDetailsResponse | OrganizationUserView,
|
UserType extends ProviderUserUserDetailsResponse | OrganizationUserView,
|
||||||
> {
|
>
|
||||||
|
implements OnInit, OnDestroy
|
||||||
|
{
|
||||||
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
|
||||||
confirmModalRef: ViewContainerRef;
|
confirmModalRef: ViewContainerRef;
|
||||||
|
|
||||||
@ -88,7 +90,6 @@ export abstract class BasePeopleComponent<
|
|||||||
status: StatusType;
|
status: StatusType;
|
||||||
users: UserType[] = [];
|
users: UserType[] = [];
|
||||||
pagedUsers: UserType[] = [];
|
pagedUsers: UserType[] = [];
|
||||||
searchText: string;
|
|
||||||
actionPromise: Promise<void>;
|
actionPromise: Promise<void>;
|
||||||
|
|
||||||
protected allUsers: UserType[] = [];
|
protected allUsers: UserType[] = [];
|
||||||
@ -97,7 +98,19 @@ export abstract class BasePeopleComponent<
|
|||||||
protected didScroll = false;
|
protected didScroll = false;
|
||||||
protected pageSize = 100;
|
protected pageSize = 100;
|
||||||
|
|
||||||
|
protected destroy$ = new Subject<void>();
|
||||||
|
|
||||||
private pagedUsersCount = 0;
|
private pagedUsersCount = 0;
|
||||||
|
private _searchText$ = new BehaviorSubject<string>("");
|
||||||
|
private isSearching: boolean = false;
|
||||||
|
|
||||||
|
get searchText() {
|
||||||
|
return this._searchText$.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set searchText(value: string) {
|
||||||
|
this._searchText$.next(value);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected apiService: ApiService,
|
protected apiService: ApiService,
|
||||||
@ -122,6 +135,22 @@ export abstract class BasePeopleComponent<
|
|||||||
abstract reinviteUser(id: string): Promise<void>;
|
abstract reinviteUser(id: string): Promise<void>;
|
||||||
abstract confirmUser(user: UserType, publicKey: Uint8Array): Promise<void>;
|
abstract confirmUser(user: UserType, publicKey: Uint8Array): Promise<void>;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this._searchText$
|
||||||
|
.pipe(
|
||||||
|
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe((isSearchable) => {
|
||||||
|
this.isSearching = isSearchable;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
const response = await this.getUsers();
|
const response = await this.getUsers();
|
||||||
this.statusMap.clear();
|
this.statusMap.clear();
|
||||||
@ -390,12 +419,8 @@ export abstract class BasePeopleComponent<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isSearching() {
|
|
||||||
return this.searchService.isSearchable(this.searchText);
|
|
||||||
}
|
|
||||||
|
|
||||||
isPaging() {
|
isPaging() {
|
||||||
const searching = this.isSearching();
|
const searching = this.isSearching;
|
||||||
if (searching && this.didScroll) {
|
if (searching && this.didScroll) {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
@ -91,15 +91,16 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||||||
private pagedGroupsCount = 0;
|
private pagedGroupsCount = 0;
|
||||||
private pagedGroups: GroupDetailsRow[];
|
private pagedGroups: GroupDetailsRow[];
|
||||||
private searchedGroups: GroupDetailsRow[];
|
private searchedGroups: GroupDetailsRow[];
|
||||||
private _searchText: string;
|
private _searchText$ = new BehaviorSubject<string>("");
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
private refreshGroups$ = new BehaviorSubject<void>(null);
|
private refreshGroups$ = new BehaviorSubject<void>(null);
|
||||||
|
private isSearching: boolean = false;
|
||||||
|
|
||||||
get searchText() {
|
get searchText() {
|
||||||
return this._searchText;
|
return this._searchText$.value;
|
||||||
}
|
}
|
||||||
set searchText(value: string) {
|
set searchText(value: string) {
|
||||||
this._searchText = value;
|
this._searchText$.next(value);
|
||||||
// Manually update as we are not using the search pipe in the template
|
// Manually update as we are not using the search pipe in the template
|
||||||
this.updateSearchedGroups();
|
this.updateSearchedGroups();
|
||||||
}
|
}
|
||||||
@ -114,7 +115,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||||||
if (this.isPaging()) {
|
if (this.isPaging()) {
|
||||||
return this.pagedGroups;
|
return this.pagedGroups;
|
||||||
}
|
}
|
||||||
if (this.isSearching()) {
|
if (this.isSearching) {
|
||||||
return this.searchedGroups;
|
return this.searchedGroups;
|
||||||
}
|
}
|
||||||
return this.groups;
|
return this.groups;
|
||||||
@ -180,6 +181,15 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||||||
takeUntil(this.destroy$),
|
takeUntil(this.destroy$),
|
||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
|
|
||||||
|
this._searchText$
|
||||||
|
.pipe(
|
||||||
|
switchMap((searchText) => this.searchService.isSearchable(searchText)),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe((isSearchable) => {
|
||||||
|
this.isSearching = isSearchable;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@ -297,10 +307,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||||||
this.loadMore();
|
this.loadMore();
|
||||||
}
|
}
|
||||||
|
|
||||||
isSearching() {
|
|
||||||
return this.searchService.isSearchable(this.searchText);
|
|
||||||
}
|
|
||||||
|
|
||||||
check(groupRow: GroupDetailsRow) {
|
check(groupRow: GroupDetailsRow) {
|
||||||
groupRow.checked = !groupRow.checked;
|
groupRow.checked = !groupRow.checked;
|
||||||
}
|
}
|
||||||
@ -310,7 +316,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isPaging() {
|
isPaging() {
|
||||||
const searching = this.isSearching();
|
const searching = this.isSearching;
|
||||||
if (searching && this.didScroll) {
|
if (searching && this.didScroll) {
|
||||||
this.resetPaging();
|
this.resetPaging();
|
||||||
}
|
}
|
||||||
@ -340,7 +346,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateSearchedGroups() {
|
private updateSearchedGroups() {
|
||||||
if (this.searchService.isSearchable(this.searchText)) {
|
if (this.isSearching) {
|
||||||
// Making use of the pipe in the component as we need know which groups where filtered
|
// Making use of the pipe in the component as we need know which groups where filtered
|
||||||
this.searchedGroups = this.searchPipe.transform(
|
this.searchedGroups = this.searchPipe.transform(
|
||||||
this.groups,
|
this.groups,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import {
|
import {
|
||||||
combineLatest,
|
combineLatest,
|
||||||
@ -9,7 +9,6 @@ import {
|
|||||||
map,
|
map,
|
||||||
Observable,
|
Observable,
|
||||||
shareReplay,
|
shareReplay,
|
||||||
Subject,
|
|
||||||
switchMap,
|
switchMap,
|
||||||
takeUntil,
|
takeUntil,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
@ -73,10 +72,7 @@ import { ResetPasswordComponent } from "./components/reset-password.component";
|
|||||||
selector: "app-org-people",
|
selector: "app-org-people",
|
||||||
templateUrl: "people.component.html",
|
templateUrl: "people.component.html",
|
||||||
})
|
})
|
||||||
export class PeopleComponent
|
export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> {
|
||||||
extends BasePeopleComponent<OrganizationUserView>
|
|
||||||
implements OnInit, OnDestroy
|
|
||||||
{
|
|
||||||
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
|
||||||
groupsModalRef: ViewContainerRef;
|
groupsModalRef: ViewContainerRef;
|
||||||
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
|
||||||
@ -99,7 +95,6 @@ export class PeopleComponent
|
|||||||
orgResetPasswordPolicyEnabled = false;
|
orgResetPasswordPolicyEnabled = false;
|
||||||
|
|
||||||
protected canUseSecretsManager$: Observable<boolean>;
|
protected canUseSecretsManager$: Observable<boolean>;
|
||||||
private destroy$ = new Subject<void>();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
apiService: ApiService,
|
apiService: ApiService,
|
||||||
@ -210,8 +205,7 @@ export class PeopleComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.destroy$.next();
|
super.ngOnDestroy();
|
||||||
this.destroy$.complete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
|
@ -281,7 +281,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
|
await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
|
||||||
|
|
||||||
this.searchService.clearIndex();
|
await this.searchService.clearIndex();
|
||||||
this.authService.logOut(async () => {
|
this.authService.logOut(async () => {
|
||||||
if (expired) {
|
if (expired) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
|
@ -272,7 +272,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
concatMap(async ([ciphers, filter, searchText]) => {
|
concatMap(async ([ciphers, filter, searchText]) => {
|
||||||
const filterFunction = createFilterFunction(filter);
|
const filterFunction = createFilterFunction(filter);
|
||||||
|
|
||||||
if (this.searchService.isSearchable(searchText)) {
|
if (await this.searchService.isSearchable(searchText)) {
|
||||||
return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers);
|
return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,7 +283,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe(
|
const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe(
|
||||||
filter(([collections, filter]) => collections != undefined && filter != undefined),
|
filter(([collections, filter]) => collections != undefined && filter != undefined),
|
||||||
map(([collections, filter, searchText]) => {
|
concatMap(async ([collections, filter, searchText]) => {
|
||||||
if (filter.collectionId === undefined || filter.collectionId === Unassigned) {
|
if (filter.collectionId === undefined || filter.collectionId === Unassigned) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -303,7 +303,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? [];
|
collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.searchService.isSearchable(searchText)) {
|
if (await this.searchService.isSearchable(searchText)) {
|
||||||
collectionsToReturn = this.searchPipe.transform(
|
collectionsToReturn = this.searchPipe.transform(
|
||||||
collectionsToReturn,
|
collectionsToReturn,
|
||||||
searchText,
|
searchText,
|
||||||
|
@ -331,7 +331,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.searchService.indexCiphers(ciphers, organization.id);
|
await this.searchService.indexCiphers(ciphers, organization.id);
|
||||||
return ciphers;
|
return ciphers;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -350,7 +350,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe(
|
const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe(
|
||||||
filter(([collections, filter]) => collections != undefined && filter != undefined),
|
filter(([collections, filter]) => collections != undefined && filter != undefined),
|
||||||
map(([collections, filter, searchText]) => {
|
concatMap(async ([collections, filter, searchText]) => {
|
||||||
if (
|
if (
|
||||||
filter.collectionId === Unassigned ||
|
filter.collectionId === Unassigned ||
|
||||||
(filter.collectionId === undefined && filter.type !== undefined)
|
(filter.collectionId === undefined && filter.type !== undefined)
|
||||||
@ -369,7 +369,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? [];
|
collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.searchService.isSearchable(searchText)) {
|
if (await this.searchService.isSearchable(searchText)) {
|
||||||
collectionsToReturn = this.searchPipe.transform(
|
collectionsToReturn = this.searchPipe.transform(
|
||||||
collectionsToReturn,
|
collectionsToReturn,
|
||||||
searchText,
|
searchText,
|
||||||
@ -436,7 +436,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const filterFunction = createFilterFunction(filter);
|
const filterFunction = createFilterFunction(filter);
|
||||||
|
|
||||||
if (this.searchService.isSearchable(searchText)) {
|
if (await this.searchService.isSearchable(searchText)) {
|
||||||
return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers);
|
return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs";
|
||||||
import { first } from "rxjs/operators";
|
import { first, switchMap, takeUntil } from "rxjs/operators";
|
||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
@ -39,7 +39,6 @@ const DisallowedPlanTypes = [
|
|||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
export class ClientsComponent implements OnInit {
|
export class ClientsComponent implements OnInit {
|
||||||
providerId: string;
|
providerId: string;
|
||||||
searchText: string;
|
|
||||||
addableOrganizations: Organization[];
|
addableOrganizations: Organization[];
|
||||||
loading = true;
|
loading = true;
|
||||||
manageOrganizations = false;
|
manageOrganizations = false;
|
||||||
@ -57,6 +56,17 @@ export class ClientsComponent implements OnInit {
|
|||||||
FeatureFlag.EnableConsolidatedBilling,
|
FeatureFlag.EnableConsolidatedBilling,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
private _searchText$ = new BehaviorSubject<string>("");
|
||||||
|
private isSearching: boolean = false;
|
||||||
|
|
||||||
|
get searchText() {
|
||||||
|
return this._searchText$.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set searchText(value: string) {
|
||||||
|
this._searchText$.next(value);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@ -77,27 +87,41 @@ export class ClientsComponent implements OnInit {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
|
||||||
|
|
||||||
const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$);
|
const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$);
|
||||||
|
|
||||||
if (enableConsolidatedBilling) {
|
if (enableConsolidatedBilling) {
|
||||||
await this.router.navigate(["../manage-client-organizations"], { relativeTo: this.route });
|
await this.router.navigate(["../manage-client-organizations"], { relativeTo: this.route });
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
this.route.parent.params
|
||||||
this.route.parent.params.subscribe(async (params) => {
|
.pipe(
|
||||||
|
switchMap((params) => {
|
||||||
this.providerId = params.providerId;
|
this.providerId = params.providerId;
|
||||||
|
return from(this.load());
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
await this.load();
|
this.route.queryParams.pipe(first(), takeUntil(this.destroy$)).subscribe((qParams) => {
|
||||||
|
|
||||||
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
|
|
||||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
|
||||||
this.searchText = qParams.search;
|
this.searchText = qParams.search;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._searchText$
|
||||||
|
.pipe(
|
||||||
|
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe((isSearchable) => {
|
||||||
|
this.isSearching = isSearchable;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
const response = await this.apiService.getProviderClients(this.providerId);
|
const response = await this.apiService.getProviderClients(this.providerId);
|
||||||
this.clients = response.data != null && response.data.length > 0 ? response.data : [];
|
this.clients = response.data != null && response.data.length > 0 ? response.data : [];
|
||||||
@ -118,20 +142,14 @@ export class ClientsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isPaging() {
|
isPaging() {
|
||||||
const searching = this.isSearching();
|
const searching = this.isSearching;
|
||||||
if (searching && this.didScroll) {
|
if (searching && this.didScroll) {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.resetPaging();
|
this.resetPaging();
|
||||||
}
|
}
|
||||||
return !searching && this.clients && this.clients.length > this.pageSize;
|
return !searching && this.clients && this.clients.length > this.pageSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
isSearching() {
|
resetPaging() {
|
||||||
return this.searchService.isSearchable(this.searchText);
|
|
||||||
}
|
|
||||||
|
|
||||||
async resetPaging() {
|
|
||||||
this.pagedClients = [];
|
this.pagedClients = [];
|
||||||
this.loadMore();
|
this.loadMore();
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { first } from "rxjs/operators";
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
@ -34,10 +34,7 @@ import { UserAddEditComponent } from "./user-add-edit.component";
|
|||||||
templateUrl: "people.component.html",
|
templateUrl: "people.component.html",
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
export class PeopleComponent
|
export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetailsResponse> {
|
||||||
extends BasePeopleComponent<ProviderUserUserDetailsResponse>
|
|
||||||
implements OnInit
|
|
||||||
{
|
|
||||||
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||||
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
|
||||||
groupsModalRef: ViewContainerRef;
|
groupsModalRef: ViewContainerRef;
|
||||||
@ -119,6 +116,10 @@ export class PeopleComponent
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
super.ngOnDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> {
|
getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> {
|
||||||
return this.apiService.getProviderUsers(this.providerId);
|
return this.apiService.getProviderUsers(this.providerId);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { SelectionModel } from "@angular/cdk/collections";
|
import { SelectionModel } from "@angular/cdk/collections";
|
||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs";
|
||||||
import { first } from "rxjs/operators";
|
import { first, switchMap, takeUntil } from "rxjs/operators";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
@ -23,12 +23,22 @@ import { ManageClientOrganizationSubscriptionComponent } from "./manage-client-o
|
|||||||
})
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
export class ManageClientOrganizationsComponent implements OnInit {
|
export class ManageClientOrganizationsComponent implements OnInit, OnDestroy {
|
||||||
providerId: string;
|
providerId: string;
|
||||||
loading = true;
|
loading = true;
|
||||||
manageOrganizations = false;
|
manageOrganizations = false;
|
||||||
|
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
private _searchText$ = new BehaviorSubject<string>("");
|
||||||
|
private isSearching: boolean = false;
|
||||||
|
|
||||||
|
get searchText() {
|
||||||
|
return this._searchText$.value;
|
||||||
|
}
|
||||||
|
|
||||||
set searchText(search: string) {
|
set searchText(search: string) {
|
||||||
|
this._searchText$.value;
|
||||||
|
|
||||||
this.selection.clear();
|
this.selection.clear();
|
||||||
this.dataSource.filter = search;
|
this.dataSource.filter = search;
|
||||||
}
|
}
|
||||||
@ -67,6 +77,20 @@ export class ManageClientOrganizationsComponent implements OnInit {
|
|||||||
this.searchText = qParams.search;
|
this.searchText = qParams.search;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._searchText$
|
||||||
|
.pipe(
|
||||||
|
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe((isSearchable) => {
|
||||||
|
this.isSearching = isSearchable;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
@ -80,7 +104,7 @@ export class ManageClientOrganizationsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isPaging() {
|
isPaging() {
|
||||||
const searching = this.isSearching();
|
const searching = this.isSearching;
|
||||||
if (searching && this.didScroll) {
|
if (searching && this.didScroll) {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
@ -89,10 +113,6 @@ export class ManageClientOrganizationsComponent implements OnInit {
|
|||||||
return !searching && this.clients && this.clients.length > this.pageSize;
|
return !searching && this.clients && this.clients.length > this.pageSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
isSearching() {
|
|
||||||
return this.searchService.isSearchable(this.searchText);
|
|
||||||
}
|
|
||||||
|
|
||||||
async resetPaging() {
|
async resetPaging() {
|
||||||
this.pagedClients = [];
|
this.pagedClients = [];
|
||||||
this.loadMore();
|
this.loadMore();
|
||||||
|
@ -726,7 +726,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: SearchServiceAbstraction,
|
provide: SearchServiceAbstraction,
|
||||||
useClass: SearchService,
|
useClass: SearchService,
|
||||||
deps: [LogService, I18nServiceAbstraction],
|
deps: [LogService, I18nServiceAbstraction, StateProvider],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: NotificationsServiceAbstraction,
|
provide: NotificationsServiceAbstraction,
|
||||||
|
@ -1,5 +1,13 @@
|
|||||||
import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core";
|
import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { Subject, firstValueFrom, mergeMap, takeUntil } from "rxjs";
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
Subject,
|
||||||
|
firstValueFrom,
|
||||||
|
mergeMap,
|
||||||
|
from,
|
||||||
|
switchMap,
|
||||||
|
takeUntil,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
@ -24,7 +32,6 @@ export class SendComponent implements OnInit, OnDestroy {
|
|||||||
expired = false;
|
expired = false;
|
||||||
type: SendType = null;
|
type: SendType = null;
|
||||||
sends: SendView[] = [];
|
sends: SendView[] = [];
|
||||||
searchText: string;
|
|
||||||
selectedType: SendType;
|
selectedType: SendType;
|
||||||
selectedAll: boolean;
|
selectedAll: boolean;
|
||||||
filter: (cipher: SendView) => boolean;
|
filter: (cipher: SendView) => boolean;
|
||||||
@ -39,6 +46,8 @@ export class SendComponent implements OnInit, OnDestroy {
|
|||||||
private searchTimeout: any;
|
private searchTimeout: any;
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
private _filteredSends: SendView[];
|
private _filteredSends: SendView[];
|
||||||
|
private _searchText$ = new BehaviorSubject<string>("");
|
||||||
|
protected isSearchable: boolean = false;
|
||||||
|
|
||||||
get filteredSends(): SendView[] {
|
get filteredSends(): SendView[] {
|
||||||
return this._filteredSends;
|
return this._filteredSends;
|
||||||
@ -48,6 +57,14 @@ export class SendComponent implements OnInit, OnDestroy {
|
|||||||
this._filteredSends = filteredSends;
|
this._filteredSends = filteredSends;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get searchText() {
|
||||||
|
return this._searchText$.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set searchText(value: string) {
|
||||||
|
this._searchText$.next(value);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected sendService: SendService,
|
protected sendService: SendService,
|
||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
@ -68,6 +85,15 @@ export class SendComponent implements OnInit, OnDestroy {
|
|||||||
.subscribe((policyAppliesToActiveUser) => {
|
.subscribe((policyAppliesToActiveUser) => {
|
||||||
this.disableSend = policyAppliesToActiveUser;
|
this.disableSend = policyAppliesToActiveUser;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._searchText$
|
||||||
|
.pipe(
|
||||||
|
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe((isSearchable) => {
|
||||||
|
this.isSearchable = isSearchable;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@ -122,14 +148,14 @@ export class SendComponent implements OnInit, OnDestroy {
|
|||||||
clearTimeout(this.searchTimeout);
|
clearTimeout(this.searchTimeout);
|
||||||
}
|
}
|
||||||
if (timeout == null) {
|
if (timeout == null) {
|
||||||
this.hasSearched = this.searchService.isSearchable(this.searchText);
|
this.hasSearched = this.isSearchable;
|
||||||
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
|
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
|
||||||
this.applyTextSearch();
|
this.applyTextSearch();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.searchPending = true;
|
this.searchPending = true;
|
||||||
this.searchTimeout = setTimeout(async () => {
|
this.searchTimeout = setTimeout(async () => {
|
||||||
this.hasSearched = this.searchService.isSearchable(this.searchText);
|
this.hasSearched = this.isSearchable;
|
||||||
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
|
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
|
||||||
this.applyTextSearch();
|
this.applyTextSearch();
|
||||||
this.searchPending = false;
|
this.searchPending = false;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Directive, EventEmitter, Input, Output } from "@angular/core";
|
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||||
|
import { BehaviorSubject, Subject, from, switchMap, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
@ -6,7 +7,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
|||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export class VaultItemsComponent {
|
export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||||
@Input() activeCipherId: string = null;
|
@Input() activeCipherId: string = null;
|
||||||
@Output() onCipherClicked = new EventEmitter<CipherView>();
|
@Output() onCipherClicked = new EventEmitter<CipherView>();
|
||||||
@Output() onCipherRightClicked = new EventEmitter<CipherView>();
|
@Output() onCipherRightClicked = new EventEmitter<CipherView>();
|
||||||
@ -23,13 +24,15 @@ export class VaultItemsComponent {
|
|||||||
|
|
||||||
protected searchPending = false;
|
protected searchPending = false;
|
||||||
|
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
private searchTimeout: any = null;
|
private searchTimeout: any = null;
|
||||||
private _searchText: string = null;
|
private isSearchable: boolean = false;
|
||||||
|
private _searchText$ = new BehaviorSubject<string>("");
|
||||||
get searchText() {
|
get searchText() {
|
||||||
return this._searchText;
|
return this._searchText$.value;
|
||||||
}
|
}
|
||||||
set searchText(value: string) {
|
set searchText(value: string) {
|
||||||
this._searchText = value;
|
this._searchText$.next(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -37,6 +40,21 @@ export class VaultItemsComponent {
|
|||||||
protected cipherService: CipherService,
|
protected cipherService: CipherService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this._searchText$
|
||||||
|
.pipe(
|
||||||
|
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe((isSearchable) => {
|
||||||
|
this.isSearchable = isSearchable;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
async load(filter: (cipher: CipherView) => boolean = null, deleted = false) {
|
async load(filter: (cipher: CipherView) => boolean = null, deleted = false) {
|
||||||
this.deleted = deleted ?? false;
|
this.deleted = deleted ?? false;
|
||||||
await this.applyFilter(filter);
|
await this.applyFilter(filter);
|
||||||
@ -90,7 +108,7 @@ export class VaultItemsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isSearching() {
|
isSearching() {
|
||||||
return !this.searchPending && this.searchService.isSearchable(this.searchText);
|
return !this.searchPending && this.isSearchable;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted;
|
protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted;
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { SendView } from "../tools/send/models/view/send.view";
|
import { SendView } from "../tools/send/models/view/send.view";
|
||||||
|
import { IndexedEntityId } from "../types/guid";
|
||||||
import { CipherView } from "../vault/models/view/cipher.view";
|
import { CipherView } from "../vault/models/view/cipher.view";
|
||||||
|
|
||||||
export abstract class SearchService {
|
export abstract class SearchService {
|
||||||
indexedEntityId?: string = null;
|
indexedEntityId$: Observable<IndexedEntityId | null>;
|
||||||
clearIndex: () => void;
|
|
||||||
isSearchable: (query: string) => boolean;
|
clearIndex: () => Promise<void>;
|
||||||
indexCiphers: (ciphersToIndex: CipherView[], indexedEntityGuid?: string) => void;
|
isSearchable: (query: string) => Promise<boolean>;
|
||||||
|
indexCiphers: (ciphersToIndex: CipherView[], indexedEntityGuid?: string) => Promise<void>;
|
||||||
searchCiphers: (
|
searchCiphers: (
|
||||||
query: string,
|
query: string,
|
||||||
filter?: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[],
|
filter?: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[],
|
||||||
|
@ -127,3 +127,4 @@ export const VAULT_SETTINGS_DISK = new StateDefinition("vaultSettings", "disk",
|
|||||||
web: "disk-local",
|
web: "disk-local",
|
||||||
});
|
});
|
||||||
export const VAULT_BROWSER_MEMORY = new StateDefinition("vaultBrowser", "memory");
|
export const VAULT_BROWSER_MEMORY = new StateDefinition("vaultBrowser", "memory");
|
||||||
|
export const VAULT_SEARCH_MEMORY = new StateDefinition("vaultSearch", "memory");
|
||||||
|
@ -1,20 +1,91 @@
|
|||||||
import * as lunr from "lunr";
|
import * as lunr from "lunr";
|
||||||
|
import { Observable, firstValueFrom, map } from "rxjs";
|
||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service";
|
import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service";
|
||||||
import { UriMatchStrategy } from "../models/domain/domain-service";
|
import { UriMatchStrategy } from "../models/domain/domain-service";
|
||||||
import { I18nService } from "../platform/abstractions/i18n.service";
|
import { I18nService } from "../platform/abstractions/i18n.service";
|
||||||
import { LogService } from "../platform/abstractions/log.service";
|
import { LogService } from "../platform/abstractions/log.service";
|
||||||
|
import {
|
||||||
|
ActiveUserState,
|
||||||
|
StateProvider,
|
||||||
|
UserKeyDefinition,
|
||||||
|
VAULT_SEARCH_MEMORY,
|
||||||
|
} from "../platform/state";
|
||||||
import { SendView } from "../tools/send/models/view/send.view";
|
import { SendView } from "../tools/send/models/view/send.view";
|
||||||
|
import { IndexedEntityId } from "../types/guid";
|
||||||
import { FieldType } from "../vault/enums";
|
import { FieldType } from "../vault/enums";
|
||||||
import { CipherType } from "../vault/enums/cipher-type";
|
import { CipherType } from "../vault/enums/cipher-type";
|
||||||
import { CipherView } from "../vault/models/view/cipher.view";
|
import { CipherView } from "../vault/models/view/cipher.view";
|
||||||
|
|
||||||
|
export type SerializedLunrIndex = {
|
||||||
|
version: string;
|
||||||
|
fields: string[];
|
||||||
|
fieldVectors: [string, number[]];
|
||||||
|
invertedIndex: any[];
|
||||||
|
pipeline: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `KeyDefinition` for accessing the search index in application state.
|
||||||
|
* The key definition is configured to clear the index when the user locks the vault.
|
||||||
|
*/
|
||||||
|
export const LUNR_SEARCH_INDEX = new UserKeyDefinition<SerializedLunrIndex>(
|
||||||
|
VAULT_SEARCH_MEMORY,
|
||||||
|
"searchIndex",
|
||||||
|
{
|
||||||
|
deserializer: (obj: Jsonify<SerializedLunrIndex>) => obj,
|
||||||
|
clearOn: ["lock"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `KeyDefinition` for accessing the ID of the entity currently indexed by Lunr search.
|
||||||
|
* The key definition is configured to clear the indexed entity ID when the user locks the vault.
|
||||||
|
*/
|
||||||
|
export const LUNR_SEARCH_INDEXED_ENTITY_ID = new UserKeyDefinition<IndexedEntityId>(
|
||||||
|
VAULT_SEARCH_MEMORY,
|
||||||
|
"searchIndexedEntityId",
|
||||||
|
{
|
||||||
|
deserializer: (obj: Jsonify<IndexedEntityId>) => obj,
|
||||||
|
clearOn: ["lock"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `KeyDefinition` for accessing the state of Lunr search indexing, indicating whether the Lunr search index is currently being built or updating.
|
||||||
|
* The key definition is configured to clear the indexing state when the user locks the vault.
|
||||||
|
*/
|
||||||
|
export const LUNR_SEARCH_INDEXING = new UserKeyDefinition<boolean>(
|
||||||
|
VAULT_SEARCH_MEMORY,
|
||||||
|
"isIndexing",
|
||||||
|
{
|
||||||
|
deserializer: (obj: Jsonify<boolean>) => obj,
|
||||||
|
clearOn: ["lock"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export class SearchService implements SearchServiceAbstraction {
|
export class SearchService implements SearchServiceAbstraction {
|
||||||
private static registeredPipeline = false;
|
private static registeredPipeline = false;
|
||||||
|
|
||||||
indexedEntityId?: string = null;
|
private searchIndexState: ActiveUserState<SerializedLunrIndex> =
|
||||||
private indexing = false;
|
this.stateProvider.getActive(LUNR_SEARCH_INDEX);
|
||||||
private index: lunr.Index = null;
|
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 immediateSearchLocales: string[] = ["zh-CN", "zh-TW", "ja", "ko", "vi"];
|
||||||
private readonly defaultSearchableMinLength: number = 2;
|
private readonly defaultSearchableMinLength: number = 2;
|
||||||
private searchableMinLength: number = this.defaultSearchableMinLength;
|
private searchableMinLength: number = this.defaultSearchableMinLength;
|
||||||
@ -22,6 +93,7 @@ export class SearchService implements SearchServiceAbstraction {
|
|||||||
constructor(
|
constructor(
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
|
private stateProvider: StateProvider,
|
||||||
) {
|
) {
|
||||||
this.i18nService.locale$.subscribe((locale) => {
|
this.i18nService.locale$.subscribe((locale) => {
|
||||||
if (this.immediateSearchLocales.indexOf(locale) !== -1) {
|
if (this.immediateSearchLocales.indexOf(locale) !== -1) {
|
||||||
@ -40,28 +112,29 @@ export class SearchService implements SearchServiceAbstraction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearIndex(): void {
|
async clearIndex(): Promise<void> {
|
||||||
this.indexedEntityId = null;
|
await this.searchIndexEntityIdState.update(() => null);
|
||||||
this.index = null;
|
await this.searchIndexState.update(() => null);
|
||||||
|
await this.searchIsIndexingState.update(() => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
isSearchable(query: string): boolean {
|
async isSearchable(query: string): Promise<boolean> {
|
||||||
query = SearchService.normalizeSearchQuery(query);
|
query = SearchService.normalizeSearchQuery(query);
|
||||||
|
const index = await this.getIndexForSearch();
|
||||||
const notSearchable =
|
const notSearchable =
|
||||||
query == null ||
|
query == null ||
|
||||||
(this.index == null && query.length < this.searchableMinLength) ||
|
(index == null && query.length < this.searchableMinLength) ||
|
||||||
(this.index != null && query.length < this.searchableMinLength && query.indexOf(">") !== 0);
|
(index != null && query.length < this.searchableMinLength && query.indexOf(">") !== 0);
|
||||||
return !notSearchable;
|
return !notSearchable;
|
||||||
}
|
}
|
||||||
|
|
||||||
indexCiphers(ciphers: CipherView[], indexedEntityId?: string): void {
|
async indexCiphers(ciphers: CipherView[], indexedEntityId?: string): Promise<void> {
|
||||||
if (this.indexing) {
|
if (await this.getIsIndexing()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.indexing = true;
|
await this.setIsIndexing(true);
|
||||||
this.indexedEntityId = indexedEntityId;
|
await this.setIndexedEntityIdForSearch(indexedEntityId as IndexedEntityId);
|
||||||
this.index = null;
|
|
||||||
const builder = new lunr.Builder();
|
const builder = new lunr.Builder();
|
||||||
builder.pipeline.add(this.normalizeAccentsPipelineFunction);
|
builder.pipeline.add(this.normalizeAccentsPipelineFunction);
|
||||||
builder.ref("id");
|
builder.ref("id");
|
||||||
@ -95,9 +168,11 @@ export class SearchService implements SearchServiceAbstraction {
|
|||||||
builder.field("organizationid", { extractor: (c: CipherView) => c.organizationId });
|
builder.field("organizationid", { extractor: (c: CipherView) => c.organizationId });
|
||||||
ciphers = ciphers || [];
|
ciphers = ciphers || [];
|
||||||
ciphers.forEach((c) => builder.add(c));
|
ciphers.forEach((c) => builder.add(c));
|
||||||
this.index = builder.build();
|
const index = builder.build();
|
||||||
|
|
||||||
this.indexing = false;
|
await this.setIndexForSearch(index.toJSON() as SerializedLunrIndex);
|
||||||
|
|
||||||
|
await this.setIsIndexing(false);
|
||||||
|
|
||||||
this.logService.info("Finished search indexing");
|
this.logService.info("Finished search indexing");
|
||||||
}
|
}
|
||||||
@ -125,18 +200,18 @@ export class SearchService implements SearchServiceAbstraction {
|
|||||||
ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean);
|
ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isSearchable(query)) {
|
if (!(await this.isSearchable(query))) {
|
||||||
return ciphers;
|
return ciphers;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.indexing) {
|
if (await this.getIsIndexing()) {
|
||||||
await new Promise((r) => setTimeout(r, 250));
|
await new Promise((r) => setTimeout(r, 250));
|
||||||
if (this.indexing) {
|
if (await this.getIsIndexing()) {
|
||||||
await new Promise((r) => setTimeout(r, 500));
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = this.getIndexForSearch();
|
const index = await this.getIndexForSearch();
|
||||||
if (index == null) {
|
if (index == null) {
|
||||||
// Fall back to basic search if index is not available
|
// Fall back to basic search if index is not available
|
||||||
return this.searchCiphersBasic(ciphers, query);
|
return this.searchCiphersBasic(ciphers, query);
|
||||||
@ -230,8 +305,24 @@ export class SearchService implements SearchServiceAbstraction {
|
|||||||
return sendsMatched.concat(lowPriorityMatched);
|
return sendsMatched.concat(lowPriorityMatched);
|
||||||
}
|
}
|
||||||
|
|
||||||
getIndexForSearch(): lunr.Index {
|
async getIndexForSearch(): Promise<lunr.Index | null> {
|
||||||
return this.index;
|
return await firstValueFrom(this.index$);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setIndexForSearch(index: SerializedLunrIndex): Promise<void> {
|
||||||
|
await this.searchIndexState.update(() => index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setIndexedEntityIdForSearch(indexedEntityId: IndexedEntityId): Promise<void> {
|
||||||
|
await this.searchIndexEntityIdState.update(() => indexedEntityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setIsIndexing(indexing: boolean): Promise<void> {
|
||||||
|
await this.searchIsIndexingState.update(() => indexing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getIsIndexing(): Promise<boolean> {
|
||||||
|
return await firstValueFrom(this.searchIsIndexing$);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fieldExtractor(c: CipherView, joined: boolean) {
|
private fieldExtractor(c: CipherView, joined: boolean) {
|
||||||
|
@ -91,7 +91,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
|||||||
const currentUserId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
const currentUserId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||||
|
|
||||||
if (userId == null || userId === currentUserId) {
|
if (userId == null || userId === currentUserId) {
|
||||||
this.searchService.clearIndex();
|
await this.searchService.clearIndex();
|
||||||
await this.folderService.clearCache();
|
await this.folderService.clearCache();
|
||||||
await this.collectionService.clearActiveUserCache();
|
await this.collectionService.clearActiveUserCache();
|
||||||
}
|
}
|
||||||
|
@ -8,3 +8,4 @@ export type CollectionId = Opaque<string, "CollectionId">;
|
|||||||
export type ProviderId = Opaque<string, "ProviderId">;
|
export type ProviderId = Opaque<string, "ProviderId">;
|
||||||
export type PolicyId = Opaque<string, "PolicyId">;
|
export type PolicyId = Opaque<string, "PolicyId">;
|
||||||
export type CipherId = Opaque<string, "CipherId">;
|
export type CipherId = Opaque<string, "CipherId">;
|
||||||
|
export type IndexedEntityId = Opaque<string, "IndexedEntityId">;
|
||||||
|
@ -89,9 +89,9 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
}
|
}
|
||||||
if (this.searchService != null) {
|
if (this.searchService != null) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
this.searchService.clearIndex();
|
await this.searchService.clearIndex();
|
||||||
} else {
|
} else {
|
||||||
this.searchService.indexCiphers(value);
|
await this.searchService.indexCiphers(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -333,9 +333,10 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
private async reindexCiphers() {
|
private async reindexCiphers() {
|
||||||
const userId = await this.stateService.getUserId();
|
const userId = await this.stateService.getUserId();
|
||||||
const reindexRequired =
|
const reindexRequired =
|
||||||
this.searchService != null && (this.searchService.indexedEntityId ?? userId) !== userId;
|
this.searchService != null &&
|
||||||
|
((await firstValueFrom(this.searchService.indexedEntityId$)) ?? userId) !== userId;
|
||||||
if (reindexRequired) {
|
if (reindexRequired) {
|
||||||
this.searchService.indexCiphers(await this.getDecryptedCipherCache(), userId);
|
await this.searchService.indexCiphers(await this.getDecryptedCipherCache(), userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user