From 5e557612c73726c7fdb1b68ad67f91372fe68918 Mon Sep 17 00:00:00 2001 From: Lukas Holzer Date: Wed, 5 Feb 2020 14:32:19 +0100 Subject: [PATCH] feat(quick-filter): Add a new quick filter component inside an experimental package. The quick filter component is used to provide a quick way to operate with the filter field inside a sidebar. Inside the quick filter only an autocomplete with simple options can be displayed. This is part of an experimental package and not meant to be used inside production. The experimental package does not follow semantic versioning like the rest of the library does. This means that we might break the api in every version. It is only meant to be used for testing and feedback purpose. Fixes #453 Fixes #254 --- .vscode/settings.json | 3 +- angular.json | 38 +++ .../src/app/app.routing.module.ts | 13 + .../filter-field/filter-field.po.ts | 15 +- .../components/filter-field/filter-field.ts | 10 +- .../quick-filter-initial-data.html | 8 + .../quick-filter-initial-data.ts | 36 +++ .../quick-filter/quick-filter.e2e.ts | 157 +++++++++++ .../quick-filter/quick-filter.module.ts | 35 +++ .../quick-filter/quick-filter.po.ts | 44 ++++ .../quick-filter/quick-filter.html | 15 ++ .../quick-filter/quick-filter/quick-filter.ts | 43 +++ .../src/components/quick-filter/util.ts | 0 .../drawer/src/drawer-container.scss | 1 - .../experimental/quick-filter/README.md | 1 + .../experimental/quick-filter/barista.json | 25 ++ .../experimental/quick-filter/index.ts | 20 ++ .../experimental/quick-filter/jest.config.js | 9 + .../experimental/quick-filter/package.json | 7 + .../src/quick-filter-data-source.ts | 92 +++++++ .../src/quick-filter-default-data-source.ts | 249 ++++++++++++++++++ .../quick-filter/src/quick-filter-group.html | 34 +++ .../quick-filter/src/quick-filter-group.scss | 22 ++ .../quick-filter/src/quick-filter-group.ts | 140 ++++++++++ .../quick-filter/src/quick-filter-utils.ts | 26 ++ .../quick-filter/src/quick-filter.html | 45 ++++ .../quick-filter/src/quick-filter.module.ts | 43 +++ .../quick-filter/src/quick-filter.scss | 67 +++++ .../quick-filter/src/quick-filter.spec.ts | 166 ++++++++++++ .../quick-filter/src/quick-filter.ts | 131 +++++++++ .../quick-filter/src/state/actions.ts | 62 +++++ .../quick-filter/src/state/effects.ts | 43 +++ .../quick-filter/src/state/reducer.spec.ts | 54 ++++ .../quick-filter/src/state/reducer.ts | 154 +++++++++++ .../quick-filter/src/state/selectors.ts | 49 ++++ .../quick-filter/src/state/store.ts | 108 ++++++++ .../quick-filter/src/test-setup.ts | 17 ++ .../experimental/quick-filter/tsconfig.json | 7 + .../quick-filter/tsconfig.lib.json | 20 ++ .../quick-filter/tsconfig.spec.json | 10 + .../experimental/quick-filter/tslint.json | 4 + libs/barista-components/filter-field/index.ts | 3 +- .../filter-field/src/filter-field.ts | 7 +- .../src/mock/mock-icon-testing-module.ts | 54 ++++ nx.json | 3 + tsconfig.json | 3 + 46 files changed, 2082 insertions(+), 11 deletions(-) create mode 100644 apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.html create mode 100644 apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.ts create mode 100644 apps/components-e2e/src/components/quick-filter/quick-filter.e2e.ts create mode 100644 apps/components-e2e/src/components/quick-filter/quick-filter.module.ts create mode 100644 apps/components-e2e/src/components/quick-filter/quick-filter.po.ts create mode 100644 apps/components-e2e/src/components/quick-filter/quick-filter/quick-filter.html create mode 100644 apps/components-e2e/src/components/quick-filter/quick-filter/quick-filter.ts create mode 100644 apps/components-e2e/src/components/quick-filter/util.ts create mode 100644 libs/barista-components/experimental/quick-filter/README.md create mode 100644 libs/barista-components/experimental/quick-filter/barista.json create mode 100644 libs/barista-components/experimental/quick-filter/index.ts create mode 100644 libs/barista-components/experimental/quick-filter/jest.config.js create mode 100644 libs/barista-components/experimental/quick-filter/package.json create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter-data-source.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter-default-data-source.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter-group.html create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter-group.scss create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter-group.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter-utils.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter.html create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter.module.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter.scss create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter.spec.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/state/actions.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/state/effects.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/state/reducer.spec.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/state/reducer.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/state/selectors.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/state/store.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/test-setup.ts create mode 100644 libs/barista-components/experimental/quick-filter/tsconfig.json create mode 100644 libs/barista-components/experimental/quick-filter/tsconfig.lib.json create mode 100644 libs/barista-components/experimental/quick-filter/tsconfig.spec.json create mode 100644 libs/barista-components/experimental/quick-filter/tslint.json create mode 100644 libs/testing/src/mock/mock-icon-testing-module.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 561c8e081c..3a5d8abc49 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "editor.tabSize": 2, "files.eol": "\n", "editor.defaultFormatter": "esbenp.prettier-vscode", - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "explorer.compactFolders": false } diff --git a/angular.json b/angular.json index f1622ff148..03bdbd5cb0 100644 --- a/angular.json +++ b/angular.json @@ -2522,6 +2522,44 @@ }, "schematics": {} }, + "quick-filter": { + "projectType": "library", + "root": "components/experimental/quick-filter", + "sourceRoot": "components/experimental/quick-filter/src", + "prefix": "dt", + "architect": { + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "components/experimental/quick-filter/tsconfig.lib.json", + "components/experimental/quick-filter/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**", + "!components/experimental/quick-filter/**" + ] + } + }, + "lint-styles": { + "builder": "@dynatrace/barista-builders:stylelint", + "options": { + "stylelintConfig": ".stylelintrc", + "reportFile": "dist/stylelint/report.xml", + "exclude": ["**/node_modules/**"], + "files": ["components/experimental/quick-filter/**/*.scss"] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "options": { + "jestConfig": "components/experimental/quick-filter/jest.config.js", + "tsConfig": "components/experimental/quick-filter/tsconfig.spec.json", + "setupFile": "components/experimental/quick-filter/src/test-setup.ts" + } + } + } + }, "radial-chart": { "projectType": "library", "root": "libs/barista-components/radial-chart", diff --git a/apps/components-e2e/src/app/app.routing.module.ts b/apps/components-e2e/src/app/app.routing.module.ts index 980c5c30d0..25c50ca510 100644 --- a/apps/components-e2e/src/app/app.routing.module.ts +++ b/apps/components-e2e/src/app/app.routing.module.ts @@ -19,6 +19,12 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; export const routes: Routes = [ + { + // TODO: remove only for testing + path: '', + pathMatch: 'full', + redirectTo: '/quick-filter', + }, { path: 'autocomplete', loadChildren: () => @@ -131,6 +137,13 @@ export const routes: Routes = [ module => module.DtE2EProgressBarModule, ), }, + { + path: 'quick-filter', + loadChildren: () => + import('../components/quick-filter/quick-filter.module').then( + module => module.DtE2EQuickFilterModule, + ), + }, { path: 'radial-chart', loadChildren: () => diff --git a/apps/components-e2e/src/components/filter-field/filter-field.po.ts b/apps/components-e2e/src/components/filter-field/filter-field.po.ts index fb639ad4f8..dc6f6dc534 100644 --- a/apps/components-e2e/src/components/filter-field/filter-field.po.ts +++ b/apps/components-e2e/src/components/filter-field/filter-field.po.ts @@ -18,11 +18,17 @@ import { Selector, t, ClientFunction } from 'testcafe'; export const errorBox = Selector('.dt-filter-field-error'); export const filterField = Selector('#filter-field'); -export const option = (nth: number) => Selector(`.dt-option:nth-child(${nth})`); +export const option = (nth: number) => Selector('.dt-option').nth(nth); export const clearAll = Selector('.dt-filter-field-clear-all-button'); export const filterTags = Selector('dt-filter-field-tag'); export const tagOverlay = Selector('.dt-overlay-container'); +/** Selector for the delete button (x) on a filter with a specific text */ +export const tagDeleteButton = (filterText: string) => + Selector('dt-filter-field-tag') + .withText(filterText) + .child('.dt-filter-field-tag-button'); + export const input = Selector('input'); export const switchToFirstDatasource = Selector('#switchToFirstDatasource'); @@ -36,9 +42,8 @@ export function clickOption( const controller = testController || t; return controller - .click(filterField, { speed: 0.2 }) - .wait(250) - .click(option(nth), { speed: 0.2 }); + .click(filterField, { speed: 0.4 }) + .click(option(nth), { speed: 0.4 }); } /** Focus the input of the filter field to send key events to it. */ @@ -46,7 +51,7 @@ export const focusFilterFieldInput = ClientFunction(() => { (document.querySelector('#filter-field input') as HTMLElement).focus(); }); -/** Retreive all set tags in the filter field and their values. */ +/** Retrieve all set tags in the filter field and their values. */ export const getFilterfieldTags = ClientFunction(() => { const filterFieldTags: HTMLElement[] = [].slice.call( document.querySelectorAll('.dt-filter-field-tag'), diff --git a/apps/components-e2e/src/components/filter-field/filter-field.ts b/apps/components-e2e/src/components/filter-field/filter-field.ts index c9d4020fe5..b60ca72c28 100644 --- a/apps/components-e2e/src/components/filter-field/filter-field.ts +++ b/apps/components-e2e/src/components/filter-field/filter-field.ts @@ -106,10 +106,18 @@ const TEST_DATA_2 = { unit: 's', }, }, + { + name: 'Not in Quickfilter', + autocomplete: [ + { name: 'Option1' }, + { name: 'Option2' }, + { name: 'Option3' }, + ], + }, ], }; -const DATA = [TEST_DATA, TEST_DATA_2]; +export const DATA = [TEST_DATA, TEST_DATA_2]; @Component({ selector: 'dt-e2e-filter-field', 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 new file mode 100644 index 0000000000..64d492a50e --- /dev/null +++ b/apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.html @@ -0,0 +1,8 @@ + + Quick-filter + + All options in the filter field above + + + 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 new file mode 100644 index 0000000000..2d54337157 --- /dev/null +++ b/apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.ts @@ -0,0 +1,36 @@ +/** + * @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 { DtFilterFieldDefaultDataSource } from '@dynatrace/barista-components/filter-field'; +import { DATA } from '../../filter-field/filter-field'; + +@Component({ + selector: 'dt-e2e-quick-filter', + templateUrl: 'quick-filter-initial-data.html', +}) +export class DtE2EQuickFilterInitialData { + _dataSource = new DtFilterFieldDefaultDataSource(DATA[1]); + + _initialFilters = [ + [DATA[1].autocomplete[0], DATA[1].autocomplete[0].autocomplete![1]], + [DATA[1].autocomplete[1], DATA[1].autocomplete[1].autocomplete![2]], + ]; + + constructor() { + console.log(this._initialFilters); + } +} 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 new file mode 100644 index 0000000000..d533744f6a --- /dev/null +++ b/apps/components-e2e/src/components/quick-filter/quick-filter.e2e.ts @@ -0,0 +1,157 @@ +/** + * @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 { waitForAngular } from '../../utils/wait-for-angular'; +import { + clearAll, + getFilterfieldTags, + tagDeleteButton, +} from '../filter-field/filter-field.po'; +import { + getGroupItem, + getGroupItemInput, + getSelectedItem, +} from './quick-filter.po'; + +fixture('Quick Filter') + .page('http://localhost:4200/quick-filter') + .meta({ + 'filter-field': true, + 'quick-filter': true, + drawer: true, + checkbox: true, + radio: true, + }) + .beforeEach(async (testController: TestController) => { + await testController.resizeWindow(1200, 800); + await waitForAngular(); + }); + +test('if nothing is selected the distinct should be set to all', async (testController: TestController) => { + await testController + .expect(getSelectedItem('AUT').textContent) + .match(/Any/) + .expect(getSelectedItem('USA').exists) + .notOk(); +}); + +test('if nothing is selected the filter friedl should be empty', async (testController: TestController) => { + await testController.expect(getFilterfieldTags()).eql([]); +}); + +test('if distinct option gets updated it should update the filter field', async (testController: TestController) => { + await testController + .expect(getFilterfieldTags()) + .eql([]) + .click(getGroupItem('AUT', 'Linz'), { speed: 0.3 }) + .expect(getFilterfieldTags()) + .eql(['AUTLinz']) + .expect(getSelectedItem('AUT').textContent) + .match(/Linz/) + .click(getGroupItem('AUT', 'Graz'), { speed: 0.3 }) + .expect(getFilterfieldTags()) + .eql(['AUTGraz']) + .expect(getSelectedItem('AUT').textContent) + .match(/Graz/); +}); + +test('if it is possible to select multiple options', async (testController: TestController) => { + await testController + .click(getGroupItem('USA', 'San Francisco'), { speed: 0.4 }) + .expect(getFilterfieldTags()) + .eql(['USASan Francisco']) + .click(getGroupItem('USA', 'Los Angeles'), { speed: 0.4 }) + .expect(getFilterfieldTags()) + .eql(['USASan Francisco', 'USALos Angeles']) + .click(getGroupItem('USA', 'New York'), { speed: 0.4 }) + .expect(getFilterfieldTags()) + .eql(['USASan Francisco', 'USALos Angeles', 'USANew York']); +}); + +test('if it is possible to select and deselect multiple options', async (testController: TestController) => { + await testController + .click(getGroupItem('USA', 'San Francisco')) + .click(getGroupItem('USA', 'Los Angeles')) + .click(getGroupItem('USA', 'New York'), { speed: 0.4 }) + .expect(getFilterfieldTags()) + .eql(['USASan Francisco', 'USALos Angeles', 'USANew York']) + .click(getGroupItem('USA', 'San Francisco'), { speed: 0.4 }) + .expect(getFilterfieldTags()) + .eql(['USALos Angeles', 'USANew York']); +}); + +test('if it is possible to reset all filters via the filter fields clearAll button', async (testController: TestController) => { + await testController + .click(getGroupItem('USA', 'San Francisco')) + .click(getGroupItem('USA', 'Los Angeles')) + .click(getGroupItem('USA', 'New York'), { speed: 0.4 }) + .expect(getFilterfieldTags()) + .eql(['USASan Francisco', 'USALos Angeles', 'USANew York']) + .click(clearAll, { speed: 0.4 }) + .expect(getFilterfieldTags()) + .eql([]) + .expect(getSelectedItem('USA').exists) + .notOk(); +}); + +test('if it is possible to delete an option via the filter field', async (testController: TestController) => { + await testController + .click(getGroupItem('USA', 'San Francisco')) + .click(getGroupItem('USA', 'Los Angeles')) + .click(getGroupItem('USA', 'New York'), { speed: 0.4 }) + .click(tagDeleteButton('Los Angeles'), { speed: 0.4 }) + .expect(getFilterfieldTags()) + .eql(['USASan Francisco', 'USANew York']) + .expect(getGroupItemInput('USA', 'New York').checked) + .ok() + .expect(getGroupItemInput('USA', 'Los Angeles').checked) + .notOk(); +}); + +fixture('Quick Filter with initial Data') + .page('http://localhost:4200/quick-filter/initial-data') + .beforeEach(async (testController: TestController) => { + await testController.resizeWindow(1200, 800); + await waitForAngular(); + }); + +test('if the initial filter in the filter field reflects the quick filter state', async (testController: TestController) => { + await testController + .expect(getSelectedItem('AUT').textContent) + .match(/Vienna/) + .expect(getSelectedItem('USA').textContent) + .match(/New York/); +}); + +test('if the initial filters are reflected in the filter field ', async (testController: TestController) => { + await testController + .expect(getFilterfieldTags()) + .eql(['AUTVienna', 'USANew York']); +}); + +test('if a distinct group get set to any remove the group from the filter', async (testController: TestController) => { + await testController + .expect(getFilterfieldTags()) + .eql(['AUTVienna', 'USANew York']) + .click(getGroupItem('AUT', 'Any'), { speed: 0.3 }) + .expect(getFilterfieldTags()) + .eql(['USANew York']) + .click(getGroupItem('AUT', 'Graz'), { speed: 0.3 }) + .expect(getFilterfieldTags()) + .eql(['USANew York', 'AUTGraz']) + .expect(getSelectedItem('AUT').textContent) + .match(/Graz/); +}); 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 new file mode 100644 index 0000000000..9949a31d66 --- /dev/null +++ b/apps/components-e2e/src/components/quick-filter/quick-filter.module.ts @@ -0,0 +1,35 @@ +/** + * @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 { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +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'; + +const routes: Route[] = [ + { path: '', component: DtE2EQuickFilter }, + { path: 'initial-data', component: DtE2EQuickFilterInitialData }, +]; + +@NgModule({ + declarations: [DtE2EQuickFilter, DtE2EQuickFilterInitialData], + imports: [CommonModule, RouterModule.forChild(routes), DtQuickFilterModule], + exports: [], + providers: [], +}) +export class DtE2EQuickFilterModule {} diff --git a/apps/components-e2e/src/components/quick-filter/quick-filter.po.ts b/apps/components-e2e/src/components/quick-filter/quick-filter.po.ts new file mode 100644 index 0000000000..915bbc6f65 --- /dev/null +++ b/apps/components-e2e/src/components/quick-filter/quick-filter.po.ts @@ -0,0 +1,44 @@ +/** + * @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 { Selector } from 'testcafe'; + +export const quickFilterCloseButton = Selector('.dt-quick-filter-close'); +export const quickFilterOpenButton = Selector('.dt-quick-filter-open'); +export const quickFilterGroup = Selector('.dt-quick-filter-group'); + +/** get a group by its name */ +export const getGroup = (group: string) => + quickFilterGroup + .child('.dt-quick-filter-group-headline') + .withText(group) + .sibling(); + +/** get a group item by its group and its name */ +export const getGroupItem = (group: string, item: string) => + getGroup(group) + .child('.dt-quick-filter-group-items > *') + .withText(item); + +/** get the native input of the specified group item */ +export const getGroupItemInput = (group: string, item: string) => + getGroupItem(group, item) + .child('label') + .child('input'); + +/** get the selected item of the group */ +export const getSelectedItem = (groupText: string) => + getGroup(groupText).child('.dt-checkbox-checked, .dt-radio-checked'); diff --git a/apps/components-e2e/src/components/quick-filter/quick-filter/quick-filter.html b/apps/components-e2e/src/components/quick-filter/quick-filter/quick-filter.html new file mode 100644 index 0000000000..026ff0bdef --- /dev/null +++ b/apps/components-e2e/src/components/quick-filter/quick-filter/quick-filter.html @@ -0,0 +1,15 @@ + + Quick-filter + + All options in the filter field above + + + my content + + + + 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 new file mode 100644 index 0000000000..73bd4a25dc --- /dev/null +++ b/apps/components-e2e/src/components/quick-filter/quick-filter/quick-filter.ts @@ -0,0 +1,43 @@ +/** + * @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 { DATA } from '../../filter-field/filter-field'; +import { + DtQuickFilterDefaultDataSource, + DtQuickFilterDefaultDataSourceConfig, +} from '@dynatrace/barista-components/experimental/quick-filter'; +import { isObject } from 'util'; + +const config: DtQuickFilterDefaultDataSourceConfig = { + showInSidebar: node => + isObject(node) && node.name && node.name !== 'Not in Quickfilter', +}; + +@Component({ + selector: 'dt-e2e-quick-filter', + templateUrl: 'quick-filter.html', +}) +export class DtE2EQuickFilter { + _dataSource = new DtQuickFilterDefaultDataSource(DATA[1], config); + + switchToDataSource(targetIndex: number): void { + this._dataSource = new DtQuickFilterDefaultDataSource( + DATA[targetIndex], + config, + ); + } +} diff --git a/apps/components-e2e/src/components/quick-filter/util.ts b/apps/components-e2e/src/components/quick-filter/util.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/barista-components/drawer/src/drawer-container.scss b/libs/barista-components/drawer/src/drawer-container.scss index 38b5d31bce..4539c4ae82 100644 --- a/libs/barista-components/drawer/src/drawer-container.scss +++ b/libs/barista-components/drawer/src/drawer-container.scss @@ -40,7 +40,6 @@ $dt-backdrop-opacity: 0.4; flex-grow: 1; flex-shrink: 1; display: block; - height: 100%; overflow: auto; will-change: contents; diff --git a/libs/barista-components/experimental/quick-filter/README.md b/libs/barista-components/experimental/quick-filter/README.md new file mode 100644 index 0000000000..87565d48a2 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/README.md @@ -0,0 +1 @@ +# Quick Filter diff --git a/libs/barista-components/experimental/quick-filter/barista.json b/libs/barista-components/experimental/quick-filter/barista.json new file mode 100644 index 0000000000..dc2002e0a9 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/barista.json @@ -0,0 +1,25 @@ +{ + "title": "Quick Filter", + "description": "", + "postid": "quick-filter", + "identifier": "Qf", + "category": "components", + "public": true, + "contributors": { + "dev": [ + { + "name": "Lukas Holzer", + "gitHubUser": "lukasholzer" + } + ], + "ux": [ + { + "name": "Xavier Javaloyas", + "gitHubUser": "Xavi-J" + } + ] + }, + "properties": ["work in progress", "experimental"], + "related": ["filter field"], + "tags": ["filter", "filter field", "quick filter", "angular", "component"] +} diff --git a/libs/barista-components/experimental/quick-filter/index.ts b/libs/barista-components/experimental/quick-filter/index.ts new file mode 100644 index 0000000000..cf545b7620 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/index.ts @@ -0,0 +1,20 @@ +/** + * @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. + */ + +export * from './src/quick-filter'; +export * from './src/quick-filter-data-source'; +export * from './src/quick-filter-default-data-source'; +export * from './src/quick-filter.module'; diff --git a/libs/barista-components/experimental/quick-filter/jest.config.js b/libs/barista-components/experimental/quick-filter/jest.config.js new file mode 100644 index 0000000000..367d5c78ef --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + name: 'quick-filter', + preset: '../../../jest.config.js', + coverageDirectory: '../../../coverage/components/experimental/quick-filter', + snapshotSerializers: [ + 'jest-preset-angular/AngularSnapshotSerializer.js', + 'jest-preset-angular/HTMLCommentSerializer.js', + ], +}; diff --git a/libs/barista-components/experimental/quick-filter/package.json b/libs/barista-components/experimental/quick-filter/package.json new file mode 100644 index 0000000000..dedb72ce9c --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/package.json @@ -0,0 +1,7 @@ +{ + "ngPackage": { + "lib": { + "entryFile": "index.ts" + } + } +} diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter-data-source.ts b/libs/barista-components/experimental/quick-filter/src/quick-filter-data-source.ts new file mode 100644 index 0000000000..42692693e5 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-data-source.ts @@ -0,0 +1,92 @@ +/** + * @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 { Observable } from 'rxjs'; +import { DtNodeDef, DtFilterFieldDataSource } from '@dynatrace/barista-components/filter-field'; + +export abstract class DtQuickFilterDataSource implements DtFilterFieldDataSource { + /** + * Used by the DtFilterFieldControl. Called when it connects to the data source. + * Should return a stream of data that will be transformed, filtered and + * displayed by the DtFilterField and the DtFilterFieldControl. + */ + abstract connect(): Observable; + + /** Used by the DtFilterField. Called when it is destroyed. */ + abstract disconnect(): void; + + /** Whether the provided data object can be transformed into an DtAutocompleteDef. */ + abstract isAutocomplete(data: T): boolean; + + /** Whether the provided data object can be transformed into an DtOptionDef. */ + abstract isOption(data: T): boolean; + + /** Whether the provided data object can be transformed into an DtGroupDef. */ + abstract isGroup(data: T): boolean; + + /** Whether the provided data object can be transformed into an DtFreeTextDef. */ + abstract isFreeText(data: T): boolean; + + /** Whether the provided data object can be transformed into an DtRangeDef. */ + abstract isRange(data: T): boolean; + + /** Transforms the provided data into a DtNodeDef which contains a DtAutocompleteDef. */ + abstract transformAutocomplete( + data: T, + parent: DtNodeDef | null, + existingDef: DtNodeDef | null, + ): DtNodeDef; + + /** Transforms the provided data into a DtNodeDef which contains a DtOptionDef. */ + abstract transformOption( + data: T, + parentAutocompleteOrOption: DtNodeDef | null, + existingDef: DtNodeDef | null, + ): DtNodeDef; + + /** Transforms the provided data into a DtNodeDef which contains a DtGroupDef. */ + abstract transformGroup( + data: T, + parentAutocomplete: DtNodeDef | null, + existingDef: DtNodeDef | null, + ): DtNodeDef; + + /** Transforms the provided data into a DtNodeDef which contains a DtFreeTextDef. */ + abstract transformFreeText( + data: T, + parent: DtNodeDef | null, + existingDef: DtNodeDef | null, + ): DtNodeDef; + + /** Transforms the provided data into a DtNodeDef which contains a DtRangeDef. */ + abstract transformRange( + data: T, + parent: DtNodeDef | null, + existingDef: DtNodeDef | null, + ): DtNodeDef; + + /** Transforms the provided data into a DtNodeDef. */ + abstract transformObject( + data: T | null, + parent: DtNodeDef | null, + ): DtNodeDef | null; + + /** Transforms the provided list of data objects into an array of DtNodeDefs. */ + abstract transformList( + list: T[], + parent: DtNodeDef | null, + ): DtNodeDef[]; +} diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter-default-data-source.ts b/libs/barista-components/experimental/quick-filter/src/quick-filter-default-data-source.ts new file mode 100644 index 0000000000..fa53616797 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-default-data-source.ts @@ -0,0 +1,249 @@ +/** + * @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 { DtQuickFilterDataSource } from './quick-filter-data-source'; +import { isDefined, isObject } from '@dynatrace/barista-components/core'; +import { + DtNodeDef, + dtRangeDef, + dtFreeTextDef, + dtGroupDef, + dtOptionDef, + isDtAutocompleteDef, + DtFilterFieldDefaultDataSourceAutocomplete, + DtFilterFieldDefaultDataSourceSimpleGroup, + DtFilterFieldDefaultDataSourceGroup, + DtFilterFieldDefaultDataSourceFreeText, + DtFilterFieldDefaultDataSourceRange, + dtAutocompleteDef, + isDtGroupDef, +} from '@dynatrace/barista-components/filter-field'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +export interface DtQuickFilterDefaultDataSourceSimpleOption { + name: string; +} + +export type DtQuickFilterDefaultDataSourceRange = DtFilterFieldDefaultDataSourceRange; +export type DtQuickFilterDefaultDataSourceFreeText = DtFilterFieldDefaultDataSourceFreeText; +export type DtQuickFilterDefaultDataSourceAutocomplete = DtFilterFieldDefaultDataSourceAutocomplete; +export type DtQuickFilterDefaultDataSourceGroup = DtFilterFieldDefaultDataSourceGroup; +export type DtQuickFilterDefaultDataSourceSimpleGroup = DtFilterFieldDefaultDataSourceSimpleGroup; + +export type DtQuickFilterDefaultDataSourceType = + | DtQuickFilterDefaultDataSourceOption + | DtQuickFilterDefaultDataSourceGroup + | DtQuickFilterDefaultDataSourceAutocomplete + | DtQuickFilterDefaultDataSourceFreeText + | DtQuickFilterDefaultDataSourceRange; + +export type DtQuickFilterDefaultDataSourceOption = + | DtQuickFilterDefaultDataSourceSimpleOption + | (DtQuickFilterDefaultDataSourceAutocomplete & + DtQuickFilterDefaultDataSourceSimpleOption) + | (DtQuickFilterDefaultDataSourceFreeText & + DtQuickFilterDefaultDataSourceSimpleOption) + | (DtQuickFilterDefaultDataSourceRange & + DtQuickFilterDefaultDataSourceSimpleOption); + +export interface DtQuickFilterDefaultDataSourceConfig { + showInSidebar: (node: any) => boolean; +} + +export class DtQuickFilterDefaultDataSource< + T extends DtQuickFilterDefaultDataSourceType +> implements DtQuickFilterDataSource { + private readonly _data$: BehaviorSubject; + + /** Structure of data that is used, transformed and rendered by the filter-field. */ + get data(): T { + return this._data$.value; + } + set data(data: T) { + this._data$.next(data); + } + + constructor( + initialData: T = (null as unknown) as T, + config: DtQuickFilterDefaultDataSourceConfig, + ) { + this._data$ = new BehaviorSubject(initialData); + } + + /** + * Used by the DtQuickFilter. Called when it connects to the data source. + * Should return a stream of data that will be transformed, filtered and + * displayed by the DtQuickFilterViewer (filter-field) + */ + connect(): Observable { + return this._data$.pipe(map(data => this.transformObject(data))); + } + + /** Used by the DtQuickFilter. Called when it is destroyed. No-op. */ + disconnect(): void { + this._data$.complete(); + } + + /** Whether the provided data object is of type AutocompleteData */ + isAutocomplete( + data: any, + ): data is DtQuickFilterDefaultDataSourceAutocomplete { + return isObject(data) && Array.isArray(data.autocomplete); + } + + /** Whether the provided data object is of type OptionData */ + isOption(data: any): data is DtQuickFilterDefaultDataSourceOption { + return isObject(data) && typeof data.name === 'string'; + } + + /** Whether the provided data object is of type GroupData */ + isGroup(data: any): data is DtQuickFilterDefaultDataSourceGroup { + return ( + isObject(data) && + typeof data.name === 'string' && + Array.isArray(data.options) + ); + } + + /** Whether the provided data object is of type FreeTextData */ + isFreeText(data: any): data is DtQuickFilterDefaultDataSourceFreeText { + return isObject(data) && Array.isArray(data.suggestions); + } + + /** Whether the provided data object is of type RangeData */ + isRange(data: any): data is DtQuickFilterDefaultDataSourceRange { + return isObject(data) && isObject(data.range); + } + + /** Transforms the provided data into a DtNodeDef which contains a DtAutocompleteDef. */ + transformAutocomplete( + data: DtQuickFilterDefaultDataSourceAutocomplete, + ): DtNodeDef { + const def = dtAutocompleteDef( + data, + null, + [], + !!data.distinct, + !!data.async, + ); + def.autocomplete!.optionsOrGroups = this.transformList( + data.autocomplete, + def, + ); + return def; + } + + /** Transforms the provided data into a DtNodeDef which contains a DtOptionDef. */ + transformOption( + data: DtQuickFilterDefaultDataSourceOption, + parentAutocompleteOrOption: DtNodeDef | null = null, + existingDef: DtNodeDef | null = null, + ): DtNodeDef { + const parentGroup = isDtGroupDef(parentAutocompleteOrOption) + ? parentAutocompleteOrOption + : null; + const parentAutocomplete = + parentGroup !== null + ? parentGroup.group.parentAutocomplete + : isDtAutocompleteDef(parentAutocompleteOrOption) + ? (parentAutocompleteOrOption as DtNodeDef) + : null; + return dtOptionDef( + data, + existingDef, + data.name, + null, + parentAutocomplete, + parentGroup, + ); + } + + /** Transforms the provided data into a DtNodeDef which contains a DtGroupDef. */ + transformGroup( + data: DtQuickFilterDefaultDataSourceGroup, + parentAutocomplete: DtNodeDef | null = null, + existingDef: DtNodeDef | null = null, + ): DtNodeDef { + const def = dtGroupDef( + data, + existingDef, + data.name, + [], + parentAutocomplete, + ); + def.group!.options = this.transformList(data.options, def); + return def; + } + + /** Transforms the provided data into a DtNodeDef which contains a DtFreeTextDef. */ + transformFreeText(data: DtQuickFilterDefaultDataSourceFreeText): DtNodeDef { + const def = dtFreeTextDef( + data, + null, + [], + data.validators, + isDefined(data.unique) ? data.unique! : false, + ); + def.freeText!.suggestions = this.transformList(data.suggestions, def); + return def; + } + + /** Transforms the provided data into a DtNodeDef which contains a DtRangeDef. */ + transformRange(data: DtQuickFilterDefaultDataSourceRange): DtNodeDef { + return dtRangeDef( + data, + null, + !!data.range.operators.range, + !!data.range.operators.equal, + !!data.range.operators.greaterThanEqual, + !!data.range.operators.lessThanEqual, + data.range.unit, + isDefined(data.unique) ? data.unique! : false, + ); + } + + /** Transforms the provided data into a DtNodeDef. */ + transformObject( + data: DtQuickFilterDefaultDataSourceType | null, + parent: DtNodeDef | null = null, + ): DtNodeDef | null { + let def: DtNodeDef | null = null; + if (this.isAutocomplete(data)) { + def = this.transformAutocomplete(data); + } else if (this.isFreeText(data)) { + def = this.transformFreeText(data); + } else if (this.isRange(data)) { + def = this.transformRange(data); + } + + if (this.isGroup(data)) { + def = this.transformGroup(data); + } else if (this.isOption(data)) { + def = this.transformOption(data, parent, def); + } + return def; + } + + /** Transforms the provided list of data objects into an array of DtNodeDefs. */ + transformList( + list: Array, + parent: DtNodeDef | null = null, + ): DtNodeDef[] { + return list + .map(item => this.transformObject(item, parent)) + .filter(item => item !== null) as DtNodeDef[]; + } +} 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 new file mode 100644 index 0000000000..9dfd8433ad --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-group.html @@ -0,0 +1,34 @@ +

