mirror of
https://github.com/bitwarden/browser
synced 2026-02-11 05:53:42 +00:00
Pre demo rush
- This breaks stories
This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
placeholder="{{ searchPlaceholder | i18n }}"
|
||||
[(ngModel)]="searchText"
|
||||
(ngModelChange)="onSearchTextChanged($event)"
|
||||
[history]="searchHistory.history$ | async"
|
||||
[savedFilters]="savedFilters$ | async"
|
||||
(filterSaved)="onFilterSaved($event)"
|
||||
(filterDeleted)="onFilterDeleted($event)"
|
||||
|
||||
@@ -29,14 +29,7 @@
|
||||
</bit-callout>
|
||||
<bit-card>
|
||||
<div class="tw-w-full">
|
||||
<app-vault-filter
|
||||
#vaultFilter
|
||||
[activeFilter]="activeFilter"
|
||||
[searchText]="currentSearchText$ | async"
|
||||
[userId]="userId$ | async"
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
(onEditFolder)="editFolder($event)"
|
||||
></app-vault-filter>
|
||||
<bit-filter-builder [searchContext]="searchContext$" initialFilter="" (searchFilterEvent)="filterSearchText($event)" />
|
||||
</div>
|
||||
|
||||
<app-vault-items
|
||||
|
||||
@@ -67,9 +67,17 @@ import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-repromp
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FilterService } from "@bitwarden/common/vault/search/filter.service";
|
||||
import { SearchContext } from "@bitwarden/common/vault/search/query.types";
|
||||
import { SavedFiltersService } from "@bitwarden/common/vault/search/saved-filters.service";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { CardComponent, DialogService, Icons, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
CardComponent,
|
||||
DialogService,
|
||||
Icons,
|
||||
SearchModule,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { Filter } from "@bitwarden/components/src/search/filter-builder.component";
|
||||
import {
|
||||
AddEditFolderDialogComponent,
|
||||
AddEditFolderDialogResult,
|
||||
@@ -124,7 +132,6 @@ import {
|
||||
} from "./vault-filter/shared/models/routed-vault-filter.model";
|
||||
import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model";
|
||||
import { FolderFilter, OrganizationFilter } from "./vault-filter/shared/models/vault-filter.type";
|
||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
||||
import { VaultOnboardingComponent } from "./vault-onboarding/vault-onboarding.component";
|
||||
|
||||
@@ -139,10 +146,10 @@ const SearchTextDebounceInterval = 200;
|
||||
VaultHeaderComponent,
|
||||
VaultOnboardingComponent,
|
||||
VaultBannersComponent,
|
||||
VaultFilterModule,
|
||||
VaultItemsModule,
|
||||
SharedModule,
|
||||
CardComponent,
|
||||
SearchModule,
|
||||
],
|
||||
providers: [
|
||||
RoutedVaultFilterService,
|
||||
@@ -179,7 +186,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
protected userId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
|
||||
|
||||
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
|
||||
private organizations$ = this.userId$.pipe(
|
||||
protected organizations$ = this.userId$.pipe(
|
||||
switchMap((id) => this.organizationService.organizations$(id)),
|
||||
);
|
||||
|
||||
@@ -241,6 +248,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
private _searchContext = new Subject<SearchContext>();
|
||||
protected searchContext$ = this._searchContext.asObservable();
|
||||
|
||||
constructor(
|
||||
private syncService: SyncService,
|
||||
private route: ActivatedRoute,
|
||||
@@ -359,6 +369,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// TODO: WHO AM I?
|
||||
context$.pipe(takeUntil(this.destroy$)).subscribe(this._searchContext);
|
||||
|
||||
const savedFilters$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) => this.savedFilterService.filtersFor$(account.id)),
|
||||
map((filters) => {
|
||||
@@ -648,8 +662,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
};
|
||||
|
||||
filterSearchText(searchText: string) {
|
||||
this.searchText$.next(searchText);
|
||||
filterSearchText(searchText: Filter) {
|
||||
this.searchText$.next(searchText.raw);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -269,10 +269,8 @@ import {
|
||||
} from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||
import {
|
||||
DefaultVaultFilterMetadataService,
|
||||
VaultFilterMetadataService,
|
||||
} from "@bitwarden/common/vault/filtering/vault-filter-metadata.service";
|
||||
import { BasicVaultFilterHandler } from "@bitwarden/common/vault/filtering/basic-vault-filter.handler";
|
||||
import { VaultFilterMetadataService } from "@bitwarden/common/vault/filtering/vault-filter-metadata.service";
|
||||
import { DefaultFilterService, FilterService } from "@bitwarden/common/vault/search/filter.service";
|
||||
import {
|
||||
SavedFiltersService,
|
||||
@@ -1500,9 +1498,14 @@ const safeProviders: SafeProvider[] = [
|
||||
}),
|
||||
safeProvider({
|
||||
provide: VaultFilterMetadataService,
|
||||
useClass: DefaultVaultFilterMetadataService,
|
||||
useClass: VaultFilterMetadataService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: BasicVaultFilterHandler,
|
||||
useClass: BasicVaultFilterHandler,
|
||||
deps: [LogService],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||
import { map, of } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherType, FieldType } from "@bitwarden/common/vault/enums";
|
||||
import {
|
||||
CustomFieldMetadata,
|
||||
VaultFilterMetadata,
|
||||
VaultFilterMetadataService,
|
||||
} from "@bitwarden/common/vault/filtering/vault-filter-metadata.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { I18nMockService } from "@bitwarden/components";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { SearchComponent } from "@bitwarden/components/src/search/search.component";
|
||||
|
||||
import { FilterBuilderComponent } from "./filter-builder.component";
|
||||
|
||||
export default {
|
||||
title: "Filter/In Search",
|
||||
component: SearchComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [FilterBuilderComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: new I18nMockService({
|
||||
search: "Search",
|
||||
multiSelectLoading: "Loading",
|
||||
multiSelectNotFound: "Not Found",
|
||||
multiSelectClearAll: "Clear All",
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: VaultFilterMetadataService,
|
||||
useValue: {
|
||||
collectMetadata: () => {
|
||||
return map<CipherView[], VaultFilterMetadata>((_ciphers) => {
|
||||
return {
|
||||
vaults: new Map([
|
||||
[null, 1],
|
||||
["1", 1],
|
||||
["2", 1],
|
||||
]),
|
||||
folders: new Map([
|
||||
["1", 1],
|
||||
["2", 1],
|
||||
]),
|
||||
collections: new Map([
|
||||
["1", 1],
|
||||
["2", 1],
|
||||
]),
|
||||
itemTypes: new Map([
|
||||
[CipherType.Login, 1],
|
||||
[CipherType.Card, 1],
|
||||
[CipherType.Identity, 1],
|
||||
[CipherType.SecureNote, 1],
|
||||
[CipherType.SshKey, 1],
|
||||
]),
|
||||
customFields: new Map<CustomFieldMetadata, number>([
|
||||
[{ name: "one", type: FieldType.Boolean, linkedType: null }, 1],
|
||||
[{ name: "one", type: FieldType.Boolean, linkedType: null }, 1],
|
||||
[{ name: "one", type: FieldType.Boolean, linkedType: null }, 1],
|
||||
]),
|
||||
attachmentCount: 1,
|
||||
} satisfies VaultFilterMetadata;
|
||||
});
|
||||
},
|
||||
} satisfies VaultFilterMetadataService,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
export const Default: StoryObj<SearchComponent & FilterBuilderComponent> = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<bit-search [history]="history">
|
||||
<div filter class="tw-absolute tw-w-full tw-z-[1000] tw-float-left tw-m-0 tw-p-5 tw-bg-background tw-rounded-xl tw-border tw-border-solid tw-border-secondary-300">
|
||||
<app-filter-builder [ciphers]="ciphers"></app-filter-builder>
|
||||
</div>
|
||||
</bit-search>
|
||||
<p>Other content below</p>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
ciphers: of([]),
|
||||
history: ["One", "Two"],
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OperatorFunction, map } from "rxjs";
|
||||
import { map } from "rxjs";
|
||||
|
||||
import { CipherType, FieldType, LinkedIdType } from "../enums";
|
||||
import { CipherView } from "../models/view/cipher.view";
|
||||
@@ -30,11 +30,7 @@ function metaDataKeyEqual<T extends MetadataType>(a: T, b: T) {
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class VaultFilterMetadataService {
|
||||
abstract collectMetadata(): OperatorFunction<CipherView[], VaultFilterMetadata>;
|
||||
}
|
||||
|
||||
export class DefaultVaultFilterMetadataService implements VaultFilterMetadataService {
|
||||
export class VaultFilterMetadataService {
|
||||
collectMetadata() {
|
||||
const setOrIncrement = <T extends MetadataType>(map: Map<T, number>, key: T) => {
|
||||
const entry = Array.from(map.entries()).find(([k]) => metaDataKeyEqual(key, k));
|
||||
@@ -47,53 +43,56 @@ export class DefaultVaultFilterMetadataService implements VaultFilterMetadataSer
|
||||
};
|
||||
|
||||
return map<CipherView[], VaultFilterMetadata>((ciphers) => {
|
||||
return ciphers.reduce<VaultFilterMetadata>(
|
||||
(metadata, cipher) => {
|
||||
// Track type
|
||||
setOrIncrement(metadata.itemTypes, cipher.type);
|
||||
const emptyMetadata = {
|
||||
vaults: new Map<string | null, number>(),
|
||||
customFields: new Map<CustomFieldMetadata, number>(),
|
||||
itemTypes: new Map<CipherType, number>(),
|
||||
folders: new Map<string, number>(),
|
||||
collections: new Map<string, number>(),
|
||||
attachmentCount: 0,
|
||||
};
|
||||
|
||||
// Track vault
|
||||
setOrIncrement(metadata.vaults, cipher.organizationId ?? null);
|
||||
if (ciphers == null) {
|
||||
return emptyMetadata;
|
||||
}
|
||||
|
||||
// Track all field names
|
||||
if (cipher.fields != null) {
|
||||
for (const field of cipher.fields) {
|
||||
setOrIncrement(metadata.customFields, {
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
linkedType: field.linkedId,
|
||||
});
|
||||
}
|
||||
return ciphers.reduce<VaultFilterMetadata>((metadata, cipher) => {
|
||||
// Track type
|
||||
setOrIncrement(metadata.itemTypes, cipher.type);
|
||||
|
||||
// Track vault
|
||||
setOrIncrement(metadata.vaults, cipher.organizationId ?? null);
|
||||
|
||||
// Track all field names
|
||||
if (cipher.fields != null) {
|
||||
for (const field of cipher.fields) {
|
||||
setOrIncrement(metadata.customFields, {
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
linkedType: field.linkedId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Track all folder ids
|
||||
if (cipher.folderId != null) {
|
||||
setOrIncrement(metadata.folders, cipher.folderId);
|
||||
// Track all folder ids
|
||||
if (cipher.folderId != null) {
|
||||
setOrIncrement(metadata.folders, cipher.folderId);
|
||||
}
|
||||
|
||||
// Track all collections
|
||||
if (cipher.collectionIds != null) {
|
||||
for (const collectionId of cipher.collectionIds) {
|
||||
setOrIncrement(metadata.collections, collectionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Track all collections
|
||||
if (cipher.collectionIds != null) {
|
||||
for (const collectionId of cipher.collectionIds) {
|
||||
setOrIncrement(metadata.collections, collectionId);
|
||||
}
|
||||
}
|
||||
// Track if any have an attachment
|
||||
if (cipher.attachments != null && cipher.attachments.length > 0) {
|
||||
metadata.attachmentCount = metadata.attachmentCount + cipher.attachments.length;
|
||||
}
|
||||
|
||||
// Track if any have an attachment
|
||||
if (cipher.attachments != null && cipher.attachments.length > 0) {
|
||||
metadata.attachmentCount = metadata.attachmentCount + cipher.attachments.length;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
},
|
||||
{
|
||||
vaults: new Map<string | null, number>(),
|
||||
customFields: new Map<CustomFieldMetadata, number>(),
|
||||
itemTypes: new Map<CipherType, number>(),
|
||||
folders: new Map<string, number>(),
|
||||
collections: new Map<string, number>(),
|
||||
attachmentCount: 0,
|
||||
},
|
||||
);
|
||||
return metadata;
|
||||
}, emptyMetadata);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Observable, combineLatestWith, map, mergeMap } from "rxjs";
|
||||
import { Observable, combineLatestWith, map, mergeMap, of } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports -- TODO this will need to move
|
||||
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
|
||||
@@ -28,7 +28,7 @@ export class SearchHistory {
|
||||
private readonly updateCallback: (newSearch: string) => Promise<void>,
|
||||
) {
|
||||
this.userHistoryIndexer = orgId ?? userId;
|
||||
this.history$ = userHistory$.pipe(map((h) => h?.[this.userHistoryIndexer] ?? []));
|
||||
this.history$ = of([]);
|
||||
}
|
||||
|
||||
async push(newSearch: string) {
|
||||
|
||||
@@ -6,31 +6,47 @@ import {
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
signal,
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { BehaviorSubject, distinctUntilChanged, map, Observable, startWith } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { VaultFilterMetadataService } from "@bitwarden/common/vault/filtering/vault-filter-metadata.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
FormFieldModule,
|
||||
ButtonModule,
|
||||
LinkModule,
|
||||
CheckboxModule,
|
||||
ChipMultiSelectComponent,
|
||||
ChipSelectOption,
|
||||
ChipSelectComponent,
|
||||
ToggleGroupModule,
|
||||
} from "@bitwarden/components";
|
||||
BehaviorSubject,
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
firstValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
of,
|
||||
startWith,
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import {
|
||||
BasicFilter,
|
||||
BasicVaultFilterHandler,
|
||||
} from "@bitwarden/common/vault/filtering/basic-vault-filter.handler";
|
||||
import { SearchComponent } from "@bitwarden/components/src/search/search.component";
|
||||
import { VaultFilterMetadataService } from "@bitwarden/common/vault/filtering/vault-filter-metadata.service";
|
||||
import { SearchContext } from "@bitwarden/common/vault/search/query.types";
|
||||
import {
|
||||
FilterName,
|
||||
FilterString,
|
||||
SavedFiltersService,
|
||||
} from "@bitwarden/common/vault/search/saved-filters.service";
|
||||
|
||||
import { ButtonModule } from "../button";
|
||||
import { CheckboxModule } from "../checkbox";
|
||||
import { ChipMultiSelectComponent } from "../chip-multi-select";
|
||||
import { ChipSelectComponent, ChipSelectOption } from "../chip-select";
|
||||
import { FormFieldModule } from "../form-field";
|
||||
import { LinkModule } from "../link";
|
||||
import { ToggleGroupModule } from "../toggle-group";
|
||||
|
||||
import { SearchComponent } from "./search.component";
|
||||
|
||||
type FilterData = {
|
||||
vaults: ChipSelectOption<string>[] | null;
|
||||
@@ -69,33 +85,39 @@ const customMap = <T, TResult>(
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-filter-builder",
|
||||
selector: "bit-filter-builder",
|
||||
template: `
|
||||
<form [formGroup]="form" *ngIf="filterData$ | async as filter">
|
||||
<div class="tw-mb-2">
|
||||
<bit-search formControlName="text" />
|
||||
<bit-search formControlName="text" [savedFilters]="savedFilters$ | async" />
|
||||
</div>
|
||||
@if (mode === "basic") {
|
||||
<bit-chip-multi-select
|
||||
placeholderText="Vault"
|
||||
placeholderIcon="bwi-vault"
|
||||
formControlName="vaults"
|
||||
[options]="filter.vaults"
|
||||
></bit-chip-multi-select>
|
||||
<bit-chip-multi-select
|
||||
placeholderText="Folders"
|
||||
placeholderIcon="bwi-folder"
|
||||
formControlName="folders"
|
||||
[options]="filter.folders"
|
||||
class="tw-pl-2"
|
||||
></bit-chip-multi-select>
|
||||
<bit-chip-multi-select
|
||||
placeholderText="Collections"
|
||||
placeholderIcon="bwi-collection"
|
||||
formControlName="collections"
|
||||
[options]="filter.collections"
|
||||
class="tw-pl-2"
|
||||
></bit-chip-multi-select>
|
||||
@if (mode() === "basic") {
|
||||
@if (filter.vaults != null && filter.vaults.length > 1) {
|
||||
<bit-chip-multi-select
|
||||
placeholderText="Vault"
|
||||
placeholderIcon="bwi-vault"
|
||||
formControlName="vaults"
|
||||
[options]="filter.vaults"
|
||||
></bit-chip-multi-select>
|
||||
}
|
||||
@if (filter.folders != null && filter.folders.length > 0) {
|
||||
<bit-chip-multi-select
|
||||
placeholderText="Folders"
|
||||
placeholderIcon="bwi-folder"
|
||||
formControlName="folders"
|
||||
[options]="filter.folders"
|
||||
class="tw-pl-2"
|
||||
></bit-chip-multi-select>
|
||||
}
|
||||
@if (filter.collections != null && filter.collections.length > 0) {
|
||||
<bit-chip-multi-select
|
||||
placeholderText="Collections"
|
||||
placeholderIcon="bwi-collection"
|
||||
formControlName="collections"
|
||||
[options]="filter.collections"
|
||||
class="tw-pl-2"
|
||||
></bit-chip-multi-select>
|
||||
}
|
||||
@for (selectedOtherOption of selectedOptions(); track selectedOtherOption) {
|
||||
@switch (selectedOtherOption) {
|
||||
@case ("types") {
|
||||
@@ -129,9 +151,13 @@ const customMap = <T, TResult>(
|
||||
>
|
||||
</bit-chip-select>
|
||||
</ng-container>
|
||||
<span
|
||||
class="tw-border-l tw-border-0 tw-border-solid tw-border-secondary-300 tw-mx-2"
|
||||
></span>
|
||||
}
|
||||
@if (formIsDirty) {
|
||||
@if (mode() === "basic") {
|
||||
<span
|
||||
class="tw-border-l tw-border-0 tw-border-solid tw-border-secondary-300 tw-mx-2"
|
||||
></span>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
bitLink
|
||||
@@ -146,9 +172,9 @@ const customMap = <T, TResult>(
|
||||
</button>
|
||||
}
|
||||
<!-- TODO: Align to the right -->
|
||||
<bit-toggle-group selected="basic" (selectedChange)="modeChanged($event)">
|
||||
<bit-toggle-group [selected]="mode()" (selectedChange)="modeChanged($event)">
|
||||
<bit-toggle value="basic">Basic</bit-toggle>
|
||||
<bit-toggle value="advanced">Advanced</bit-toggle>
|
||||
<bit-toggle value="advanced" disabled="true">Advanced</bit-toggle>
|
||||
</bit-toggle-group>
|
||||
</form>
|
||||
`,
|
||||
@@ -172,13 +198,13 @@ const customMap = <T, TResult>(
|
||||
export class FilterBuilderComponent implements OnInit {
|
||||
@Input({ required: true }) initialFilter: string;
|
||||
|
||||
@Input({ required: true }) ciphers: Observable<CipherView[]> | undefined;
|
||||
@Input({ required: true }) searchContext: Observable<SearchContext>;
|
||||
|
||||
@Output() searchFilterEvent = new EventEmitter<Filter>();
|
||||
|
||||
@Output() saveFilterEvent = new EventEmitter<string>();
|
||||
|
||||
protected mode: string = "basic";
|
||||
protected mode = signal("basic");
|
||||
|
||||
protected form = this.formBuilder.group({
|
||||
text: this.formBuilder.control<string>(null),
|
||||
@@ -191,6 +217,8 @@ export class FilterBuilderComponent implements OnInit {
|
||||
selectedOtherOptions: this.formBuilder.control<string[]>([]),
|
||||
});
|
||||
|
||||
protected savedFilters$: Observable<Record<FilterName, FilterString>>;
|
||||
|
||||
private loadingFilter: FilterData;
|
||||
|
||||
protected filterData$: Observable<FilterData>;
|
||||
@@ -207,6 +235,9 @@ export class FilterBuilderComponent implements OnInit {
|
||||
private readonly formBuilder: FormBuilder,
|
||||
private readonly vaultFilterMetadataService: VaultFilterMetadataService,
|
||||
private readonly basicVaultFilterHandler: BasicVaultFilterHandler,
|
||||
private readonly logService: LogService,
|
||||
private readonly savedFilterService: SavedFiltersService,
|
||||
private readonly accountService: AccountService,
|
||||
) {
|
||||
// TODO: i18n
|
||||
this.loadingFilter = {
|
||||
@@ -262,9 +293,13 @@ export class FilterBuilderComponent implements OnInit {
|
||||
this.form.controls.otherOptions.setValue(null);
|
||||
});
|
||||
|
||||
this.savedFilters$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((acc) => this.savedFilterService.filtersFor$(acc.id)),
|
||||
);
|
||||
|
||||
this.form.valueChanges
|
||||
.pipe(
|
||||
// TODO: Debounce?
|
||||
debounceTime(200),
|
||||
map((v) => this.convertFilter(v)),
|
||||
distinctUntilChanged((previous, current) => {
|
||||
return previous.raw === current.raw;
|
||||
@@ -275,8 +310,7 @@ export class FilterBuilderComponent implements OnInit {
|
||||
}
|
||||
|
||||
private convertFilter(filter: Partial<FilterModel>): Filter {
|
||||
// TODO: Support advanced mode
|
||||
if (this.mode === "advanced") {
|
||||
if (this.mode() === "advanced") {
|
||||
return { type: "advanced", raw: filter.text };
|
||||
}
|
||||
|
||||
@@ -298,83 +332,91 @@ export class FilterBuilderComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.initialFilter != null) {
|
||||
//
|
||||
if (!this.trySetBasicFilterElements(this.initialFilter)) {
|
||||
this.form.controls.text.setValue(this.initialFilter);
|
||||
this.mode.set("advanced");
|
||||
}
|
||||
}
|
||||
|
||||
this.filterData$ = this.ciphers.pipe(
|
||||
this.vaultFilterMetadataService.collectMetadata(),
|
||||
map((metadata) => {
|
||||
// TODO: Combine with other info
|
||||
return {
|
||||
vaults: customMap(metadata.vaults, (v, i) => {
|
||||
if (v == null) {
|
||||
// Personal vault
|
||||
this.filterData$ = this.searchContext.pipe(
|
||||
switchMap((context) => {
|
||||
return of(context.ciphers).pipe(
|
||||
this.vaultFilterMetadataService.collectMetadata(),
|
||||
map((metadata) => {
|
||||
{
|
||||
return {
|
||||
value: null,
|
||||
label: "My Vault",
|
||||
};
|
||||
} else {
|
||||
// Get organization info
|
||||
return {
|
||||
value: v,
|
||||
label: `Organization ${i}`,
|
||||
};
|
||||
vaults: customMap(metadata.vaults, (v, i) => {
|
||||
if (v == null) {
|
||||
// Personal vault
|
||||
return {
|
||||
value: null,
|
||||
label: "My Vault",
|
||||
};
|
||||
} else {
|
||||
// Get organization info
|
||||
const org = context.organizations.find((o) => o.id === v);
|
||||
|
||||
return {
|
||||
value: org.name,
|
||||
label: org.name,
|
||||
};
|
||||
}
|
||||
}),
|
||||
folders: customMap(metadata.folders, (id, i) => {
|
||||
const folder = context.folders.find((f) => f.id === id);
|
||||
return {
|
||||
value: folder.name,
|
||||
label: folder.name,
|
||||
} satisfies ChipSelectOption<string>;
|
||||
}),
|
||||
collections: customMap(metadata.collections, (id, i) => {
|
||||
const collection = context.collections.find((c) => c.id === id);
|
||||
return {
|
||||
value: collection.name,
|
||||
label: collection.name,
|
||||
} satisfies ChipSelectOption<string>;
|
||||
}),
|
||||
types: customMap(metadata.itemTypes, (t) => {
|
||||
switch (t) {
|
||||
case CipherType.Login:
|
||||
return { value: "login", label: "Login", icon: "bwi-globe" };
|
||||
case CipherType.Card:
|
||||
return {
|
||||
value: "card",
|
||||
label: "Card",
|
||||
icon: "bwi-credit-card",
|
||||
};
|
||||
case CipherType.Identity:
|
||||
return {
|
||||
value: "identity",
|
||||
label: "Identity",
|
||||
icon: "bwi-id-card",
|
||||
};
|
||||
case CipherType.SecureNote:
|
||||
return {
|
||||
value: "note",
|
||||
label: "Secure Note",
|
||||
icon: "bwi-sticky-note",
|
||||
};
|
||||
case CipherType.SshKey:
|
||||
return {
|
||||
value: "sshkey",
|
||||
label: "SSH Key",
|
||||
icon: "bwi-key",
|
||||
};
|
||||
default:
|
||||
throw new Error("Unreachable");
|
||||
}
|
||||
}),
|
||||
fields: customMap(
|
||||
metadata.customFields,
|
||||
(f, i) => ({ value: f.name, label: f.name }) satisfies ChipSelectOption<string>,
|
||||
),
|
||||
anyHaveAttachment: metadata.attachmentCount !== 0,
|
||||
} satisfies FilterData;
|
||||
}
|
||||
}),
|
||||
folders: customMap(
|
||||
metadata.folders,
|
||||
(f, i) =>
|
||||
({
|
||||
value: f,
|
||||
label: `Folder ${i}`,
|
||||
}) satisfies ChipSelectOption<string>,
|
||||
),
|
||||
collections: customMap(
|
||||
metadata.collections,
|
||||
(c, i) =>
|
||||
({
|
||||
value: c,
|
||||
label: `Collection ${i}`,
|
||||
}) satisfies ChipSelectOption<string>,
|
||||
),
|
||||
types: customMap(metadata.itemTypes, (t) => {
|
||||
switch (t) {
|
||||
case CipherType.Login:
|
||||
return { value: "login", label: "Login", icon: "bwi-globe" };
|
||||
case CipherType.Card:
|
||||
return {
|
||||
value: "card",
|
||||
label: "Card",
|
||||
icon: "bwi-credit-card",
|
||||
};
|
||||
case CipherType.Identity:
|
||||
return {
|
||||
value: "identity",
|
||||
label: "Identity",
|
||||
icon: "bwi-id-card",
|
||||
};
|
||||
case CipherType.SecureNote:
|
||||
return {
|
||||
value: "note",
|
||||
label: "Secure Note",
|
||||
icon: "bwi-sticky-note",
|
||||
};
|
||||
case CipherType.SshKey:
|
||||
return {
|
||||
value: "sshkey",
|
||||
label: "SSH Key",
|
||||
icon: "bwi-key",
|
||||
};
|
||||
default:
|
||||
throw new Error("Unreachable");
|
||||
}
|
||||
}),
|
||||
fields: customMap(
|
||||
metadata.customFields,
|
||||
(f, i) => ({ value: f.name, label: f.name }) satisfies ChipSelectOption<string>,
|
||||
),
|
||||
anyHaveAttachment: metadata.attachmentCount !== 0,
|
||||
} satisfies FilterData;
|
||||
);
|
||||
}),
|
||||
startWith(this.loadingFilter),
|
||||
);
|
||||
@@ -384,46 +426,57 @@ export class FilterBuilderComponent implements OnInit {
|
||||
return this.form.controls.selectedOtherOptions.value;
|
||||
}
|
||||
|
||||
protected get formIsDirty() {
|
||||
return this.form.dirty;
|
||||
}
|
||||
|
||||
private trySetBasicFilterElements(value: string) {
|
||||
if (value == null || value === "") {
|
||||
this.logService.info("Reseting form.");
|
||||
this.resetFilter();
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
this.logService.info("Parsing", value);
|
||||
const parseResult = this.basicVaultFilterHandler.tryParse(value);
|
||||
|
||||
if (parseResult.success) {
|
||||
if (parseResult.filter.terms.length >= 1) {
|
||||
throw new Error("More than 1 term not actually supported in basic");
|
||||
}
|
||||
|
||||
// This item can be displayed with basic, lets do that.
|
||||
const selectedOtherOptions: string[] = [];
|
||||
|
||||
if (parseResult.filter.types.length !== 0) {
|
||||
selectedOtherOptions.push("types");
|
||||
}
|
||||
|
||||
if (parseResult.filter.fields.length !== 0) {
|
||||
selectedOtherOptions.push("fields");
|
||||
}
|
||||
|
||||
console.log("Parse advanced query", value, parseResult.filter);
|
||||
|
||||
this.form.setValue({
|
||||
text: parseResult.filter.terms.length === 1 ? parseResult.filter.terms[0] : null,
|
||||
vaults: parseResult.filter.vaults,
|
||||
folders: parseResult.filter.folders,
|
||||
collections: parseResult.filter.collections,
|
||||
fields: parseResult.filter.fields,
|
||||
types: parseResult.filter.types,
|
||||
otherOptions: null,
|
||||
selectedOtherOptions: selectedOtherOptions,
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
// set form to advanced mode and disable switching to basic
|
||||
if (!parseResult.success) {
|
||||
// Could not parse query
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parseResult.filter.terms.length >= 1) {
|
||||
throw new Error("More than 1 term not actually supported in basic");
|
||||
}
|
||||
|
||||
// This item can be displayed with basic, lets do that.
|
||||
const selectedOtherOptions: string[] = [];
|
||||
|
||||
if (parseResult.filter.types.length !== 0) {
|
||||
selectedOtherOptions.push("types");
|
||||
}
|
||||
|
||||
if (parseResult.filter.fields.length !== 0) {
|
||||
selectedOtherOptions.push("fields");
|
||||
}
|
||||
|
||||
const term = parseResult.filter.terms.length === 1 ? parseResult.filter.terms[0] : null;
|
||||
|
||||
this.form.setValue({
|
||||
text: term === "" ? null : term,
|
||||
vaults: parseResult.filter.vaults,
|
||||
folders: parseResult.filter.folders,
|
||||
collections: parseResult.filter.collections,
|
||||
fields: parseResult.filter.fields,
|
||||
types: parseResult.filter.types,
|
||||
otherOptions: null,
|
||||
selectedOtherOptions: selectedOtherOptions,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
// How should I show off parse errors
|
||||
console.log("Error", err);
|
||||
this.logService.debug("Error while parsing advanced query", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -441,13 +494,29 @@ export class FilterBuilderComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
protected saveFilter() {
|
||||
protected async saveFilter() {
|
||||
const currentFilter = this.convertFilter(this.form.value);
|
||||
this.saveFilterEvent.emit(currentFilter.raw);
|
||||
|
||||
if (currentFilter.raw == null || currentFilter.raw === "") {
|
||||
// Skip
|
||||
return;
|
||||
}
|
||||
|
||||
const activeUser = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((acc) => acc.id)),
|
||||
);
|
||||
await this.savedFilterService.saveFilter(
|
||||
activeUser,
|
||||
currentFilter.raw as FilterName,
|
||||
currentFilter.raw as FilterString,
|
||||
);
|
||||
}
|
||||
|
||||
protected modeChanged(newMode: string) {
|
||||
this.mode = newMode;
|
||||
if (this.mode() === newMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newMode === "advanced") {
|
||||
// Switching to advanced, place basic contents into text
|
||||
this.form.controls.text.setValue(
|
||||
@@ -455,10 +524,15 @@ export class FilterBuilderComponent implements OnInit {
|
||||
);
|
||||
} else {
|
||||
if (!this.trySetBasicFilterElements(this.form.controls.text.value)) {
|
||||
console.log("Could not set filter back to basic, button should have been disabled.");
|
||||
this.mode = "advanced";
|
||||
this.logService.info(
|
||||
"Could not set filter back to basic, button should have been disabled.",
|
||||
);
|
||||
// This doesn't actually change the UI, we need to actually disable the button but that
|
||||
// doesn't look available right now.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.mode.set(newMode);
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,20 @@ import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||
import { map, of } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
import { CipherType, FieldType } from "@bitwarden/common/vault/enums";
|
||||
import { BasicVaultFilterHandler } from "@bitwarden/common/vault/filtering/basic-vault-filter.handler";
|
||||
import {
|
||||
CustomFieldMetadata,
|
||||
VaultFilterMetadata,
|
||||
VaultFilterMetadataService,
|
||||
} from "@bitwarden/common/vault/filtering/vault-filter-metadata.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { I18nMockService } from "@bitwarden/components";
|
||||
|
||||
import { I18nMockService } from "../utils";
|
||||
|
||||
import { FilterBuilderComponent } from "./filter-builder.component";
|
||||
import { BasicVaultFilterHandler } from "@bitwarden/common/vault/filtering/basic-vault-filter.handler";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
|
||||
export default {
|
||||
title: "Filter/Filter Builder",
|
||||
@@ -61,8 +62,8 @@ export default {
|
||||
]),
|
||||
customFields: new Map<CustomFieldMetadata, number>([
|
||||
[{ name: "one", type: FieldType.Boolean, linkedType: null }, 1],
|
||||
[{ name: "one", type: FieldType.Boolean, linkedType: null }, 1],
|
||||
[{ name: "one", type: FieldType.Boolean, linkedType: null }, 1],
|
||||
[{ name: "two", type: FieldType.Boolean, linkedType: null }, 1],
|
||||
[{ name: "three", type: FieldType.Boolean, linkedType: null }, 1],
|
||||
]),
|
||||
attachmentCount: 1,
|
||||
} satisfies VaultFilterMetadata;
|
||||
@@ -90,7 +91,7 @@ export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<app-filter-builder [ciphers]="ciphers" (searchFilterEvent)="searchFilterEvent($event)" (saveFilterEvent)="saveFilterEvent($event)"></app-filter-builder>
|
||||
<bit-filter-builder [ciphers]="ciphers" (searchFilterEvent)="searchFilterEvent($event)" (saveFilterEvent)="saveFilterEvent($event)"></bit-filter-builder>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
@@ -38,7 +38,7 @@
|
||||
[buttonType]=""
|
||||
></button>
|
||||
</div>
|
||||
<ng-container *ngIf="(textUpdated$ | async)?.length > 0">
|
||||
<!-- <ng-container *ngIf="(textUpdated$ | async)?.length > 0">
|
||||
<button
|
||||
type="button"
|
||||
bitLink
|
||||
@@ -47,7 +47,7 @@
|
||||
>
|
||||
Save filter
|
||||
</button>
|
||||
</ng-container>
|
||||
</ng-container> -->
|
||||
</div>
|
||||
<ng-container *ngIf="showSavedFilters">
|
||||
<div class="tw-size-full">
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
} from "@angular/forms";
|
||||
import { BehaviorSubject, map } from "rxjs";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { isBrowserSafariApi } from "@bitwarden/platform";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
@@ -87,9 +87,7 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
|
||||
}
|
||||
|
||||
get filteredHistory$() {
|
||||
// TODO: Not clear if filtering is better or worse
|
||||
return this.textUpdated$.pipe(map((text) => this.history));
|
||||
// return this.textUpdated$.pipe(map((text) => this.history.filter((h) => h.startsWith(text))));
|
||||
return of([]);
|
||||
}
|
||||
|
||||
private _selectedContent = new BehaviorSubject<string | null>(null);
|
||||
|
||||
@@ -2,10 +2,11 @@ import { NgModule } from "@angular/core";
|
||||
|
||||
import { ButtonModule } from "../button";
|
||||
|
||||
import { FilterBuilderComponent } from "./filter-builder.component";
|
||||
import { SearchComponent } from "./search.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SearchComponent, ButtonModule],
|
||||
exports: [SearchComponent],
|
||||
imports: [SearchComponent, ButtonModule, FilterBuilderComponent],
|
||||
exports: [SearchComponent, FilterBuilderComponent],
|
||||
})
|
||||
export class SearchModule {}
|
||||
|
||||
Reference in New Issue
Block a user