From 43f9d7a04896b6ebf1b3f55e973228803593415a Mon Sep 17 00:00:00 2001 From: "rowa.audil" Date: Thu, 14 Jan 2021 18:10:30 +0000 Subject: [PATCH] feat(filter-field): Adds a default search option for increased filtering speed. NOTE: The debounce for the inputChange event was removed. This leads to the inputChange event being fired more often. Please cancel requests that are inflight or debounce the event yourself. --- .../components/consumption/consumption.html | 12 +- .../src/components/drawer/drawer.html | 12 +- .../src/components/event-chart/event-chart.ts | 5 +- .../components/filter-field/filter-field.ts | 5 +- .../quick-filter-async/quick-filter-async.ts | 11 +- .../quick-filter/quick-filter.e2e.ts | 1 - .../quick-filter/quick-filter/quick-filter.ts | 7 +- .../components/radial-chart/radial-chart.html | 4 +- .../src/components/select/select.html | 4 +- .../sunburst-chart/sunburst-chart.html | 2 +- .../src/components/tree-table/tree-table.html | 4 +- apps/demos/src/app-routing.module.ts | 7 +- apps/demos/src/barista-examples.a11y.ts | 1 + apps/demos/src/nav-items.ts | 4 + apps/dev/src/filter-field/testdata.ts | 11 ++ .../barista-components/filter-field/README.md | 14 ++ .../src/filter-field-default-data-source.ts | 2 + .../filter-field/src/filter-field-util.ts | 31 +++++ .../filter-field/src/filter-field.html | 23 +++ .../filter-field/src/filter-field.spec.ts | 15 +- .../filter-field/src/filter-field.ts | 105 +++++++++++--- .../src/testing/filter-field-test-helpers.ts | 10 +- .../filter-field/src/types.ts | 24 +++- libs/examples/src/filter-field/BUILD.bazel | 1 + .../filter-field-default-search-example.html | 5 + .../filter-field-default-search-example.ts | 131 ++++++++++++++++++ .../filter-field-examples.module.ts | 2 + .../filter-field-validator-example.ts | 6 +- libs/examples/src/filter-field/index.ts | 1 + libs/examples/src/index.ts | 13 +- 30 files changed, 382 insertions(+), 91 deletions(-) create mode 100644 libs/examples/src/filter-field/filter-field-default-search-example/filter-field-default-search-example.html create mode 100644 libs/examples/src/filter-field/filter-field-default-search-example/filter-field-default-search-example.ts diff --git a/apps/components-e2e/src/components/consumption/consumption.html b/apps/components-e2e/src/components/consumption/consumption.html index d279a8c5b6..c910ff06fb 100644 --- a/apps/components-e2e/src/components/consumption/consumption.html +++ b/apps/components-e2e/src/components/consumption/consumption.html @@ -7,20 +7,14 @@ - - Host units - + Host units {{ value }}/{{ max }} - - Restricted host unit hours - + Restricted host unit hours -
- Dummy content. -
+
Dummy content.
diff --git a/apps/components-e2e/src/components/drawer/drawer.html b/apps/components-e2e/src/components/drawer/drawer.html index bf3b245879..497a950115 100644 --- a/apps/components-e2e/src/components/drawer/drawer.html +++ b/apps/components-e2e/src/components/drawer/drawer.html @@ -6,12 +6,12 @@ mode="side" (opened)="open()" (closed)="close()" - style="width: 80%; height: 3000px;" + style="width: 80%; height: 3000px" > -
+
  • {{ data.series.name }} @@ -54,18 +54,14 @@ >
    - - Hover me - + Hover me

    Overlay content

    {{ openCount }} {{ closeCount }} - +
    diff --git a/apps/components-e2e/src/components/event-chart/event-chart.ts b/apps/components-e2e/src/components/event-chart/event-chart.ts index 71fc8ea585..d91354e807 100644 --- a/apps/components-e2e/src/components/event-chart/event-chart.ts +++ b/apps/components-e2e/src/components/event-chart/event-chart.ts @@ -30,9 +30,8 @@ export class DtE2EEventChart { i = 0; - @ViewChild(DtEventChart, { static: true }) private _eventChart: DtEventChart< - T - >; + @ViewChild(DtEventChart, { static: true }) + private _eventChart: DtEventChart; constructor(private _changeDetectorRef: ChangeDetectorRef) { (window as any).e2eTestInstance = this; 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 a3e85d3d13..4ecce6ef74 100644 --- a/apps/components-e2e/src/components/filter-field/filter-field.ts +++ b/apps/components-e2e/src/components/filter-field/filter-field.ts @@ -46,9 +46,8 @@ export class DtE2EFilterField implements OnDestroy { _dataSource = new DtFilterFieldDefaultDataSource(DATA[0]); - @ViewChild(DtFilterField, { static: true }) _filterfield: DtFilterField< - DtFilterFieldDefaultDataSourceType - >; + @ViewChild(DtFilterField, { static: true }) + _filterfield: DtFilterField; switchToDatasource(targetIndex: number): void { this._dataSource = new DtFilterFieldDefaultDataSource(DATA[targetIndex]); 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 index 5543fbd070..31063ff31d 100644 --- 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 @@ -59,14 +59,13 @@ export class DtE2EQuickFilterAsync { isObject(node) && node.name && node.name !== 'AUT (async)', }; - _dataSource = new DtQuickFilterDefaultDataSource< - DtQuickFilterDefaultDataSourceType - >(filterFieldData, this._config); + _dataSource = new DtQuickFilterDefaultDataSource( + filterFieldData, + this._config, + ); currentFilterChanges( - event: DtQuickFilterCurrentFilterChangeEvent< - DtQuickFilterDefaultDataSourceType - >, + event: DtQuickFilterCurrentFilterChangeEvent, ): void { if (event.added[0] === filterFieldData.autocomplete[0]) { setTimeout(() => { 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 de76f5da11..639bce8657 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 @@ -329,7 +329,6 @@ test('should work with async data and handle distincts correctly', async (testCo await clickOption(2); // Click option Los Angeles await clickOption(2); - // Expect the filter to be set await testController .expect(getFilterfieldTags()) 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 61415f3dc0..ca4b37bb70 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 @@ -38,9 +38,10 @@ const config: DtQuickFilterDefaultDataSourceConfig = { templateUrl: 'quick-filter.html', }) export class DtE2EQuickFilter { - _dataSource: DtQuickFilterDefaultDataSource< - any - > = new DtQuickFilterDefaultDataSource(DATA[1], config); + _dataSource: DtQuickFilterDefaultDataSource = new DtQuickFilterDefaultDataSource( + DATA[1], + config, + ); filterChanges(filterEvent: DtQuickFilterChangeEvent): void { console.log(filterEvent); diff --git a/apps/components-e2e/src/components/radial-chart/radial-chart.html b/apps/components-e2e/src/components/radial-chart/radial-chart.html index cf72ceeb31..da8a929d2d 100644 --- a/apps/components-e2e/src/components/radial-chart/radial-chart.html +++ b/apps/components-e2e/src/components/radial-chart/radial-chart.html @@ -29,9 +29,7 @@ - + diff --git a/apps/components-e2e/src/components/select/select.html b/apps/components-e2e/src/components/select/select.html index e7de210773..e291c025f0 100644 --- a/apps/components-e2e/src/components/select/select.html +++ b/apps/components-e2e/src/components/select/select.html @@ -1,7 +1,5 @@ - - 90th percentile - + 90th percentile 95th percentile Sum Average diff --git a/apps/components-e2e/src/components/sunburst-chart/sunburst-chart.html b/apps/components-e2e/src/components/sunburst-chart/sunburst-chart.html index a32375c975..05f0805f91 100644 --- a/apps/components-e2e/src/components/sunburst-chart/sunburst-chart.html +++ b/apps/components-e2e/src/components/sunburst-chart/sunburst-chart.html @@ -4,7 +4,7 @@ [series]="series" (selectedChange)="selected = $event" [valueDisplayMode]="valueDisplayMode" - style="max-width: 600px;" + style="max-width: 600px" > {{ tooltip.label }}: {{ tooltip.value }} diff --git a/apps/components-e2e/src/components/tree-table/tree-table.html b/apps/components-e2e/src/components/tree-table/tree-table.html index 8d7d65f2a9..3c79ba7c0f 100644 --- a/apps/components-e2e/src/components/tree-table/tree-table.html +++ b/apps/components-e2e/src/components/tree-table/tree-table.html @@ -56,9 +56,7 @@ Actions - - buttons... - + buttons... +## Default search + +To optimize frequently used labels e.g. `Name`, set a default search option +(defaultSearch) in your dataSource like shown in the example below. The +defaultSearch option can then be used similar to a free-text but not quite. +Instead of selecting a label and then typing, just type the preferred tag e.g +`Errors` and the filter field will be combine the defaultSearch option name +(`name: 'Name'`) attribute and the typed text automatically resulting in +`Name: Errors`. + +Type anything while no label is selected to see the Default Search in action. + + + ## Filters Every filter is an array of: diff --git a/libs/barista-components/filter-field/src/filter-field-default-data-source.ts b/libs/barista-components/filter-field/src/filter-field-default-data-source.ts index 6e38fcae09..aee4745e11 100644 --- a/libs/barista-components/filter-field/src/filter-field-default-data-source.ts +++ b/libs/barista-components/filter-field/src/filter-field-default-data-source.ts @@ -86,6 +86,7 @@ export interface DtFilterFieldDefaultDataSourceFreeText { >; validators: DtFilterFieldValidator[]; unique?: boolean; + defaultSearch?: boolean; } export interface DtFilterFieldDefaultDataSourceRange { @@ -333,6 +334,7 @@ export class DtFilterFieldDefaultDataSource [], data.validators, isDefined(data.unique) ? data.unique! : false, + !!data.defaultSearch, ); def.freeText!.suggestions = this.transformList(data.suggestions, def); return def; diff --git a/libs/barista-components/filter-field/src/filter-field-util.ts b/libs/barista-components/filter-field/src/filter-field-util.ts index 1046e2c39e..07ad103eef 100644 --- a/libs/barista-components/filter-field/src/filter-field-util.ts +++ b/libs/barista-components/filter-field/src/filter-field-util.ts @@ -41,6 +41,8 @@ import { isDtRenderType, isPartialDtAutocompleteDef, isDtMultiSelectValue, + DtAutocompleteDef, + DtFreeTextDef, } from './types'; /** @@ -550,3 +552,32 @@ export function isDtRangeValueEqual(a: DtRangeValue, b: DtRangeValue): boolean { } return false; } + +export function findDefaultSearch( + def: DtNodeDef & { autocomplete: DtAutocompleteDef }, +): + | (DtNodeDef & { + freeText: DtFreeTextDef; + option: DtOptionDef; + }) + | null { + for (const optionOrGroup of def.autocomplete.optionsOrGroups) { + if ( + isDtOptionDef(optionOrGroup) && + isDtFreeTextDef(optionOrGroup) && + optionOrGroup.freeText.defaultSearch + ) { + return optionOrGroup; + } else if (isDtGroupDef(optionOrGroup)) { + for (const option of optionOrGroup.group.options) { + if (isDtFreeTextDef(option) && option.freeText.defaultSearch) { + return option as DtNodeDef & { + freeText: DtFreeTextDef; + option: DtOptionDef; + }; + } + } + } + } + return null; +} diff --git a/libs/barista-components/filter-field/src/filter-field.html b/libs/barista-components/filter-field/src/filter-field.html index 1a43a3ecf4..1fed7950a5 100644 --- a/libs/barista-components/filter-field/src/filter-field.html +++ b/libs/barista-components/filter-field/src/filter-field.html @@ -61,6 +61,29 @@ panelWidth="400px" class="dt-filter-field-panel" > + + + {{ _defaultSearchDef.option!.viewValue }}: {{ _inputValue }} + + + + {{ _defaultSearchDef.option!.viewValue }}: + {{ suggestionsDef.option!.viewValue }} + + diff --git a/libs/barista-components/filter-field/src/filter-field.spec.ts b/libs/barista-components/filter-field/src/filter-field.spec.ts index d15f7777b2..21aa50e788 100644 --- a/libs/barista-components/filter-field/src/filter-field.spec.ts +++ b/libs/barista-components/filter-field/src/filter-field.spec.ts @@ -30,7 +30,6 @@ import { By } from '@angular/platform-browser'; import { DtFilterField, DtFilterFieldChangeEvent, - DT_FILTER_FIELD_TYPING_DEBOUNCE, } from '@dynatrace/barista-components/filter-field'; import { createComponent, @@ -299,7 +298,7 @@ describe('DtFilterField', () => { expect(filterField._autocomplete.isOpen).toBe(true); }); - it('should emit the inputChange event when typing into the input field with autocomplete', fakeAsync(() => { + it('should emit the inputChange event when typing into the input field with autocomplete', () => { const spy = jest.fn(); const subscription = filterField.inputChange.subscribe(spy); @@ -313,7 +312,7 @@ describe('DtFilterField', () => { expect(spy).toHaveBeenCalledWith('xy'); subscription.unsubscribe(); - })); + }); it('should create the correct options and option groups', () => { filterField.focus(); @@ -524,7 +523,7 @@ describe('DtFilterField', () => { sub.unsubscribe(); })); - it('should switch to free text and on enter fire a filterChanges event and create a tag', fakeAsync(() => { + it('should switch to free text and on enter fire a filterChanges event and create a tag', () => { const spy = jest.fn(); const subscription = filterField.filterChanges.subscribe(spy); filterField.focus(); @@ -538,7 +537,6 @@ describe('DtFilterField', () => { const inputEl = getInput(fixture); dispatchKeyboardEvent(inputEl, 'keydown', ENTER); - tick(DT_FILTER_FIELD_TYPING_DEBOUNCE); fixture.detectChanges(); const tags = getFilterTags(fixture); @@ -548,9 +546,9 @@ describe('DtFilterField', () => { expect(tags[0].value).toBe('abc'); expect(spy).toHaveBeenCalledTimes(1); subscription.unsubscribe(); - })); + }); - it('should switch to free text with keyboard interaction and on enter fire a filterChanges event and create a tag', fakeAsync(() => { + it('should switch to free text with keyboard interaction and on enter fire a filterChanges event and create a tag', () => { const spy = jest.fn(); const subscription = filterField.filterChanges.subscribe(spy); filterField.focus(); @@ -571,7 +569,6 @@ describe('DtFilterField', () => { dispatchKeyboardEvent(inputEl, 'keydown', ENTER); - tick(DT_FILTER_FIELD_TYPING_DEBOUNCE); fixture.detectChanges(); const tags = getFilterTags(fixture); @@ -582,7 +579,7 @@ describe('DtFilterField', () => { expect(tags[0].value).toBe('abc'); expect(spy).toHaveBeenCalledTimes(1); subscription.unsubscribe(); - })); + }); it('should fire currentFilterChanges when an option is selected', fakeAsync(() => { const spy = jest.fn(); diff --git a/libs/barista-components/filter-field/src/filter-field.ts b/libs/barista-components/filter-field/src/filter-field.ts index a86c1fddb5..0af4457be3 100644 --- a/libs/barista-components/filter-field/src/filter-field.ts +++ b/libs/barista-components/filter-field/src/filter-field.ts @@ -75,7 +75,6 @@ import { Subscription, } from 'rxjs'; import { - debounceTime, delay, distinctUntilChanged, filter, @@ -116,6 +115,7 @@ import { filterAutocompleteDef, filterFreeTextDef, filterMultiSelectDef, + findDefaultSearch, findFilterValuesForSources, isDtAutocompleteValueEqual, isDtFreeTextValueEqual, @@ -124,13 +124,16 @@ import { } from './filter-field-util'; import { DtFilterFieldControl } from './filter-field-validation'; import { + DefaultSearchOption, DtAutocompleteValue, DtFilterFieldTagData, DtFilterValue, + DtFreeTextDef, DtNodeDef, DtOptionDef, isAsyncDtAutocompleteDef, isAsyncDtMultiSelectDef, + isDefaultSearchOption, isDtAutocompleteDef, isDtAutocompleteValue, isDtFreeTextDef, @@ -168,8 +171,6 @@ export class DtFilterFieldCurrentFilterChangeEvent { ) {} } -export const DT_FILTER_FIELD_TYPING_DEBOUNCE = 200; - // We need to save the instance of the filterField that currently // has a flap open, to make sure we can close the old one. let currentlyOpenFilterField: DtFilterField | null = null; @@ -220,7 +221,6 @@ export class DtFilterField if (value !== null) { this.tagValuesParser = value; } - this._updateTagData(); this._changeDetectorRef.markForCheck(); } @@ -440,6 +440,14 @@ export class DtFilterField /** Holds all tagdata including the data for a tag that might be edited */ tagData: DtFilterFieldTagData[] = []; + /** @internal Holds the default search node */ + _defaultSearchDef: + | (DtNodeDef & { + freeText: DtFreeTextDef; + option: DtOptionDef; + }) + | null = null; + /** * @internal * Holds the view value of the filter by label. @@ -650,11 +658,9 @@ export class DtFilterField this._handleAutocompleteSelected(event); }); - // Using fromEvent instead of an html binding so we get a stream and can easily do a debounce merge( fromEvent(this._inputEl.nativeElement, 'input').pipe( tap(() => (this._inputValue = this._inputEl.nativeElement.value)), - debounceTime(DT_FILTER_FIELD_TYPING_DEBOUNCE), ), this._inputReset$.pipe( tap(() => (this._inputValue = this._inputEl.nativeElement.value)), @@ -673,6 +679,7 @@ export class DtFilterField .subscribe(() => { this._currentTags.next(this._tags.toArray()); }); + this._updateDefaultSearchDef(); } ngOnDestroy(): void { @@ -742,6 +749,7 @@ export class DtFilterField const value = this._inputEl.nativeElement.value; this._writeControlValue(value); this._updateAutocompleteOptionsOrGroups(); + this._updateDefaultSearchDef(); this.inputChange.emit(value); this._validateInput(); @@ -842,6 +850,7 @@ export class DtFilterField this._updateLoading(); this._updateAutocompleteOptionsOrGroups(); this._updateMultiSelectOptionsOrGroups(); + this._updateDefaultSearchDef(); // If the currently edited part is a range it should pre-fill the // previously set values. if (removed.length === 1) { @@ -1057,27 +1066,36 @@ export class DtFilterField private _handleAutocompleteSelected( event: DtAutocompleteSelectedEvent, ): void { - const optionDef = event.option.value as DtAutocompleteValue; - this._peekCurrentFilterValues().push(optionDef); - // Reset input value to empty string after handling the value provided by the autocomplete. - // Otherwise the value of the autocomplete would be in the input elements and the next options - // would be filtered by the input value - this._writeInputValue(''); - if ( - isDtAutocompleteDef(optionDef) || - isDtFreeTextDef(optionDef) || - isDtRangeDef(optionDef) || - isDtMultiSelectDef(optionDef) + const submittedOption = event.option.value as + | DtAutocompleteValue + | DefaultSearchOption; + if (isDefaultSearchOption(submittedOption)) { + this._peekCurrentFilterValues().push(submittedOption.defaultSearchDef); + this._writeInputValue(submittedOption.inputValue); + this._handleFreeTextSubmitted(); + this._switchToRootDef(true); + } else if ( + isDtAutocompleteDef(submittedOption) || + isDtFreeTextDef(submittedOption) || + isDtRangeDef(submittedOption) || + isDtMultiSelectDef(submittedOption) ) { - this._currentDef = optionDef; + this._peekCurrentFilterValues().push(submittedOption); + this._currentDef = submittedOption; this._updateControl(); this._updateLoading(); this._updateFilterByLabel(); this._updateAutocompleteOptionsOrGroups(); - this._emitCurrentFilterChanges([optionDef], []); + this._updateDefaultSearchDef(); + this._emitCurrentFilterChanges([submittedOption], []); } else { + this._peekCurrentFilterValues().push(submittedOption); this._switchToRootDef(true); } + // Reset input value to empty string after handling the value provided by the autocomplete. + // Otherwise the value of the autocomplete would be in the input elements and the next options + // would be filtered by the input value + this._writeInputValue(''); // Clear any previous selected option. this._autocomplete._options.forEach((option) => { @@ -1110,7 +1128,10 @@ export class DtFilterField this._changeDetectorRef.markForCheck(); } - /** @internal */ + /** + * @internal Handles submitting the range field. + * Usually called when the user hits enter on the input field when a range filter is set + */ _handleRangeSubmitted(event: DtFilterFieldRangeSubmittedEvent): void { this._peekCurrentFilterValues().push({ operator: event.operator, @@ -1129,7 +1150,10 @@ export class DtFilterField this._changeDetectorRef.markForCheck(); } - /** @internal */ + /** + * @internal Handles submitting the multiselect field. + * Usually called when the user hits enter on the input field when a multiselect filter is set + */ _handleMultiSelectSubmitted( event: DtFilterFieldMultiSelectSubmittedEvent, ): void { @@ -1209,6 +1233,7 @@ export class DtFilterField this._updateTagData(); this._updateAutocompleteOptionsOrGroups(); this._updateMultiSelectOptionsOrGroups(); + this._updateDefaultSearchDef(); this._emitFilterChanges([], removedFilters); } this._resetEditMode(); @@ -1340,6 +1365,7 @@ export class DtFilterField this._updateControl(); this._updateLoading(); this._updateAutocompleteOptionsOrGroups(); + this._updateDefaultSearchDef(); this._stateChanges.next(); this._changeDetectorRef.markForCheck(); }, @@ -1361,6 +1387,7 @@ export class DtFilterField this._updateControl(); this._updateLoading(); this._updateAutocompleteOptionsOrGroups(); + this._updateDefaultSearchDef(); const currentFilterValues = this._currentFilterValues; this._currentFilterValues = []; if (shouldEmit && currentFilterValues.length) { @@ -1378,6 +1405,10 @@ export class DtFilterField this._control = new DtFilterFieldControl( this._currentDef.freeText.validators, ); + } else if (this._defaultSearchDef) { + this._control = new DtFilterFieldControl( + this._defaultSearchDef.freeText.validators, + ); } else { this._control = null; this._errors = []; @@ -1389,6 +1420,9 @@ export class DtFilterField if (this._rootDef) { const splitIndex = this._filters.indexOf(this._currentFilterValues); const tags = this._filters.map((values, i) => { + if (this._defaultSearchDef && values.length === 1) { + values.unshift(this._defaultSearchDef); + } let prevData: DtFilterFieldTagData | undefined; if (this.tagData.length) { prevData = this.tagData[i]; @@ -1404,7 +1438,6 @@ export class DtFilterField ); }); this.tagData = tags; - this._prefixTagData = this.tagData .slice(0, this._currentFilterValues.length ? splitIndex : undefined) .filter((tag: DtFilterFieldTagData | null) => tag !== null); @@ -1446,6 +1479,34 @@ export class DtFilterField } } + /** Updates the _defaultDearchDef member */ + private _updateDefaultSearchDef(): void { + const currentDef = this._currentDef; + if ( + isDtAutocompleteDef(currentDef) && + !isAsyncDtAutocompleteDef(currentDef) && + this._inputValue.length + ) { + if (this._defaultSearchDef === null) { + this._defaultSearchDef = findDefaultSearch(currentDef); + this._updateControl(); + this._writeControlValue(this._inputValue); + } else { + this._defaultSearchDef = findDefaultSearch(currentDef); + } + if (this._defaultSearchDef) + this._defaultSearchDef = filterFreeTextDef( + this._defaultSearchDef, + this._inputValue, + ) as DtNodeDef & { + freeText: DtFreeTextDef; + option: DtOptionDef; + }; + } else { + this._defaultSearchDef = null; + } + } + /** Updates the list of options or groups displayed in the multi select overlay */ private _updateMultiSelectOptionsOrGroups(): void { const currentDef = this._currentDef; diff --git a/libs/barista-components/filter-field/src/testing/filter-field-test-helpers.ts b/libs/barista-components/filter-field/src/testing/filter-field-test-helpers.ts index 0e5bbff2fa..df2bceef48 100644 --- a/libs/barista-components/filter-field/src/testing/filter-field-test-helpers.ts +++ b/libs/barista-components/filter-field/src/testing/filter-field-test-helpers.ts @@ -21,7 +21,6 @@ import { ComponentFixture, fakeAsync, TestBed, - tick, inject, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -33,10 +32,7 @@ import { } from '@dynatrace/testing/browser'; import { FILTER_FIELD_TEST_DATA_ASYNC } from '@dynatrace/testing/fixtures'; import { defaultTagDataForFilterValuesParser } from '../filter-field-util'; -import { - DtFilterField, - DT_FILTER_FIELD_TYPING_DEBOUNCE, -} from '../filter-field'; +import { DtFilterField } from '../filter-field'; import { DT_FILTER_VALUES_PARSER_CONFIG } from '../filter-field-config'; import { DtFilterFieldDefaultDataSource } from '../filter-field-default-data-source'; import { DtFilterFieldModule } from '../filter-field-module'; @@ -130,13 +126,11 @@ export function setupFilterFieldTest(): FilterFieldTestContext { } /** - * Types the passed value into the filter field input element, - * waits for the debounce time. + * Types the passed value into the filter field input element */ function typeIntoFilterElement(inputString: string): void { const inputEl = fixture!.debugElement.query(By.css('input')).nativeElement; typeInElement(inputString, inputEl); - tick(DT_FILTER_FIELD_TYPING_DEBOUNCE); } return { diff --git a/libs/barista-components/filter-field/src/types.ts b/libs/barista-components/filter-field/src/types.ts index 3fd1fecbab..3823ed9c0a 100644 --- a/libs/barista-components/filter-field/src/types.ts +++ b/libs/barista-components/filter-field/src/types.ts @@ -35,6 +35,11 @@ export enum DtNodeFlags { TypeMultiSelect, } +export interface DefaultSearchOption { + defaultSearchDef: DtAutocompleteValue; + inputValue: string; +} + export interface DtNodeDef { nodeFlags: DtNodeFlags; autocomplete: DtAutocompleteDef | null; @@ -59,6 +64,7 @@ export interface DtFreeTextDef { suggestions: DtNodeDef[]; validators: DtFilterFieldValidator[]; unique: boolean; + defaultSearch?: boolean; } export interface DtMultiSelectDef { @@ -191,6 +197,13 @@ export function isAsyncDtMultiSelectDef( return !!(isDtMultiSelectDef(def) && isDtOptionDef(def)); } +/** Whether the provided def object is an object and consists of a DefaultSearchDef */ +export function isDefaultSearchOption( + option: any, +): option is DefaultSearchOption { + return isObject(option) && isDtAutocompleteValue(option.defaultSearchDef); +} + /** Creates a new DtAutocompleteDef onto a provided existing NodeDef or a newly created one. */ export function dtAutocompleteDef( data: D, @@ -202,7 +215,13 @@ export function dtAutocompleteDef( ): DtNodeDef & { autocomplete: DtAutocompleteDef } { const def = { ...nodeDef(data, existingNodeDef), - autocomplete: { optionsOrGroups, distinct, async, partial, operators: [] }, + autocomplete: { + optionsOrGroups, + distinct, + async, + partial, + operators: [], + }, }; def.nodeFlags |= DtNodeFlags.TypeAutocomplete; return def; @@ -302,10 +321,11 @@ export function dtFreeTextDef( suggestions: DtNodeDef[], validators: DtFilterFieldValidator[], unique: boolean, + defaultSearch: boolean = false, ): DtNodeDef & { freeText: DtFreeTextDef } { const def = { ...nodeDef(data, existingNodeDef), - freeText: { suggestions, validators, unique }, + freeText: { suggestions, validators, unique, defaultSearch }, }; def.nodeFlags |= DtNodeFlags.TypeFreeText; return def; diff --git a/libs/examples/src/filter-field/BUILD.bazel b/libs/examples/src/filter-field/BUILD.bazel index 56c3de9d7b..d922abb404 100644 --- a/libs/examples/src/filter-field/BUILD.bazel +++ b/libs/examples/src/filter-field/BUILD.bazel @@ -17,6 +17,7 @@ ng_module( "filter-field-custom-parser-example/filter-field-custom-parser-example.html", "filter-field-custom-placeholder/filter-field-custom-placeholder.example.html", "filter-field-default-example/filter-field-default-example.html", + "filter-field-default-search-example/filter-field-default-search-example.html", "filter-field-disabled-example/filter-field-disabled-example.html", "filter-field-distinct-example/filter-field-distinct-example.html", "filter-field-infinite-data-depth-example/filter-field-infinite-data-depth-example.html", diff --git a/libs/examples/src/filter-field/filter-field-default-search-example/filter-field-default-search-example.html b/libs/examples/src/filter-field/filter-field-default-search-example/filter-field-default-search-example.html new file mode 100644 index 0000000000..b1ea813fc0 --- /dev/null +++ b/libs/examples/src/filter-field/filter-field-default-search-example/filter-field-default-search-example.html @@ -0,0 +1,5 @@ + diff --git a/libs/examples/src/filter-field/filter-field-default-search-example/filter-field-default-search-example.ts b/libs/examples/src/filter-field/filter-field-default-search-example/filter-field-default-search-example.ts new file mode 100644 index 0000000000..ff06cde81d --- /dev/null +++ b/libs/examples/src/filter-field/filter-field-default-search-example/filter-field-default-search-example.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, Component, OnDestroy, ViewChild } from '@angular/core'; +import { + DtFilterField, + DtFilterFieldDefaultDataSource, + DtFilterFieldDefaultDataSourceType, +} from '@dynatrace/barista-components/filter-field'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + selector: 'dt-example-filter-field-default-search', + templateUrl: 'filter-field-default-search-example.html', +}) +export class DtExampleFilterFieldDefaultSearch + implements AfterViewInit, OnDestroy { + private DATA: DtFilterFieldDefaultDataSourceType = { + autocomplete: [ + { + name: 'Asia', + defaultSearch: true, + suggestions: [ + { name: 'Australia' }, + { name: 'China' }, + { name: 'India' }, + ], + validators: [], + }, + { + name: 'Europe', + distinct: true, + autocomplete: [ + { + name: 'Austria', + autocomplete: [ + { + name: 'Upper Austria', + autocomplete: [ + { + name: 'Linz', + }, + ], + }, + { + name: 'Vienna', + autocomplete: [ + { + name: 'Districts', + range: { + operators: { + range: true, + }, + unit: ' district', + }, + }, + ], + }, + ], + }, + ], + }, + { + name: 'North America', + autocomplete: [ + { + name: 'USA', + autocomplete: [ + { + name: 'California', + autocomplete: [ + { + name: 'San Francisco', + }, + { + name: 'Los Angeles', + }, + ], + }, + { + name: 'New York', + autocomplete: [ + { + name: 'New York', + }, + ], + }, + ], + }, + ], + }, + ], + }; + + _dataSource = new DtFilterFieldDefaultDataSource(this.DATA); + + @ViewChild(DtFilterField) filterField: DtFilterField; + + private readonly _destroy$ = new Subject(); + + ngAfterViewInit(): void { + this.filterField.interactionStateChange + .pipe(takeUntil(this._destroy$)) + .subscribe((state) => { + console.log('interaction state: ', state); + console.log( + 'interaction state member: ', + this.filterField.interactionState, + ); + }); + } + + ngOnDestroy(): void { + this._destroy$.next(); + this._destroy$.complete(); + } +} diff --git a/libs/examples/src/filter-field/filter-field-examples.module.ts b/libs/examples/src/filter-field/filter-field-examples.module.ts index 9e7bed1c18..148b41b134 100644 --- a/libs/examples/src/filter-field/filter-field-examples.module.ts +++ b/libs/examples/src/filter-field/filter-field-examples.module.ts @@ -22,6 +22,7 @@ import { DtExampleFilterFieldClearall } from './filter-field-clearall-example/fi import { DtExampleFilterFieldCustomParser } from './filter-field-custom-parser-example/filter-field-custom-parser-example'; import { DtExampleFilterFieldCustomPlaceholder } from './filter-field-custom-placeholder/filter-field-custom-placeholder.example'; import { DtExampleFilterFieldDefault } from './filter-field-default-example/filter-field-default-example'; +import { DtExampleFilterFieldDefaultSearch } from './filter-field-default-search-example/filter-field-default-search-example'; import { DtExampleFilterFieldDisabled } from './filter-field-disabled-example/filter-field-disabled-example'; import { DtExampleFilterFieldDistinct } from './filter-field-distinct-example/filter-field-distinct-example'; import { DtExampleFilterFieldInfiniteDataDepth } from './filter-field-infinite-data-depth-example/filter-field-infinite-data-depth-example'; @@ -40,6 +41,7 @@ import { DtExampleFilterFieldValidator } from './filter-field-validator-example/ DtExampleFilterFieldCustomParser, DtExampleFilterFieldCustomPlaceholder, DtExampleFilterFieldDefault, + DtExampleFilterFieldDefaultSearch, DtExampleFilterFieldDistinct, DtExampleFilterFieldInfiniteDataDepth, DtExampleFilterFieldPartial, diff --git a/libs/examples/src/filter-field/filter-field-validator-example/filter-field-validator-example.ts b/libs/examples/src/filter-field/filter-field-validator-example/filter-field-validator-example.ts index ba9d0c86fc..73df1f1055 100644 --- a/libs/examples/src/filter-field/filter-field-validator-example/filter-field-validator-example.ts +++ b/libs/examples/src/filter-field/filter-field-validator-example/filter-field-validator-example.ts @@ -30,7 +30,11 @@ export class DtExampleFilterFieldValidator { autocomplete: [ { name: 'Custom', - suggestions: ['Linz', 'Vienna', 'Graz'], + suggestions: [ + { name: 'Linz' }, + { name: 'Vienna' }, + { name: 'Graz' }, + ], validators: [ { validatorFn: Validators.required, error: 'is required' }, ], diff --git a/libs/examples/src/filter-field/index.ts b/libs/examples/src/filter-field/index.ts index 9b9fab65c2..f07f59eda7 100644 --- a/libs/examples/src/filter-field/index.ts +++ b/libs/examples/src/filter-field/index.ts @@ -19,6 +19,7 @@ export * from './filter-field-clearall-example/filter-field-clearall-example'; export * from './filter-field-custom-parser-example/filter-field-custom-parser-example'; export * from './filter-field-custom-placeholder/filter-field-custom-placeholder.example'; export * from './filter-field-default-example/filter-field-default-example'; +export * from './filter-field-default-search-example/filter-field-default-search-example'; export * from './filter-field-disabled-example/filter-field-disabled-example'; export * from './filter-field-distinct-example/filter-field-distinct-example'; export * from './filter-field-examples.module'; diff --git a/libs/examples/src/index.ts b/libs/examples/src/index.ts index 04e959ab5b..6c935a7274 100644 --- a/libs/examples/src/index.ts +++ b/libs/examples/src/index.ts @@ -149,6 +149,7 @@ import { DtExampleFilterFieldClearall } from './filter-field/filter-field-cleara import { DtExampleFilterFieldCustomParser } from './filter-field/filter-field-custom-parser-example/filter-field-custom-parser-example'; import { DtExampleFilterFieldCustomPlaceholder } from './filter-field/filter-field-custom-placeholder/filter-field-custom-placeholder.example'; import { DtExampleFilterFieldDefault } from './filter-field/filter-field-default-example/filter-field-default-example'; +import { DtExampleFilterFieldDefaultSearch } from './filter-field/filter-field-default-search-example/filter-field-default-search-example'; import { DtExampleFilterFieldDisabled } from './filter-field/filter-field-disabled-example/filter-field-disabled-example'; import { DtExampleFilterFieldDistinct } from './filter-field/filter-field-distinct-example/filter-field-distinct-example'; import { DtExampleFilterFieldInfiniteDataDepth } from './filter-field/filter-field-infinite-data-depth-example/filter-field-infinite-data-depth-example'; @@ -525,8 +526,10 @@ export { DtExampleFilterFieldCustomParser, DtExampleFilterFieldCustomPlaceholder as DtExampleFilterFieldCustomPlaceholderExample, DtExampleFilterFieldDefault, + DtExampleFilterFieldDefaultSearch, DtExampleFilterFieldDisabled, DtExampleFilterFieldDistinct, + DtExampleFilterFieldInfiniteDataDepth, DtExampleFilterFieldPartial, DtExampleFilterFieldProgrammaticFilters, DtExampleFilterFieldReadOnlyTags, @@ -712,7 +715,6 @@ export { DtExampleTreeTableDefault, DtExampleTreeTableProblemIndicator, DtExampleTreeTableSimple, - DtExampleFilterFieldInfiniteDataDepth, DtExampleSelectCustomValueTemplate, DtExampleCalendarMinMax, DtExampleTimepickerMinMax, @@ -882,8 +884,13 @@ export const EXAMPLES_MAP = new Map>([ DtExampleFilterFieldCustomPlaceholder, ], ['DtExampleFilterFieldDefault', DtExampleFilterFieldDefault], + ['DtExampleFilterFieldDefaultSearch', DtExampleFilterFieldDefaultSearch], ['DtExampleFilterFieldDisabled', DtExampleFilterFieldDisabled], ['DtExampleFilterFieldDistinct', DtExampleFilterFieldDistinct], + [ + 'DtExampleFilterFieldInfiniteDataDepth', + DtExampleFilterFieldInfiniteDataDepth, + ], ['DtExampleFilterFieldPartial', DtExampleFilterFieldPartial], [ 'DtExampleFilterFieldProgrammaticFilters', @@ -1100,8 +1107,4 @@ export const EXAMPLES_MAP = new Map>([ ['DtExampleTreeTableDefault', DtExampleTreeTableDefault], ['DtExampleTreeTableProblemIndicator', DtExampleTreeTableProblemIndicator], ['DtExampleTreeTableSimple', DtExampleTreeTableSimple], - [ - 'DtExampleFilterFieldInfiniteDataDepth', - DtExampleFilterFieldInfiniteDataDepth, - ], ]);