{{ _getViewValue(_nodeDef) }}

+ + + + + Any + + + {{_getViewValue(item)}} + + + + + +
+ + {{_getViewValue(item)}} + +
+
+ + diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter-group.scss b/libs/barista-components/experimental/quick-filter/src/quick-filter-group.scss new file mode 100644 index 0000000000..b7e9db03b3 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-group.scss @@ -0,0 +1,22 @@ +@import '../../../core/src/style/variables'; + +:host { + display: block; + padding: 8px 0; +} + +.dt-quick-filter-group-headline { + margin: 0 0 8px; + font-size: 18px; + font-weight: normal; + color: $gray-700; +} + +.dt-quick-filter-group-items { + display: flex; + flex-flow: column; + + > * { + margin-bottom: 8px; + } +} 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 new file mode 100644 index 0000000000..aec0160295 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-group.ts @@ -0,0 +1,140 @@ +/** + * @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, + ChangeDetectionStrategy, + Input, + Output, + EventEmitter, + ChangeDetectorRef, + ViewEncapsulation, +} from '@angular/core'; +import { + DtNodeDef, + isDtOptionDef, + isDtRenderType, +} from '@dynatrace/barista-components/filter-field'; +import { DtRadioChange } from '@dynatrace/barista-components/radio'; +import { DtCheckboxChange } from '../../../checkbox'; +import { + addFilter, + removeFilter, + updateFilter, + Action, + unsetFilterGroup, +} from './state/actions'; +import { buildIdPathsFromFilters } from './quick-filter-utils'; + +/** @internal */ +@Component({ + selector: 'dt-quick-filter-group', + templateUrl: './quick-filter-group.html', + styleUrls: ['./quick-filter-group.scss'], + encapsulation: ViewEncapsulation.Emulated, + changeDetection: ChangeDetectionStrategy.OnPush, + preserveWhitespaces: false, + host: { + class: 'dt-quick-filter-group', + }, +}) +export class DtQuickFilterGroup { + /** @internal The nodeDef of the autocomplete that should be rendered */ + @Input('nodeDef') _nodeDef: DtNodeDef; + + /** @internal The list of all active filters */ + @Input() + set activeFilters(filters: any[][]) { + this._activeFilterPaths = buildIdPathsFromFilters(filters || []); + this._changeDetectorRef.markForCheck(); + } + + /** @internal Emits a new action that changes the filter */ + @Output() readonly filterChange = new EventEmitter(); + + /** A list of active filter ids */ + private _activeFilterPaths: string[] = []; + + constructor(private _changeDetectorRef: ChangeDetectorRef) {} + + _unsetGroup(): void { + this.filterChange.emit(unsetFilterGroup(this._nodeDef)); + } + + /** @internal Updates a radio box */ + _selectOption(change: DtRadioChange): void { + if (change.value) { + this.filterChange.emit(updateFilter(change.value)); + } + } + + /** @internal Select or de select a checkbox */ + _selectCheckBox(change: DtCheckboxChange): void { + const action = change.checked + ? addFilter(change.source.value) + : removeFilter(change.source.value); + this.filterChange.emit(action); + } + + /** @internal Helper function that checks if nothing is selected inside a group */ + _isNothingSelected(): boolean { + if (this._nodeDef.option && this._nodeDef.option.uid) { + const index = this._activeFilterPaths.findIndex(path => + path.startsWith(this._nodeDef.option!.uid!), + ); + return index === -1; + } + return false; + } + + /** @internal Helper function that checks if an options is active */ + _isActive(node: DtNodeDef): boolean { + return !!( + node.option && this._activeFilterPaths.includes(node.option.uid || '') + ); + } + + /** + * @internal + * Helper function that checks if the nodeDef of the autocomplete is distinct. + * Needed to differentiate between radios or checkboxes. + */ + _isDistinct(): boolean { + return !!( + this._nodeDef.autocomplete && this._nodeDef.autocomplete.distinct + ); + } + + /** @internal Helper function that returns safely the viewValue of a nodeDef */ + _getViewValue(nodeDef: DtNodeDef): string { + return nodeDef && nodeDef.option ? nodeDef.option.viewValue : ''; + } + + /** + * @internal + * Helper function that returns all options that can be displayed. + * They can only be displayed if it is an option of an autocomplete that + * is not a render type so only have a text. + */ + _getOptions(): DtNodeDef[] { + if (this._nodeDef && this._nodeDef.autocomplete) { + return this._nodeDef.autocomplete.optionsOrGroups.filter( + def => isDtOptionDef(def) && !isDtRenderType(def), + ); + } + return []; + } +} 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 new file mode 100644 index 0000000000..35a931723e --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-utils.ts @@ -0,0 +1,26 @@ +/** + * @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 { DELIMITER } from '../../../filter-field/src/filter-field-util'; + +export function buildIdPathsFromFilters(filters: any[][]): string[] { + return filters.map(path => + path.reduce( + (previousValue, currentValue) => + `${previousValue.name}${DELIMITER}${currentValue.name}${DELIMITER}`, + ), + ); +} diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter.html b/libs/barista-components/experimental/quick-filter/src/quick-filter.html new file mode 100644 index 0000000000..0d9fb95939 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter.html @@ -0,0 +1,45 @@ + + + + + + +

+ +

+

+ +

+ + +
+ +
+ + +
+
diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter.module.ts b/libs/barista-components/experimental/quick-filter/src/quick-filter.module.ts new file mode 100644 index 0000000000..baaa361c34 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter.module.ts @@ -0,0 +1,43 @@ +/** + * @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 { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { DtCheckboxModule } from '@dynatrace/barista-components/checkbox'; +import { DtDrawerModule } from '@dynatrace/barista-components/drawer'; +import { DtFilterFieldModule } from '@dynatrace/barista-components/filter-field'; +import { DtRadioModule } from '@dynatrace/barista-components/radio'; +import { + DtQuckFilterSubTitle, + DtQuckFilterTitle, + DtQuickFilter, +} from './quick-filter'; +import { DtQuickFilterGroup } from './quick-filter-group'; + +const COMPONENTS = [DtQuickFilter, DtQuckFilterSubTitle, DtQuckFilterTitle]; + +@NgModule({ + imports: [ + CommonModule, + DtDrawerModule, + DtFilterFieldModule, + DtCheckboxModule, + DtRadioModule, + ], + exports: COMPONENTS, + declarations: [...COMPONENTS, DtQuickFilterGroup], +}) +export class DtQuickFilterModule {} diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter.scss b/libs/barista-components/experimental/quick-filter/src/quick-filter.scss new file mode 100644 index 0000000000..6ef02c51bd --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter.scss @@ -0,0 +1,67 @@ +@import '../../../core/src/style/variables'; +@import '../../../core/src/style/interactive-common'; + +:host { + display: block; + background-color: $gray-130; + padding: 20px; +} + +.dt-quick-filter-drawer { + margin-top: 12px; +} + +.dt-quick-filter-content { + position: relative; + display: block; + padding: 12px 12px 12px 26px; + height: 100%; + background-color: white; + border-radius: 3px; +} + +.dt-drawer { + background-color: $gray-130; + position: relative; + padding-right: 26px; +} + +.dt-quick-filter-title { + font-size: 20px; + color: $gray-700; + margin: 0; +} + +.dt-quick-filter-sub-title { + font-size: 12px; + color: $gray-500; + margin: 0; +} + +.dt-quick-filter-group + .dt-quick-filter-group { + border-top: 1px solid $gray-200; +} + +.dt-quick-filter-open, +.dt-quick-filter-close { + top: 8px; + background: white; + border-radius: 3px; + position: absolute; + color: $turquoise-600; + font-size: 1.2em; + cursor: pointer; +} + +.dt-quick-filter-open { + left: -4px; + padding: 6px 6px 6px 0; + border-left: none; + background-color: $gray-130; +} + +.dt-quick-filter-close { + right: -2px; + padding: 6px 0 6px 6px; + border-right: none; +} 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 new file mode 100644 index 0000000000..443ef3ba43 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter.spec.ts @@ -0,0 +1,166 @@ +/** + * @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. + */ + +// tslint:disable no-lifecycle-call no-use-before-declare no-magic-numbers +// tslint:disable no-any max-file-line-count no-unbound-method use-component-selector + +import { Component, DebugElement } from '@angular/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { + DtQuickFilter, + DtQuickFilterModule, +} from '@dynatrace/barista-components/experimental/quick-filter'; + +import { createComponent } from '@dynatrace/barista-components/testing'; +import { mockIconTestingModule } from '@dynatrace/barista-components/testing/mock'; +import { + DtFilterFieldDefaultDataSource, + DtFilterField, +} from '@dynatrace/barista-components/filter-field'; + +describe('dt-quick-filter', () => { + let instanceDebugElement: DebugElement; + let quickFilterInstance: DtQuickFilter; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + DtQuickFilterModule, + NoopAnimationsModule, + mockIconTestingModule(), + ], + declarations: [QuickFilterSimpleComponent, QuickFilterDefaultComponent], + }); + TestBed.compileComponents(); + }); + + describe('Simple QuickFilter without dataSource', () => { + let fixture: ComponentFixture; + beforeEach(() => { + fixture = createComponent(QuickFilterSimpleComponent); + instanceDebugElement = fixture.debugElement.query( + By.directive(DtQuickFilter), + ); + quickFilterInstance = instanceDebugElement.injector.get( + DtQuickFilter, + ); + }); + + it('should have an empty filters array if no dataSource is set', () => { + expect(quickFilterInstance.filters).toHaveLength(0); + }); + }); + + describe('Normal QuickFilter with mixed dataSource', () => { + let fixture: ComponentFixture; + let filterFieldDebugElement: DebugElement; + // let filterFieldInstance: DtFilterField; + beforeEach(() => { + fixture = createComponent(QuickFilterDefaultComponent); + instanceDebugElement = fixture.debugElement.query( + By.directive(DtQuickFilter), + ); + quickFilterInstance = instanceDebugElement.injector.get( + DtQuickFilter, + ); + + filterFieldDebugElement = fixture.debugElement.query( + By.directive(DtFilterField), + ); + // filterFieldInstance = filterFieldDebugElement.injector.get( + // DtFilterField, + // ); + }); + + it('should have an empty filters array if no dataSource is set', () => { + expect(quickFilterInstance.filters).toHaveLength(0); + quickFilterInstance.filters = [ + [DATA.autocomplete[0], DATA.autocomplete[0].autocomplete![0]], + ]; + + fixture.detectChanges(); + + // expect(filterFieldDebugElement).toMatchSnapshot(); + }); + }); +}); + +@Component({ + selector: 'dt-quick-filter-simple', + template: ` + + `, +}) +class QuickFilterSimpleComponent {} + +@Component({ + selector: 'dt-quick-filter-simple', + template: ` + + Quick-filter + + All options in the filter field above + + + my content + + `, +}) +class QuickFilterDefaultComponent { + _dataSource = new DtFilterFieldDefaultDataSource(DATA); +} + +const DATA = { + autocomplete: [ + { + name: 'AUT', + distinct: true, + autocomplete: [{ name: 'Linz' }, { name: 'Vienna' }, { name: 'Graz' }], + }, + { + name: 'USA', + autocomplete: [ + { name: 'San Francisco' }, + { name: 'Los Angeles' }, + { name: 'New York' }, + { name: 'Custom', suggestions: [] }, + ], + }, + { + name: 'Requests per minute', + range: { + operators: { + range: true, + equal: true, + greaterThanEqual: true, + lessThanEqual: true, + }, + unit: 's', + }, + }, + { + name: 'Not in Quickfilter', + autocomplete: [ + { name: 'Option1' }, + { name: 'Option2' }, + { name: 'Option3' }, + ], + }, + ], +}; diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter.ts b/libs/barista-components/experimental/quick-filter/src/quick-filter.ts new file mode 100644 index 0000000000..d6af66cc0c --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter.ts @@ -0,0 +1,131 @@ +/** + * @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 { + AfterViewInit, + ChangeDetectionStrategy, + Component, + Directive, + EventEmitter, + Input, + OnDestroy, + Output, + ViewChild, + ViewEncapsulation, +} from '@angular/core'; +import { + DtFilterField, + DtFilterFieldChangeEvent, +} from '@dynatrace/barista-components/filter-field'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { DtQuickFilterDataSource } from './quick-filter-data-source'; +import { Action, setFilters, switchDataSource } from './state/actions'; +import { quickFilterReducer } from './state/reducer'; +import { getAutocompletes, getDataSource, getFilters } from './state/selectors'; +import { createQuickFilterStore } from './state/store'; + +@Directive({ + selector: 'dt-quick-filter-title', + exportAs: 'dtQuickFilterTitle', + host: { + class: 'dt-quick-filter-title', + }, +}) +export class DtQuckFilterTitle {} + +@Directive({ + selector: 'dt-quick-filter-sub-title', + exportAs: 'dtQuickFilterSubTitle', + host: { + class: 'dt-quick-filter-sub-title', + }, +}) +export class DtQuckFilterSubTitle {} + +@Component({ + selector: 'dt-quick-filter', + exportAs: 'dtQuickFilter', + templateUrl: 'quick-filter.html', + styleUrls: ['quick-filter.scss'], + host: { + class: 'dt-quick-filter', + }, + preserveWhitespaces: false, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.Emulated, +}) +export class DtQuickFilter implements AfterViewInit, OnDestroy { + /** Emits when a new filter has been added or removed. */ + @Output() readonly filterChanges = new EventEmitter< + DtFilterFieldChangeEvent + >(); + + /** instance of the filter field that will be orchestrated by the quick filter */ + @ViewChild(DtFilterField, { static: true }) + private _filterField: DtFilterField; + + /** The data source instance that should be connected to the filter field. */ + @Input() + set dataSource(dataSource: DtQuickFilterDataSource) { + this._store.dispatch(switchDataSource(dataSource)); + } + + /** The currently applied filters */ + @Input() + get filters(): T[][] { + return this._filterField.filters; + } + set filters(filters: T[][]) { + this._store.dispatch(setFilters(filters)); + } + + /** The store where the data flow is managed */ + private _store = createQuickFilterStore(quickFilterReducer); + + /** @internal the autocomplete fields that should be rendered by the quick filter */ + readonly _autocompleteData$ = this._store.select(getAutocompletes); + /** @internal the dataSource that gets passed to the filter field */ + readonly _filterFieldDataSource$ = this._store.select(getDataSource); + /** @internal the list of all current active filters */ + readonly _activeFilters$ = this._store.select(getFilters); + + private _destroy$ = new Subject(); + + ngAfterViewInit(): void { + // When the filters changes apply them to the filter field + this._activeFilters$.pipe(takeUntil(this._destroy$)).subscribe(filters => { + if (this._filterField.filters !== filters) { + this._filterField.filters = filters; + } + }); + } + + ngOnDestroy(): void { + this._destroy$.next(); + this._destroy$.complete(); + } + + _changeFilter(action: Action): void { + this._store.dispatch(action); + } + + /** @internal Bubble the filter field change event through */ + _filterFiledChanged(change: DtFilterFieldChangeEvent): void { + this._store.dispatch(setFilters(change.filters)); + this.filterChanges.emit(change); + } +} diff --git a/libs/barista-components/experimental/quick-filter/src/state/actions.ts b/libs/barista-components/experimental/quick-filter/src/state/actions.ts new file mode 100644 index 0000000000..eaaef35e37 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/state/actions.ts @@ -0,0 +1,62 @@ +/** + * @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 { + DtNodeDef, + DtFilterFieldDataSource, +} from '@dynatrace/barista-components/filter-field'; + +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', + UNSET_FILTER_GROUP = '@@actions unset filter group', + SWITCH_DATA_SOURCE = '@@actions switch dataSource', + UPDATE_DATA_SOURCE = '@@actions update dataSource', +} + +/** Interface for an action */ +export interface Action { + readonly type: ActionType; + payload?: T; +} +/** Function which helps to create actions without mistakes */ +export const action = (type: ActionType, payload?: T): Action => ({ + type, + payload, +}); + +export const setFilters = (filters: any[][]) => + action(ActionType.SET_FILTERS, filters); + +export const unsetFilterGroup = (group: DtNodeDef) => + action(ActionType.UNSET_FILTER_GROUP, group); + +export const addFilter = (item: DtNodeDef) => + action(ActionType.ADD_FILTER, item); + +export const removeFilter = (item: DtNodeDef) => + action(ActionType.REMOVE_FILTER, item); + +export const updateFilter = (item: DtNodeDef) => + action(ActionType.UPDATE_FILTER, item); + +export const switchDataSource = (item: DtFilterFieldDataSource) => + action(ActionType.SWITCH_DATA_SOURCE, item); + +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 new file mode 100644 index 0000000000..b4a72725d1 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/state/effects.ts @@ -0,0 +1,43 @@ +/** + * @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 { MonoTypeOperatorFunction, Observable } from 'rxjs'; +import { filter, switchMap, map } from 'rxjs/operators'; +import { DtFilterFieldDataSource, DtNodeDef } from '../../../../filter-field'; +import { Action, ActionType, updateDataSource } from './actions'; +import { QuickFilterState } from './store'; + +/** Type for an effect */ +export type Effect = ( + action$: Observable, + state$?: Observable, +) => Observable; + +/** Operator to filter actions */ +export const ofType = ( + ...types: ActionType[] +): MonoTypeOperatorFunction> => + filter((action: Action) => types.indexOf(action.type) > -1); + +/** Connects to a new Data dataSource */ +export const switchDataSourceEffect: Effect = (action$: Observable) => + action$.pipe( + ofType(ActionType.SWITCH_DATA_SOURCE), + switchMap(action => { + return action.payload!.connect(); + }), + 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 new file mode 100644 index 0000000000..4e0c264fb9 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/state/reducer.spec.ts @@ -0,0 +1,54 @@ +/** + * @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. + */ + +describe('QuickFilter Reducer', () => { + test('', () => {}); +}); + +// import { dtAutocompleteDef } from '@dynatrace/barista-components/filter-field'; + +// const createDistinctNode = dtAutocompleteDef( +// data, +// null, +// [], +// !!data.distinct, +// !!data.async, +// ); + +// const createOption = dtOptionDef( +// data, +// existingDef, +// data.name, +// null, +// parentAutocomplete, +// parentGroup, +// ); + +// /** Transforms the provided data into a DtNodeDef which contains a DtOptionDef. */ +// transformOption( +// data: DtFilterFieldDefaultDataSourceOption, +// parentAutocompleteOrOption: DtNodeDef | null = null, +// existingDef: DtNodeDef | null = null, +// ): DtNodeDef { +// const parentGroup = isDtGroupDef(parentAutocompleteOrOption) +// ? parentAutocompleteOrOption +// : null; +// const parentAutocomplete = +// parentGroup !== null +// ? parentGroup.group.parentAutocomplete +// : isDtAutocompleteDef(parentAutocompleteOrOption) +// ? (parentAutocompleteOrOption as DtNodeDef) +// : null; diff --git a/libs/barista-components/experimental/quick-filter/src/state/reducer.ts b/libs/barista-components/experimental/quick-filter/src/state/reducer.ts new file mode 100644 index 0000000000..2c8db19e53 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/state/reducer.ts @@ -0,0 +1,154 @@ +/** + * @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 { + DtNodeDef, + DELIMITER, +} from '@dynatrace/barista-components/filter-field'; +import { Action, ActionType } from './actions'; +import { QuickFilterState, initialState } from './store'; + +/** @internal Type of a reducer */ +export type Reducer = ( + state: QuickFilterState, + action: Action, +) => QuickFilterState; + +/** + * @internal + * The Quick Filter reducer is the place where we handle all the state updates + * To have a single entry point. Every action can trigger an update of the state. + * It has to be a immutable function that always returns a new object of the state. + * @param state The state that should be modified. + * @param action The current action that should be handled. + */ +export function quickFilterReducer( + state: QuickFilterState, + action: Action, +): QuickFilterState { + console.info( + `%c Reducer <${action.type}> `, + ' background-color: lightblue; color: black; font-size: 1.2em; padding: 5px; border-radius: 1em', + action, + ); + + switch (action.type) { + case ActionType.SWITCH_DATA_SOURCE: + if (state.dataSource) { + state.dataSource.disconnect(); + } + return { ...initialState, dataSource: action.payload }; + case ActionType.UPDATE_DATA_SOURCE: + return { ...state, nodeDef: action.payload }; + case ActionType.SET_FILTERS: + return { ...state, filters: action.payload }; + case ActionType.UNSET_FILTER_GROUP: + return { + ...state, + filters: unsetFilterGroup(state.filters, action.payload), + }; + case ActionType.ADD_FILTER: + return { ...state, filters: addFilter(state.filters, action.payload) }; + case ActionType.UPDATE_FILTER: + return { ...state, filters: updateFilter(state.filters, action.payload) }; + case ActionType.REMOVE_FILTER: + return { ...state, filters: removeFilter(state.filters, action.payload) }; + default: + // Default return the same state as it was passed so don't modify anything + return state; + } +} + +// # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +// # +// # HELPER functions for modifying the filters +// # +// # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +/** @internal Add a filter to the filters array */ +export function addFilter(filters: any[][], item: DtNodeDef): any[][] { + return [...filters, buildData(item)]; +} + +/** @internal Remove a filter from the filters array */ +export function removeFilter(filters: any[][], item: DtNodeDef): any[][] { + const index = findSelectedOption(filters, item, false); + const updatedState = [...filters]; + + if (index > -1) { + delete updatedState[index]; + } + + return updatedState.filter(Boolean); +} + +/** @internal Update a filter inside the filters array */ +export function updateFilter(filters: any[][], item: DtNodeDef): any[][] { + const index = findSelectedOption(filters, item, true); + + if (index < 0) { + return addFilter(filters, item); + } + + filters[index] = buildData(item); + + 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 Add a filter to the filters array */ +export function buildData(item: DtNodeDef): any[] { + const data = [item.data]; + + if (item.option && item.option.parentAutocomplete) { + data.unshift(item.option.parentAutocomplete.data); + } + return data; +} + +/** @internal Find a filter inside the filters array based on a NodeDef */ +export function findSelectedOption( + filters: any[][], + item: DtNodeDef, + distinct: boolean = false, +): number { + return filters.findIndex(path => { + if (item.option && item.option.uid) { + const parts = item.option.uid.split(DELIMITER); + + if (distinct && parts[0] === path[0].name) { + return true; + } + + const dataPath = path.reduce( + (previousValue, currentValue) => + `${previousValue.name}${DELIMITER}${currentValue.name}${DELIMITER}`, + ); + + if (item.option.uid === dataPath) { + 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 new file mode 100644 index 0000000000..66fa5569ef --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/state/selectors.ts @@ -0,0 +1,49 @@ +/** + * @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 { + applyDtOptionIds, + DtNodeDef, + isDtAutocompleteDef, +} from '@dynatrace/barista-components/filter-field'; +import { Observable } from 'rxjs'; +import { filter, map, pluck, tap } from 'rxjs/operators'; +import { QuickFilterState } from './store'; + +/** @internal Select all autocompletes from the root Node Def out of the store */ +export const getAutocompletes = ( + state$: Observable, +): Observable => + state$.pipe( + tap(state => { + // apply the ids to the node to identify them later on + if (state.nodeDef) { + applyDtOptionIds(state.nodeDef); + } + }), + pluck('nodeDef'), + filter(isDtAutocompleteDef), + map(({ autocomplete }) => + autocomplete.optionsOrGroups.filter(isDtAutocompleteDef), + ), + ); + +/** @internal Select the data Source from the store */ +export const getDataSource = (state$: Observable) => + state$.pipe(pluck('dataSource')); + +/** @internal Select the actual applied filters */ +export const getFilters = (state$: Observable) => + state$.pipe(pluck('filters')); diff --git a/libs/barista-components/experimental/quick-filter/src/state/store.ts b/libs/barista-components/experimental/quick-filter/src/state/store.ts new file mode 100644 index 0000000000..b8b614ac06 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/state/store.ts @@ -0,0 +1,108 @@ +/** + * @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 { Observable, merge, BehaviorSubject } from 'rxjs'; +import { shareReplay, map, withLatestFrom } from 'rxjs/operators'; +import { Reducer } from './reducer'; +import { Action, ActionType } from './actions'; +import { Effect, switchDataSourceEffect } from './effects'; +import { + DtNodeDef, + DtFilterFieldDataSource, +} from '@dynatrace/barista-components/filter-field'; + +/** @internal Interface that describes the QuickFilter state */ +export interface QuickFilterState { + /** The root NodeDef of the dataSource */ + nodeDef?: DtNodeDef; + /** The dataSource that is connected with the QuickFilter */ + dataSource?: DtFilterFieldDataSource; + /** Array of all active filters */ + filters: any[][]; +} + +/** @internal The initial QuickFilter state */ +export const initialState: QuickFilterState = { + filters: [], +}; + +/** Array of side effects */ +const effects: Effect[] = [switchDataSourceEffect]; + +/** + * The Quick Filter Store is one place where the state is handled. + * It is a minimal implementation of a redux like architecture to handle + * state in an immutable and on-directional way. + * + * This makes testing and debugging way easier, because there is always a + * clear state that can only be modified through actions. + * + * 1. Action gets dispatched (An action indicates a change in the store) + * 2. The reducer gets an action and the current state and according to the action + * modifies the state. + * 3. A Selector can always read the latest value from the store and displays it in + * a template. So the only way to modify the state is dispatching an action. + * 4. If some async work has to be done the effect is responsible for that. + * Effects are listening for actions then doing some async work and dispatching some + * Other actions with the payload of the async stuff. + */ +class QuickFilterStore { + /** The current action that got dispatched */ + private readonly action$ = new BehaviorSubject({ + type: ActionType.INIT, + }); + + /** The current state that is present */ + private readonly state$: BehaviorSubject; + + constructor(reducer: Reducer, initialStoreState: QuickFilterState) { + this.state$ = new BehaviorSubject(initialStoreState); + + this.action$ + .pipe( + shareReplay(), + withLatestFrom(this.state$), + map(([action, state]) => reducer(state, action)), + ) + .subscribe(state => { + // Here the state gets modified through the outcome of the reducer + this.state$.next(state); + }); + + // Each effect will get the stream of actions and will dispatch other actions in return + // The emitted actions will be immediately dispatched through the normal store.dispatch() + merge(...effects.map(epic => epic(this.action$, this.state$))).subscribe( + (action: Action) => { + this.dispatch(action); + }, + ); + } + + /** Dispatch a new Action that modifies the store */ + dispatch(action: Action): void { + this.action$.next(action); + } + + /** Use a provided selector function to get a State out of the store */ + select(selector: (state$: Observable) => T): T { + return selector(this.state$); + } +} + +/** @internal This function creates the store for the quick filter */ +export function createQuickFilterStore(reducer: Reducer): QuickFilterStore { + return new QuickFilterStore(reducer, initialState); +} diff --git a/libs/barista-components/experimental/quick-filter/src/test-setup.ts b/libs/barista-components/experimental/quick-filter/src/test-setup.ts new file mode 100644 index 0000000000..3c66e43d72 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/test-setup.ts @@ -0,0 +1,17 @@ +/** + * @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 'jest-preset-angular'; diff --git a/libs/barista-components/experimental/quick-filter/tsconfig.json b/libs/barista-components/experimental/quick-filter/tsconfig.json new file mode 100644 index 0000000000..08c7db8c96 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "types": ["node", "jest"] + }, + "include": ["**/*.ts"] +} diff --git a/libs/barista-components/experimental/quick-filter/tsconfig.lib.json b/libs/barista-components/experimental/quick-filter/tsconfig.lib.json new file mode 100644 index 0000000000..1c600457d3 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/tsconfig.lib.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "target": "es2015", + "declaration": true, + "inlineSources": true, + "types": [], + "lib": ["dom", "es2018"] + }, + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true, + "enableResourceInlining": true + }, + "exclude": ["src/test-setup.ts", "**/*.spec.ts"] +} diff --git a/libs/barista-components/experimental/quick-filter/tsconfig.spec.json b/libs/barista-components/experimental/quick-filter/tsconfig.spec.json new file mode 100644 index 0000000000..fd405a65ef --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/barista-components/experimental/quick-filter/tslint.json b/libs/barista-components/experimental/quick-filter/tslint.json new file mode 100644 index 0000000000..7a84ad6df5 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/tslint.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tslint.json", + "rules": {} +} diff --git a/libs/barista-components/filter-field/index.ts b/libs/barista-components/filter-field/index.ts index 4f9b0f832f..30986293e2 100644 --- a/libs/barista-components/filter-field/index.ts +++ b/libs/barista-components/filter-field/index.ts @@ -22,7 +22,7 @@ export * from './src/filter-field-range/filter-field-range-trigger'; export * from './src/filter-field-data-source'; export * from './src/filter-field-default-data-source'; export * from './src/filter-field-errors'; - +export { applyDtOptionIds, DELIMITER } from './src/filter-field-util'; export { DtNodeFlags, DtNodeDef, @@ -43,4 +43,5 @@ export { isDtOptionDef, dtGroupDef, isDtGroupDef, + isDtRenderType, } from './src/types'; diff --git a/libs/barista-components/filter-field/src/filter-field.ts b/libs/barista-components/filter-field/src/filter-field.ts index 92b05d169a..d80a042694 100644 --- a/libs/barista-components/filter-field/src/filter-field.ts +++ b/libs/barista-components/filter-field/src/filter-field.ts @@ -177,7 +177,7 @@ export const DT_FILTER_FIELD_TYPING_DEBOUNCE = 200; ]), ], }) -export class DtFilterField +export class DtFilterField implements CanDisable, AfterViewInit, OnDestroy, OnChanges { /** Label for the filter field (e.g. "Filter by"). Will be placed next to the filter icon. */ @Input() label = ''; @@ -506,7 +506,10 @@ export class DtFilterField }); // tslint:disable-next-line: deprecation this.tags.changes - .pipe(startWith(null), takeUntil(this._destroy$)) + .pipe( + startWith(null), + takeUntil(this._destroy$), + ) .subscribe(() => { // tslint:disable-next-line: deprecation this._currentTags.next(this.tags.toArray()); diff --git a/libs/testing/src/mock/mock-icon-testing-module.ts b/libs/testing/src/mock/mock-icon-testing-module.ts new file mode 100644 index 0000000000..ef66ada67d --- /dev/null +++ b/libs/testing/src/mock/mock-icon-testing-module.ts @@ -0,0 +1,54 @@ +/** + * @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 { NgModule, Type } from '@angular/core'; +import { TestModuleMetadata } from '@angular/core/testing'; +import { DtIconModule } from '@dynatrace/barista-components/icon'; +import { HttpClient } from '@angular/common/http'; +import { of } from 'rxjs'; + +/** + * Creates a mock icon testing module without trying to fetch any svg. + * Returns an empty svg tag for unit testing. + */ +export function mockIconTestingModule( + config: TestModuleMetadata = { + imports: [], + providers: [], + declarations: [], + }, +): Type { + @NgModule({ + imports: [ + DtIconModule.forRoot({ svgIconLocation: '{{name}}.svg' }), + ...config.imports!, + ], + providers: [ + { + provide: HttpClient, + useValue: { + get: jest.fn().mockReturnValue(of('')), + }, + }, + ...config.providers!, + ], + declarations: config.declarations, + exports: [DtIconModule], + }) + class IconTestingModule {} + + return IconTestingModule; +} diff --git a/nx.json b/nx.json index f26dd02371..02f1d1aeac 100644 --- a/nx.json +++ b/nx.json @@ -242,6 +242,9 @@ "progress-circle": { "tags": ["scope:components", "type:library"] }, + "quick-filter": { + "tags": ["scope:components", "type:library"] + }, "radial-chart": { "tags": ["scope:components", "type:library"] }, diff --git a/tsconfig.json b/tsconfig.json index 0bff269c2a..6d2a3596ad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -86,6 +86,9 @@ "@dynatrace/barista-components/expandable-text": [ "libs/barista-components/expandable-text/index.ts" ], + "@dynatrace/barista-components/experimental/quick-filter": [ + "components/experimental/quick-filter/index.ts" + ], "@dynatrace/barista-components/filter-field": [ "libs/barista-components/filter-field/index.ts" ],