1
0
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:
Justin Baur
2025-03-21 15:17:37 -04:00
parent 39deac5191
commit 4b5bb84124
12 changed files with 323 additions and 333 deletions

View File

@@ -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)"

View File

@@ -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

View File

@@ -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);
}
/**

View File

@@ -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({

View File

@@ -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"],
},
};

View File

@@ -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);
});
}
}

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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: {

View File

@@ -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">

View File

@@ -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);

View File

@@ -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 {}