From eafc15c05269df28543d8c1f7bf3b6b6136f9ea5 Mon Sep 17 00:00:00 2001 From: Lukas Holzer Date: Fri, 25 Sep 2020 10:30:23 +0000 Subject: [PATCH] fix(quick-filter): Use internal uid for the filter generation. This issue should be a further step to make the quick filter stable. Fixes #1647 Fixes #1522 --- .../filter-field/filter-field.module.ts | 16 ++- .../quick-filter-async.html | 14 +++ .../quick-filter-async/quick-filter-async.ts | 77 ++++++++++++ .../quick-filter-initial-data.html | 4 + .../quick-filter-initial-data.ts | 9 ++ .../quick-filter/quick-filter.e2e.ts | 17 +++ .../quick-filter/quick-filter.module.ts | 10 +- .../quick-filter/quick-filter/quick-filter.ts | 4 +- .../src/components/quick-filter/util.ts | 0 .../experimental/quick-filter/BUILD.bazel | 25 ++-- .../quick-filter/src/quick-filter-group.html | 4 +- .../quick-filter/src/quick-filter-group.ts | 15 +-- .../quick-filter/src/quick-filter-utils.ts | 14 +-- .../quick-filter/src/quick-filter.spec.ts | 3 +- .../quick-filter/src/quick-filter.ts | 59 +++++++--- .../quick-filter/src/state/actions.ts | 42 ++++--- .../quick-filter/src/state/effects.ts | 10 +- .../quick-filter/src/state/reducer.spec.ts | 30 ----- .../quick-filter/src/state/reducer.ts | 110 ++++++++---------- .../quick-filter/src/state/selectors.ts | 14 ++- .../quick-filter/src/state/store.ts | 11 +- libs/barista-components/filter-field/index.ts | 2 + .../filter-field/src/filter-field.ts | 4 + 23 files changed, 323 insertions(+), 171 deletions(-) create mode 100644 apps/components-e2e/src/components/quick-filter/quick-filter-async/quick-filter-async.html create mode 100644 apps/components-e2e/src/components/quick-filter/quick-filter-async/quick-filter-async.ts delete mode 100644 apps/components-e2e/src/components/quick-filter/util.ts delete mode 100644 libs/barista-components/experimental/quick-filter/src/state/reducer.spec.ts diff --git a/apps/components-e2e/src/components/filter-field/filter-field.module.ts b/apps/components-e2e/src/components/filter-field/filter-field.module.ts index e2be60b4f0..052c459374 100644 --- a/apps/components-e2e/src/components/filter-field/filter-field.module.ts +++ b/apps/components-e2e/src/components/filter-field/filter-field.module.ts @@ -19,12 +19,24 @@ import { NgModule } from '@angular/core'; import { Route, RouterModule } from '@angular/router'; import { DtFilterFieldModule } from '@dynatrace/barista-components/filter-field'; import { DtE2EFilterField } from './filter-field'; +import { + DtExampleFilterFieldAsync, + DtFilterFieldExamplesModule, +} from '@dynatrace/examples/filter-field'; -const routes: Route[] = [{ path: '', component: DtE2EFilterField }]; +const routes: Route[] = [ + { path: '', component: DtE2EFilterField }, + { path: 'async', component: DtExampleFilterFieldAsync }, +]; @NgModule({ declarations: [DtE2EFilterField], - imports: [CommonModule, RouterModule.forChild(routes), DtFilterFieldModule], + imports: [ + CommonModule, + DtFilterFieldExamplesModule, + RouterModule.forChild(routes), + DtFilterFieldModule, + ], exports: [], providers: [], }) diff --git a/apps/components-e2e/src/components/quick-filter/quick-filter-async/quick-filter-async.html b/apps/components-e2e/src/components/quick-filter/quick-filter-async/quick-filter-async.html new file mode 100644 index 0000000000..04fd3e8ecc --- /dev/null +++ b/apps/components-e2e/src/components/quick-filter/quick-filter-async/quick-filter-async.html @@ -0,0 +1,14 @@ + + Quick-filter + + All options in the filter field above + + + Quick-filter async example + diff --git a/apps/components-e2e/src/components/quick-filter/quick-filter-async/quick-filter-async.ts b/apps/components-e2e/src/components/quick-filter/quick-filter-async/quick-filter-async.ts new file mode 100644 index 0000000000..4415936339 --- /dev/null +++ b/apps/components-e2e/src/components/quick-filter/quick-filter-async/quick-filter-async.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component } from '@angular/core'; +import { isObject } from '@dynatrace/barista-components/core'; +import { + DtQuickFilterDefaultDataSource, + DtQuickFilterDefaultDataSourceConfig, + DtQuickFilterCurrentFilterChangeEvent, + DtQuickFilterDefaultDataSourceType, +} from '@dynatrace/barista-components/experimental/quick-filter'; + +const filterFieldData = { + autocomplete: [ + { + name: 'AUT (async)', + async: true, + autocomplete: [], + }, + { + name: 'USA', + autocomplete: [ + { name: 'San Francisco' }, + { name: 'Los Angeles' }, + { name: 'New York' }, + ], + }, + ], +}; + +const asyncData = { + name: 'AUT (async)', + autocomplete: [{ name: 'Linz' }, { name: 'Vienna' }, { name: 'Graz' }], +}; + +@Component({ + selector: 'dt-e2e-quick-filter-async', + templateUrl: './quick-filter-async.html', + // template: '' +}) +export class DtE2EQuickFilterAsync { + /** configuration for the quick filter */ + private _config: DtQuickFilterDefaultDataSourceConfig = { + // Method to decide if a node should be displayed in the quick filter + showInSidebar: (node) => + isObject(node) && node.name && node.name !== 'AUT (async)', + }; + + _dataSource = new DtQuickFilterDefaultDataSource< + DtQuickFilterDefaultDataSourceType + >(filterFieldData, this._config); + + currentFilterChanges( + event: DtQuickFilterCurrentFilterChangeEvent< + DtQuickFilterDefaultDataSourceType + >, + ): void { + if (event.added[0] === filterFieldData.autocomplete[0]) { + setTimeout(() => { + this._dataSource.data = asyncData; + }, 1000); + } + } +} diff --git a/apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.html b/apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.html index 64d492a50e..f60de008be 100644 --- a/apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.html +++ b/apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.html @@ -6,3 +6,7 @@ my content + + diff --git a/apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.ts b/apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.ts index 64f90d7db7..b730ad316a 100644 --- a/apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.ts +++ b/apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.ts @@ -37,4 +37,13 @@ export class DtE2EQuickFilterInitialData { FILTER_FIELD_TEST_DATA.autocomplete[1].autocomplete![2], ], ]; + + changeInitialFilters(): void { + this._initialFilters = [ + [ + FILTER_FIELD_TEST_DATA.autocomplete[1], + FILTER_FIELD_TEST_DATA.autocomplete[1].autocomplete![2], + ], + ]; + } } diff --git a/apps/components-e2e/src/components/quick-filter/quick-filter.e2e.ts b/apps/components-e2e/src/components/quick-filter/quick-filter.e2e.ts index e3f09ec6a6..bfb004db0b 100644 --- a/apps/components-e2e/src/components/quick-filter/quick-filter.e2e.ts +++ b/apps/components-e2e/src/components/quick-filter/quick-filter.e2e.ts @@ -26,6 +26,7 @@ import { getSelectedItem, } from './quick-filter.po'; import { resetWindowSizeToDefault } from '../../utils'; +import { Selector } from 'testcafe'; fixture('Quick Filter') .page('http://localhost:4200/quick-filter') @@ -156,3 +157,19 @@ test('when the distinct group get set to the any option, then remove the group f .expect(getSelectedItem('AUT').textContent) .match(/Graz/); }); + +test('should be possible to change the filters via binding on the quick-filter', async (testController: TestController) => { + await testController + .expect(getFilterfieldTags()) + .eql(['AUTVienna', 'USANew York']) + .click(tagDeleteButton('New York'), { speed: 0.4 }) + .expect(getFilterfieldTags()) + .eql(['AUTVienna']) + .click(Selector('.change-filter-binding')) + .expect(getFilterfieldTags()) + .eql(['USANew York']) + .expect(getSelectedItem('USA').textContent) + .match(/New York/) + .expect(getSelectedItem('AUT').textContent) + .match(/Any/); +}); diff --git a/apps/components-e2e/src/components/quick-filter/quick-filter.module.ts b/apps/components-e2e/src/components/quick-filter/quick-filter.module.ts index 9949a31d66..c34239b836 100644 --- a/apps/components-e2e/src/components/quick-filter/quick-filter.module.ts +++ b/apps/components-e2e/src/components/quick-filter/quick-filter.module.ts @@ -20,15 +20,21 @@ import { Route, RouterModule } from '@angular/router'; import { DtQuickFilterModule } from '@dynatrace/barista-components/experimental/quick-filter'; import { DtE2EQuickFilter } from './quick-filter/quick-filter'; import { DtE2EQuickFilterInitialData } from './quick-filter-initial-data/quick-filter-initial-data'; +import { DtE2EQuickFilterAsync } from './quick-filter-async/quick-filter-async'; const routes: Route[] = [ { path: '', component: DtE2EQuickFilter }, { path: 'initial-data', component: DtE2EQuickFilterInitialData }, + { path: 'async', component: DtE2EQuickFilterAsync }, ]; @NgModule({ - declarations: [DtE2EQuickFilter, DtE2EQuickFilterInitialData], - imports: [CommonModule, RouterModule.forChild(routes), DtQuickFilterModule], + declarations: [ + DtE2EQuickFilter, + DtE2EQuickFilterInitialData, + DtE2EQuickFilterAsync, + ], + imports: [CommonModule, DtQuickFilterModule, RouterModule.forChild(routes)], exports: [], providers: [], }) diff --git a/apps/components-e2e/src/components/quick-filter/quick-filter/quick-filter.ts b/apps/components-e2e/src/components/quick-filter/quick-filter/quick-filter.ts index 5d1358955f..02310cdac2 100644 --- a/apps/components-e2e/src/components/quick-filter/quick-filter/quick-filter.ts +++ b/apps/components-e2e/src/components/quick-filter/quick-filter/quick-filter.ts @@ -24,13 +24,13 @@ import { FILTER_FIELD_TEST_DATA, FILTER_FIELD_TEST_DATA_VALIDATORS, } from '@dynatrace/testing/fixtures'; -import { isObject } from 'util'; +import { isObject } from 'lodash-es'; const DATA = [FILTER_FIELD_TEST_DATA_VALIDATORS, FILTER_FIELD_TEST_DATA]; const config: DtQuickFilterDefaultDataSourceConfig = { showInSidebar: (node) => - isObject(node) && node.name && node.name !== 'Not in Quickfilter', + (isObject(node) as any) && node.name && node.name !== 'Not in Quickfilter', }; @Component({ diff --git a/apps/components-e2e/src/components/quick-filter/util.ts b/apps/components-e2e/src/components/quick-filter/util.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/libs/barista-components/experimental/quick-filter/BUILD.bazel b/libs/barista-components/experimental/quick-filter/BUILD.bazel index 65d7010fb1..24b4bb46a3 100644 --- a/libs/barista-components/experimental/quick-filter/BUILD.bazel +++ b/libs/barista-components/experimental/quick-filter/BUILD.bazel @@ -22,16 +22,17 @@ ng_module_view_engine( module_name = "@dynatrace/barista-components/experimental/quick-filter", tsconfig = "tsconfig_lib", deps = [ - "@npm//@angular/cdk", - "@npm//@angular/common", - "@npm//@angular/core", - "@npm//rxjs", - "//libs/barista-components/core:compile", "//libs/barista-components/checkbox:compile", + "//libs/barista-components/core:compile", "//libs/barista-components/drawer:compile", "//libs/barista-components/filter-field:compile", "//libs/barista-components/icon:compile", "//libs/barista-components/radio:compile", + "@npm//@angular/cdk", + "@npm//@angular/common", + "@npm//@angular/core", + "@npm//lodash-es", + "@npm//rxjs", ], ) @@ -72,19 +73,19 @@ jest( ts_config = ":tsconfig_test", deps = [ ":compile", + "//libs/barista-components/checkbox:compile", + "//libs/barista-components/core:compile", + "//libs/barista-components/drawer:compile", + "//libs/barista-components/filter-field:compile", + "//libs/barista-components/icon:compile", + "//libs/barista-components/radio:compile", "//libs/testing/browser", "//libs/testing/fixtures", "@npm//@angular/cdk", "@npm//@angular/common", "@npm//@angular/core", - "@npm//rxjs", "@npm//@angular/platform-browser", - "//libs/barista-components/core:compile", - "//libs/barista-components/checkbox:compile", - "//libs/barista-components/drawer:compile", - "//libs/barista-components/filter-field:compile", - "//libs/barista-components/icon:compile", - "//libs/barista-components/radio:compile", + "@npm//rxjs", ], ) diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter-group.html b/libs/barista-components/experimental/quick-filter/src/quick-filter-group.html index a755e18d9a..22d397af93 100644 --- a/libs/barista-components/experimental/quick-filter/src/quick-filter-group.html +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-group.html @@ -9,7 +9,7 @@ Any @@ -30,7 +30,7 @@ *ngFor="let item of _getOptions()" [value]="item" [checked]="_isActive(item)" - (change)="_selectCheckBox($event)" + (change)="_selectCheckBox($event, _nodeDef)" > {{ _getViewValue(item) }} diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter-group.ts b/libs/barista-components/experimental/quick-filter/src/quick-filter-group.ts index ac13c0868d..ec0b98898a 100644 --- a/libs/barista-components/experimental/quick-filter/src/quick-filter-group.ts +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-group.ts @@ -25,6 +25,7 @@ import { } from '@angular/core'; import { DtCheckboxChange } from '@dynatrace/barista-components/checkbox'; import { + DtAutocompleteValue, DtNodeDef, isDtOptionDef, isDtRenderType, @@ -51,7 +52,7 @@ import { class: 'dt-quick-filter-group', }, }) -export class DtQuickFilterGroup { +export class DtQuickFilterGroup { /** * @internal * The aria-level of the group headlines for the document outline. @@ -63,7 +64,7 @@ export class DtQuickFilterGroup { /** @internal The list of all active filters */ @Input() - set activeFilters(filters: any[][]) { + set activeFilters(filters: DtAutocompleteValue[][]) { this._activeFilterPaths = buildIdPathsFromFilters(filters || []); this._changeDetectorRef.markForCheck(); } @@ -82,17 +83,17 @@ export class DtQuickFilterGroup { } /** @internal Updates a radio box */ - _selectOption(change: DtRadioChange): void { + _selectOption(change: DtRadioChange, group: DtNodeDef): void { if (change.value) { - this.filterChange.emit(updateFilter(change.value)); + this.filterChange.emit(updateFilter([group, change.value])); } } /** @internal Select or deselect a checkbox */ - _selectCheckBox(change: DtCheckboxChange): void { + _selectCheckBox(change: DtCheckboxChange, group: DtNodeDef): void { const action = change.checked - ? addFilter(change.source.value) - : removeFilter(change.source.value); + ? addFilter([group, change.source.value]) + : removeFilter(change.source.value.option!.uid!); this.filterChange.emit(action); } diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter-utils.ts b/libs/barista-components/experimental/quick-filter/src/quick-filter-utils.ts index 9e635ad8a4..1df2d1ef36 100644 --- a/libs/barista-components/experimental/quick-filter/src/quick-filter-utils.ts +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-utils.ts @@ -14,13 +14,11 @@ * limitations under the License. */ -import { DELIMITER } from '@dynatrace/barista-components/filter-field'; +import { DtAutocompleteValue } from '@dynatrace/barista-components/filter-field'; -export function buildIdPathsFromFilters(filters: any[][]): string[] { - return filters.map((path) => - path.reduce( - (previousValue, currentValue) => - `${previousValue.name}${DELIMITER}${currentValue.name}${DELIMITER}`, - ), - ); +/** @internal Build an array of uids from the options without the groups */ +export function buildIdPathsFromFilters( + filters: DtAutocompleteValue[][], +): string[] { + return filters.map((group) => group[group.length - 1].option?.uid || ''); } diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter.spec.ts b/libs/barista-components/experimental/quick-filter/src/quick-filter.spec.ts index 9df20a47f1..1d5cd10346 100644 --- a/libs/barista-components/experimental/quick-filter/src/quick-filter.spec.ts +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter.spec.ts @@ -123,6 +123,7 @@ describe('dt-quick-filter', () => { FILTER_FIELD_TEST_DATA.autocomplete[0].autocomplete![0], ], ]; + zone.simulateZoneExit(); fixture.detectChanges(); fixture.componentInstance._dataSource = new DtQuickFilterDefaultDataSource( @@ -131,8 +132,8 @@ describe('dt-quick-filter', () => { showInSidebar: () => true, }, ); + zone.simulateZoneExit(); fixture.detectChanges(); - expect(filterFieldInstance.filters).toMatchObject([]); expect(quickFilterInstance.filters).toMatchObject([]); }); diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter.ts b/libs/barista-components/experimental/quick-filter/src/quick-filter.ts index 94d943c9e9..90f032a1ea 100644 --- a/libs/barista-components/experimental/quick-filter/src/quick-filter.ts +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter.ts @@ -31,13 +31,26 @@ import { DtFilterField, DtFilterFieldChangeEvent, DtFilterFieldCurrentFilterChangeEvent, + DtFilterValue, + isDtAutocompleteValue, + _getSourcesOfDtFilterValues, } from '@dynatrace/barista-components/filter-field'; -import { BehaviorSubject, Subject, Observable } from 'rxjs'; -import { switchMap, take, takeUntil } from 'rxjs/operators'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { filter, map, switchMap, take, takeUntil } from 'rxjs/operators'; import { DtQuickFilterDataSource } from './quick-filter-data-source'; -import { Action, setFilters, switchDataSource } from './state/actions'; +import { + Action, + addInitialFilters, + setFilters, + switchDataSource, +} from './state/actions'; import { quickFilterReducer } from './state/reducer'; -import { getAutocompletes, getDataSource, getFilters } from './state/selectors'; +import { + getAutocompletes, + getDataSource, + getFilters, + getInitialFilters, +} from './state/selectors'; import { createQuickFilterStore, QuickFilterState } from './state/store'; /** Directive that is used to place a title inside the quick filters sidebar */ @@ -143,7 +156,7 @@ export class DtQuickFilter implements AfterViewInit, OnDestroy { return this._filterField.filters; } set filters(filters: T[][]) { - this._store.dispatch(setFilters(filters)); + this._store.dispatch(addInitialFilters(filters)); } /** @@ -169,21 +182,33 @@ export class DtQuickFilter implements AfterViewInit, OnDestroy { /** Angular life-cycle hook that will be called after the view is initialized */ ngAfterViewInit(): void { // We need to wait for the first on stable call, otherwise the - // underlying filterfield will thow an expression changed after checked + // underlying filter field will throw an expression changed after checked // error. Deferring the first filter setting. // Relates to a very weird and hard to reproduce bug described in // https://github.com/dynatrace-oss/barista/issues/1305 - this._zone.onStable + const stable$ = this._zone.onStable.pipe(take(1)); + + stable$ .pipe( - take(1), switchMap(() => this._activeFilters$), + map((filters) => + filters.map((values) => _getSourcesOfDtFilterValues(values)), + ), + takeUntil(this._destroy$), + ) + .subscribe((filters) => { + this._filterField.filters = filters; + }); + + stable$ + .pipe( + switchMap(() => this._store.select(getInitialFilters)), + filter(Boolean), takeUntil(this._destroy$), ) - // When the filters changes apply them to the filter field .subscribe((filters) => { - if (this._filterField.filters !== filters) { - this._filterField.filters = filters; - } + this._filterField.filters = filters; + this._store.dispatch(setFilters(this._getFilteredValues())); }); } @@ -212,7 +237,15 @@ export class DtQuickFilter implements AfterViewInit, OnDestroy { /** @internal Bubble the filter field change event through */ _filterFieldChanged(change: DtFilterFieldChangeEvent): void { - this._store.dispatch(setFilters(change.filters)); + // Filter only autocomplete filters as we don't use free-text and range in the quick-filter + this._store.dispatch(setFilters(this._getFilteredValues())); this.filterChanges.emit(change); } + + /** Get the filter Values from the Filter Field with only the displayable autocompletes */ + private _getFilteredValues(): DtFilterValue[][] { + return this._filterField._filterValues.filter((group) => + group.every((value) => isDtAutocompleteValue(value)), + ); + } } diff --git a/libs/barista-components/experimental/quick-filter/src/state/actions.ts b/libs/barista-components/experimental/quick-filter/src/state/actions.ts index 75301aac02..b73d7b1c8e 100644 --- a/libs/barista-components/experimental/quick-filter/src/state/actions.ts +++ b/libs/barista-components/experimental/quick-filter/src/state/actions.ts @@ -15,56 +15,62 @@ */ import { DtFilterFieldDataSource, + DtFilterValue, DtNodeDef, } from '@dynatrace/barista-components/filter-field'; -/** Enum for all the possible action types */ +/** @internal Enum for all the possible action types */ export enum ActionType { INIT = '@@actions init', ADD_FILTER = '@@actions add filter', REMOVE_FILTER = '@@actions remove filter', UPDATE_FILTER = '@@actions update filter', SET_FILTERS = '@@actions set filters', + ADD_INITIAL_FILTERS = '@@actions add initial filters', UNSET_FILTER_GROUP = '@@actions unset filter group', SWITCH_DATA_SOURCE = '@@actions switch dataSource', UPDATE_DATA_SOURCE = '@@actions update dataSource', } -/** Interface for an action */ +/** @internal Interface for an action */ export interface Action { readonly type: ActionType; payload?: T; } -/** Function which helps to create actions without mistakes */ +/** @internal Function which helps to create actions without mistakes */ export const action = (type: ActionType, payload?: T): Action => ({ type, payload, }); -/** Action that sets filters (Bulk operation for addFilter) */ -export const setFilters = (filters: any[][]) => - action(ActionType.SET_FILTERS, filters); +/** @internal Action that sets filters (Bulk operation for addFilter) */ +export const setFilters = (filters: DtFilterValue[][]) => + action(ActionType.SET_FILTERS, filters); -/** Action that unsets a filter group */ +/** @internal Initial filters are set via binding on the quick filter so they are not in the value data format. */ +export const addInitialFilters = (filters: any[][]) => + action(ActionType.ADD_INITIAL_FILTERS, filters); + +/** @internal Action that unsets a filter group */ export const unsetFilterGroup = (group: DtNodeDef) => action(ActionType.UNSET_FILTER_GROUP, group); -/** Action that adds a filter */ -export const addFilter = (item: DtNodeDef) => - action(ActionType.ADD_FILTER, item); +/** @internal Action that adds a filter */ +export const addFilter = (filter: DtNodeDef[]) => + action(ActionType.ADD_FILTER, filter); -/** Action that removes a filter */ -export const removeFilter = (item: DtNodeDef) => - action(ActionType.REMOVE_FILTER, item); +/** @internal Action that removes a filter */ +export const removeFilter = (uid: string) => + action(ActionType.REMOVE_FILTER, uid); -/** Action that updates a filter */ -export const updateFilter = (item: DtNodeDef) => - action(ActionType.UPDATE_FILTER, item); +/** @internal Action that updates a filter */ +export const updateFilter = (filter: DtNodeDef[]) => + action(ActionType.UPDATE_FILTER, filter); -/** Action that subscribes to a new data source */ +/** @internal Action that subscribes to a new data source */ export const switchDataSource = (item: DtFilterFieldDataSource) => action>(ActionType.SWITCH_DATA_SOURCE, item); -/** Action that updates the data source */ +/** @internal Action that updates the data source */ export const updateDataSource = (nodeDef: DtNodeDef) => action(ActionType.UPDATE_DATA_SOURCE, nodeDef); diff --git a/libs/barista-components/experimental/quick-filter/src/state/effects.ts b/libs/barista-components/experimental/quick-filter/src/state/effects.ts index 6219d31479..9daa685703 100644 --- a/libs/barista-components/experimental/quick-filter/src/state/effects.ts +++ b/libs/barista-components/experimental/quick-filter/src/state/effects.ts @@ -19,26 +19,26 @@ import { DtNodeDef, } from '@dynatrace/barista-components/filter-field'; import { MonoTypeOperatorFunction, Observable } from 'rxjs'; -import { filter, map, switchMap } from 'rxjs/operators'; +import { filter, map, switchMap, take } from 'rxjs/operators'; import { Action, ActionType, updateDataSource } from './actions'; import { QuickFilterState } from './store'; -/** Type for an effect */ +/** @internal Type for an effect */ export type Effect = ( action$: Observable, state$?: Observable, ) => Observable; -/** Operator to filter actions */ +/** @internal Operator to filter actions */ export const ofType = ( ...types: ActionType[] ): MonoTypeOperatorFunction> => filter((action: Action) => types.indexOf(action.type) > -1); -/** Connects to a new Data dataSource */ +/** @internal Connects to a new Data dataSource */ export const switchDataSourceEffect: Effect = (action$: Observable) => action$.pipe( ofType>(ActionType.SWITCH_DATA_SOURCE), - switchMap((action) => action.payload!.connect()), + switchMap((action) => action.payload!.connect().pipe(take(1))), map((nodeDef: DtNodeDef) => updateDataSource(nodeDef)), ); diff --git a/libs/barista-components/experimental/quick-filter/src/state/reducer.spec.ts b/libs/barista-components/experimental/quick-filter/src/state/reducer.spec.ts deleted file mode 100644 index e72639e769..0000000000 --- a/libs/barista-components/experimental/quick-filter/src/state/reducer.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @license - * Copyright 2020 Dynatrace LLC - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { dtAutocompleteDef } from '@dynatrace/barista-components/filter-field'; -import { buildData } from './reducer'; - -test('should build the data with an object', () => { - const data = { name: 'Linz' }; - const autocomplete = dtAutocompleteDef(data, null, [], false, false, false); - expect(buildData(autocomplete)).toMatchObject([data]); -}); - -test('should build the data with undefined', () => { - const data = undefined; - const autocomplete = dtAutocompleteDef(data, null, [], false, false, false); - - expect(buildData(autocomplete)).toMatchObject([data]); -}); diff --git a/libs/barista-components/experimental/quick-filter/src/state/reducer.ts b/libs/barista-components/experimental/quick-filter/src/state/reducer.ts index 761d82eb4d..8ff2e6bc61 100644 --- a/libs/barista-components/experimental/quick-filter/src/state/reducer.ts +++ b/libs/barista-components/experimental/quick-filter/src/state/reducer.ts @@ -14,10 +14,7 @@ * limitations under the License. */ import { DtLogger, DtLoggerFactory } from '@dynatrace/barista-components/core'; -import { - DELIMITER, - DtNodeDef, -} from '@dynatrace/barista-components/filter-field'; +import { DtAutocompleteValue } from '@dynatrace/barista-components/filter-field'; import { Action, ActionType } from './actions'; import { initialState, QuickFilterState } from './store'; @@ -50,9 +47,16 @@ export function quickFilterReducer( } return { ...initialState, dataSource: action.payload }; case ActionType.UPDATE_DATA_SOURCE: - return { ...state, nodeDef: action.payload }; + return { + ...state, + filters: [], + nodeDef: action.payload, + }; case ActionType.SET_FILTERS: - return { ...state, filters: action.payload }; + // reset the initial filters as we have already applied them if there are one. + return { ...state, filters: action.payload, initialFilters: undefined }; + case ActionType.ADD_INITIAL_FILTERS: + return { ...state, initialFilters: action.payload }; case ActionType.UNSET_FILTER_GROUP: return { ...state, @@ -77,13 +81,19 @@ export function quickFilterReducer( // # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # /** @internal Add a filter to the filters array */ -export function addFilter(filters: any[][], item: DtNodeDef): any[][] { - return [...filters, buildData(item)]; +export function addFilter( + filters: DtAutocompleteValue[][], + filter: DtAutocompleteValue[], +): DtAutocompleteValue[][] { + return [...filters, filter]; } /** @internal Remove a filter from the filters array */ -export function removeFilter(filters: any[][], item: DtNodeDef): any[][] { - const index = findSelectedOption(filters, item, false); +export function removeFilter( + filters: DtAutocompleteValue[][], + uid: string, +): DtAutocompleteValue[][] { + const index = findSelectedOption(filters, uid, false); const updatedState = [...filters]; if (index > -1) { @@ -94,70 +104,48 @@ export function removeFilter(filters: any[][], item: DtNodeDef): any[][] { } /** @internal Update a filter inside the filters array */ -export function updateFilter(filters: any[][], item: DtNodeDef): any[][] { - const index = findSelectedOption(filters, item, true); - +export function updateFilter( + filters: DtAutocompleteValue[][], + filter: DtAutocompleteValue[], +): DtAutocompleteValue[][] { + const uid = filter[filter.length - 1].option.uid; + const index = findSelectedOption(filters, uid, true); + // if the filter is not in the filters list add it if (index < 0) { - return addFilter(filters, item); + return addFilter(filters, filter); } - - filters[index] = buildData(item); - + // replace the existing filter + filters[index] = filter; return filters; } /** @internal Remove a group from the filters array */ -export function unsetFilterGroup(filters: any[][], group: DtNodeDef): any[][] { - if (group.option && group.option.viewValue) { - return filters.filter( - (filter) => filter[0].name !== group.option!.viewValue, - ); - } - return filters; -} - -/** @internal Build the quick filter data out of a node definition */ -export function buildData(item: DtNodeDef): any[] { - const data = [item.data]; - - if (item.option && item.option.parentAutocomplete) { - data.unshift(item.option.parentAutocomplete.data); - } - return data; +export function unsetFilterGroup( + filters: DtAutocompleteValue[][], + group: DtAutocompleteValue, +): DtAutocompleteValue[][] { + const index = findSelectedOption(filters, group.option.uid, true); + return filters.filter((_, i) => i !== index); } /** @internal Find a filter inside the filters array based on a NodeDef */ export function findSelectedOption( - filters: any[][], - item: DtNodeDef, + filters: DtAutocompleteValue[][], + uid: string | null, distinct: boolean = false, ): number { return filters.findIndex((path) => { - if (item.option && item.option.uid) { - // split the uid in the parts that define the path - // (like the user clicked through in the filter field) - const parts = item.option.uid.split(DELIMITER); - - // if the option is distinct we only have to check for the groups name because - // there can only be one distinct option selected so we know immediately if it is selected - if (distinct && parts[0] === path[0].name) { - return true; - } - - // if it is not distinct we have to build the full path out of the current filters to check - // wether the path matches them provided node definition - const dataPath = path.reduce( - (previousValue, currentValue) => - `${previousValue.name}${DELIMITER}${currentValue.name}${DELIMITER}`, - ); - - // if the built path for the filters inside the array is equal to the option - // in the provided nodeDef then we found our selected option - if (item.option.uid === dataPath) { - return true; - } + if (!uid) { + return false; + } + // if the option is distinct we only have to check for the groups name because + // there can only be one distinct option selected so we know immediately if it is selected + if (distinct && uid.startsWith(path[0].option.uid!)) { + return true; + } + // if the last items uid in the path is equal to the uid, then we found our option. + if (uid === path[path.length - 1].option.uid) { + return true; } - - return false; }); } diff --git a/libs/barista-components/experimental/quick-filter/src/state/selectors.ts b/libs/barista-components/experimental/quick-filter/src/state/selectors.ts index 5f90dad464..f2a2d9bb83 100644 --- a/libs/barista-components/experimental/quick-filter/src/state/selectors.ts +++ b/libs/barista-components/experimental/quick-filter/src/state/selectors.ts @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { isDefined } from '@dynatrace/barista-components/core'; import { applyDtOptionIds, DtNodeDef, isDtAutocompleteDef, } from '@dynatrace/barista-components/filter-field'; import { Observable } from 'rxjs'; -import { filter, map, pluck, tap, withLatestFrom } from 'rxjs/operators'; +import { filter, map, tap, withLatestFrom } from 'rxjs/operators'; import { DtQuickFilterDataSource } from '../quick-filter-data-source'; import { QuickFilterState } from './store'; -import { isDefined } from '@dynatrace/barista-components/core'; /** @internal Select all autocompletes from the root Node Def from the store */ export const getAutocompletes = ( @@ -35,7 +35,7 @@ export const getAutocompletes = ( applyDtOptionIds(state.nodeDef); } }), - pluck('nodeDef'), + map(({ nodeDef }) => nodeDef), filter((state) => isDefined(state) && isDtAutocompleteDef(state)), withLatestFrom( getDataSource(state$).pipe(filter(Boolean)), @@ -51,8 +51,12 @@ export const getAutocompletes = ( /** @internal Select the data Source from the store */ export const getDataSource = (state$: Observable) => - state$.pipe(pluck('dataSource')); + state$.pipe(map((state) => state?.dataSource)); /** @internal Select the actual applied filters */ export const getFilters = (state$: Observable) => - state$.pipe(pluck('filters')); + state$.pipe(map(({ filters }) => filters)); + +/** @internal Get the initial filters */ +export const getInitialFilters = (state$: Observable) => + state$.pipe(map(({ initialFilters }) => initialFilters)); diff --git a/libs/barista-components/experimental/quick-filter/src/state/store.ts b/libs/barista-components/experimental/quick-filter/src/state/store.ts index 723e10cc3f..95c70ff365 100644 --- a/libs/barista-components/experimental/quick-filter/src/state/store.ts +++ b/libs/barista-components/experimental/quick-filter/src/state/store.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { DtNodeDef } from '@dynatrace/barista-components/filter-field'; +import { + DtAutocompleteValue, + DtNodeDef, +} from '@dynatrace/barista-components/filter-field'; import { BehaviorSubject, merge, Observable } from 'rxjs'; import { map, shareReplay, withLatestFrom } from 'rxjs/operators'; import { DtQuickFilterDataSource } from '../quick-filter-data-source'; @@ -28,8 +31,10 @@ export interface QuickFilterState { nodeDef?: DtNodeDef; /** The dataSource that is connected with the QuickFilter */ dataSource?: DtQuickFilterDataSource; - /** Array of all active filters */ - filters: any[][]; + /** Array of all active filter values (internal filter representation of the filter field) */ + filters: DtAutocompleteValue[][]; + /** Initial Filter array that might be added via a binding to the quick filter */ + initialFilters?: any[][]; } /** @internal The initial QuickFilter state */ diff --git a/libs/barista-components/filter-field/index.ts b/libs/barista-components/filter-field/index.ts index 01c80ff7e2..fd59ed5790 100644 --- a/libs/barista-components/filter-field/index.ts +++ b/libs/barista-components/filter-field/index.ts @@ -53,5 +53,7 @@ export { isDtAutocompleteValue, isDtFreeTextValue, isDtRangeValue, + DtAutocompleteValue, + _getSourcesOfDtFilterValues, } from './src/types'; export { DT_FILTER_VALUES_PARSER_CONFIG } from './src/filter-field-config'; diff --git a/libs/barista-components/filter-field/src/filter-field.ts b/libs/barista-components/filter-field/src/filter-field.ts index d4c5afbc1a..763a7e377b 100644 --- a/libs/barista-components/filter-field/src/filter-field.ts +++ b/libs/barista-components/filter-field/src/filter-field.ts @@ -261,6 +261,10 @@ export class DtFilterField this._tryApplyFilters(value); this._changeDetectorRef.markForCheck(); } + /** @internal */ + get _filterValues(): DtFilterValue[][] { + return this._filters; + } private _filters: DtFilterValue[][] = []; /** Set the Aria-Label attribute */