From 61d5aa965b3aff650f86bddd2a34217da34d615c Mon Sep 17 00:00:00 2001 From: Mykyta Maliarchuk <84377976+nikita-web-ua@users.noreply.github.com> Date: Wed, 14 Jun 2023 12:29:43 +0200 Subject: [PATCH] [ACS-4986] Advanced Search - New component for Tags and Location filters (#8655) * [ACS-4986] SearchChipsAutocompleteComponent - minimal state * [ACS-4986] refactored components * [ACS-4986] documentation * [ACS-4986] i18n * [ACS-4986] versionIndex.md * [ACS-4986] unit tests * [ACS-4986] replaced function calls on pipe * [ACS-4986] linting * [ACS-4986] slight correction * [ACS-4986] missing types * [ACS-4986] space * [ACS-4986] moved pipe + docs & tests * [ACS-4986] changed pipe type * [ACS-4986] versionIndex.md * [ACS-4986] removed 'important' in styles * [ACS-4986] fixed code smell * [ACS-4986] linting --- docs/README.md | 2 + ...earch-chip-autocomplete-input.component.md | 52 +++++ ...rch-filter-autocomplete-chips.component.md | 67 +++++++ .../pipes/is-included.pipe.md | 28 +++ .../images/search-chip-autocomplete-input.png | Bin 0 -> 10331 bytes .../search-filter-autocomplete-chips.png | Bin 0 -> 13620 bytes docs/versionIndex.md | 3 + lib/content-services/src/lib/i18n/en.json | 6 +- .../src/lib/pipes/content-pipe.module.ts | 10 +- .../src/lib/pipes/is-included.pipe.spec.ts | 44 +++++ .../src/lib/pipes/is-included.pipe.ts | 27 +++ .../src/lib/pipes/public-api.ts | 1 + ...rch-chip-autocomplete-input.component.html | 28 +++ ...rch-chip-autocomplete-input.component.scss | 46 +++++ ...-chip-autocomplete-input.component.spec.ts | 177 ++++++++++++++++++ ...earch-chip-autocomplete-input.component.ts | 120 ++++++++++++ ...h-filter-autocomplete-chips.component.html | 15 ++ ...ilter-autocomplete-chips.component.spec.ts | 122 ++++++++++++ ...rch-filter-autocomplete-chips.component.ts | 108 +++++++++++ .../search-widget-settings.interface.ts | 2 + .../src/lib/search/public-api.ts | 2 + .../src/lib/search/search.module.ts | 8 + .../search/services/search-filter.service.ts | 4 +- 23 files changed, 866 insertions(+), 6 deletions(-) create mode 100644 docs/content-services/components/search-chip-autocomplete-input.component.md create mode 100644 docs/content-services/components/search-filter-autocomplete-chips.component.md create mode 100644 docs/content-services/pipes/is-included.pipe.md create mode 100644 docs/docassets/images/search-chip-autocomplete-input.png create mode 100644 docs/docassets/images/search-filter-autocomplete-chips.png create mode 100644 lib/content-services/src/lib/pipes/is-included.pipe.spec.ts create mode 100644 lib/content-services/src/lib/pipes/is-included.pipe.ts create mode 100644 lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.html create mode 100644 lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.scss create mode 100644 lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.spec.ts create mode 100644 lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.ts create mode 100644 lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.html create mode 100644 lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.spec.ts create mode 100644 lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.ts diff --git a/docs/README.md b/docs/README.md index 3c4834bae2..cc9081ec5e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -287,10 +287,12 @@ for more information about installing and using the source code. | [Rating component](content-services/components/rating.component.md) | Allows a user to add and remove rating to an item. | [Source](../lib/content-services/src/lib/social/rating.component.ts) | | [Search check list component](content-services/components/search-check-list.component.md) | Implements a checklist widget for the Search Filter component. | [Source](../lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.ts) | | [Search Chip Input Component](content-services/components/search-chip-input.component.md) | Displays input for providing phrases display as "chips". | [Source](../lib/content-services/src/lib/search/components/search-chip-input/search-chip-input.component.ts) | +| [Search Chip Autocomplete Input component](content-services/components/search-chip-autocomplete-input.component.md) | Displays an input with autocomplete options. | [Source](../lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.ts) | | [Search Chip List Component](content-services/components/search-chip-list.component.md) | Displays search criteria as a set of "chips". | [Source](../lib/content-services/src/lib/search/components/search-chip-list/search-chip-list.component.ts) | | [Search control component](content-services/components/search-control.component.md) | Displays a input text that shows find-as-you-type suggestions. | [Source](../lib/content-services/src/lib/search/components/search-control.component.ts) | | [Search date range component](content-services/components/search-date-range.component.md) | Implements a search widget for the Search Filter component. | [Source](../lib/content-services/src/lib/search/components/search-date-range/search-date-range.component.ts) | | [Search datetime range component](content-services/components/search-datetime-range.component.md) | Implements a search widget for the Search Filter component. | [Source](../lib/content-services/src/lib/search/components/search-datetime-range/search-datetime-range.component.ts) | +| [Search Filter Autocomplete Chips component](content-services/components/search-filter-autocomplete-chips.component.md) | Implements a search widget for the Search Filter component. | [Source](../lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.ts) | | [Search Filter Chips component](content-services/components/search-filter-chips.component.md) | Represents a chip based container component for custom search and faceted search settings. | [Source](../lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.ts) | | [Search Filter component](content-services/components/search-filter.component.md) | Represents a main container component for custom search and faceted search settings. | [Source](../lib/content-services/src/lib/search/components/search-filter/search-filter.component.ts) | | [Search Form component](content-services/components/search-form.component.md) | Search Form screenshot | [Source](../lib/content-services/src/lib/search/components/search-form/search-form.component.ts) | diff --git a/docs/content-services/components/search-chip-autocomplete-input.component.md b/docs/content-services/components/search-chip-autocomplete-input.component.md new file mode 100644 index 0000000000..c39dbb6ea2 --- /dev/null +++ b/docs/content-services/components/search-chip-autocomplete-input.component.md @@ -0,0 +1,52 @@ +--- +Title: Search Chip Autocomplete Input component +Added: v6.1.0 +Status: Active +Last reviewed: 2023-06-13 +--- + +# [Search Chip Autocomplete Input component](../../../lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.ts "Defined in search-chip-autocomplete-input.component.ts") + +Represents an input with autocomplete options. + +![Search Chip Autocomplete Input](../../docassets/images/search-chip-autocomplete-input.png) + +## Basic usage + +```html + + +``` + +### Properties + +| Name | Type | Default value | Description | +|---------------------------|--------------------------|----|-----------------------------------------------------------------------------------------------| +| autocompleteOptions | `string[]` | [] | Options for autocomplete | +| onReset$ | [`Observable`](https://rxjs.dev/guide/observable)`` | | Observable that will listen to any reset event causing component to clear the chips and input | +| allowOnlyPredefinedValues | boolean | true | A flag that indicates whether it is possible to add a value not from the predefined ones | + +### Events + +| Name | Type | Description | +| ---- | ---- |-----------------------------------------------| +| optionsChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when the selected options are changed | + +## See also + +- [Search Configuration Guide](../../user-guide/search-configuration-guide.md) +- [Search Query Builder service](../services/search-query-builder.service.md) +- [Search Widget Interface](../interfaces/search-widget.interface.md) +- [Search Filter Autocomplete Chips component](search-filter-autocomplete-chips.component.md) +- [Search Logical Filter component](search-logical-filter.component.md) +- [Search check list component](search-check-list.component.md) +- [Search date range component](search-date-range.component.md) +- [Search number range component](search-number-range.component.md) +- [Search radio component](search-radio.component.md) +- [Search slider component](search-slider.component.md) +- [Search text component](search-text.component.md) +- [Search Chip Input component](search-chip-input.component.md) diff --git a/docs/content-services/components/search-filter-autocomplete-chips.component.md b/docs/content-services/components/search-filter-autocomplete-chips.component.md new file mode 100644 index 0000000000..9f6351b8ac --- /dev/null +++ b/docs/content-services/components/search-filter-autocomplete-chips.component.md @@ -0,0 +1,67 @@ +--- +Title: Search Filter Autocomplete Chips component +Added: v6.1.0 +Status: Active +Last reviewed: 2023-06-13 +--- + +# [Search Filter Autocomplete Chips component](../../../lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.ts "Defined in search-filter-autocomplete-chips.component.ts") + +Implements a [search widget](../../../lib/content-services/src/lib/search/models/search-widget.interface.ts) consists of 1 input with autocomplete options representing conditions to form search query. + +![Search Filter Autocomplete Chips](../../docassets/images/search-filter-autocomplete-chips.png) + +## Basic usage + +```json +{ + "search": { + "categories": [ + { + "id": "location", + "name": "Location", + "enabled": true, + "component": { + "selector": "autocomplete-chips", + "settings": { + "allowUpdateOnChange": false, + "hideDefaultAction": true, + "allowOnlyPredefinedValues": false, + "field": "SITE", + "options": [ "Option 1", "Option 2" ] + } + } + } + ] + } +} +``` + +### Settings + +| Name | Type | Description | +| ---- |----------|--------------------------------------------------------------------------------------------------------------------| +| field | `string` | Field to apply the query to. Required value | +| options | `string[]` | Predefined options for autocomplete | +| allowOnlyPredefinedValues | `boolean` | Specifies whether the input values should only be from predefined | +| allowUpdateOnChange | `boolean` | Enable/Disable the update fire event when text has been changed. By default is true | +| hideDefaultAction | `boolean` | Show/hide the widget actions. By default is false | +## Details + +This component allows the user to choose filter options for the search query. +See the [Search Chip Autocomplete Input component](search-chip-autocomplete-input.component.md) for more details. + +## See also + +- [Search Configuration Guide](../../user-guide/search-configuration-guide.md) +- [Search Query Builder service](../services/search-query-builder.service.md) +- [Search Widget Interface](../interfaces/search-widget.interface.md) +- [Search Chip Autocomplete Input component](search-chip-autocomplete-input.component.md) +- [Search Chip Input component](search-chip-input.component.md) +- [Search check list component](search-check-list.component.md) +- [Search date range component](search-date-range.component.md) +- [Search number range component](search-number-range.component.md) +- [Search radio component](search-radio.component.md) +- [Search slider component](search-slider.component.md) +- [Search text component](search-text.component.md) +- [Search Logical Filter component](search-logical-filter.component.md) diff --git a/docs/content-services/pipes/is-included.pipe.md b/docs/content-services/pipes/is-included.pipe.md new file mode 100644 index 0000000000..405ab61c9c --- /dev/null +++ b/docs/content-services/pipes/is-included.pipe.md @@ -0,0 +1,28 @@ +--- +Title: Is Included pipe +Added: v6.1.0 +Status: Active +Last reviewed: 2023-06-13 +--- + +# [Is Included pipe](../../../lib/content-services/src/lib/pipes/is-included.pipe.ts "Defined in is-included.pipe.ts") + +Checks if the provided value is contained in the provided array. + +## Basic Usage + + + +```HTML + +``` + + + +## Details + +The pipe takes the provided value and checks if that value is included in the provided array and returns the appropriate boolean value. + +## See also + +- [File upload error pipe](./file-upload-error.pipe.md) diff --git a/docs/docassets/images/search-chip-autocomplete-input.png b/docs/docassets/images/search-chip-autocomplete-input.png new file mode 100644 index 0000000000000000000000000000000000000000..ab726b1a0b580b018eb7a88ed356c1b0a233e150 GIT binary patch literal 10331 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 Brkel& literal 0 HcmV?d00001 diff --git a/docs/docassets/images/search-filter-autocomplete-chips.png b/docs/docassets/images/search-filter-autocomplete-chips.png new file mode 100644 index 0000000000000000000000000000000000000000..cba4249eb80356e7ab51c24a350edad420d39112 GIT binary patch literal 13620 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 diff --git a/lib/content-services/src/lib/i18n/en.json b/lib/content-services/src/lib/i18n/en.json index b5cd5d8a5d..59caf12cf0 100644 --- a/lib/content-services/src/lib/i18n/en.json +++ b/lib/content-services/src/lib/i18n/en.json @@ -297,7 +297,8 @@ "APPLY": "Apply", "CLEAR-ALL": "Clear all", "SHOW-MORE": "Show more", - "SHOW-LESS": "Show less" + "SHOW-LESS": "Show less", + "ADD_OPTION": "Add Option..." }, "BUTTONS": { "CLOSE": "Close", @@ -331,7 +332,8 @@ "BEYOND-MAX-DATETIME": "The datetime is beyond the maximum datetime." }, "ARIA-LABEL": { - "SEARCH_FILTER": "Search Filter List" + "SEARCH_FILTER": "Search Filter List", + "OPTIONS-SELECTION": "Options Selection" }, "ANY": "Any" }, diff --git a/lib/content-services/src/lib/pipes/content-pipe.module.ts b/lib/content-services/src/lib/pipes/content-pipe.module.ts index 994dc2d36f..37eae541b2 100644 --- a/lib/content-services/src/lib/pipes/content-pipe.module.ts +++ b/lib/content-services/src/lib/pipes/content-pipe.module.ts @@ -19,6 +19,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { NodeNameTooltipPipe } from './node-name-tooltip.pipe'; import { TranslateModule } from '@ngx-translate/core'; +import { IsIncludedPipe } from './is-included.pipe'; @NgModule({ imports: [ @@ -26,13 +27,16 @@ import { TranslateModule } from '@ngx-translate/core'; TranslateModule ], declarations: [ - NodeNameTooltipPipe + NodeNameTooltipPipe, + IsIncludedPipe ], providers: [ - NodeNameTooltipPipe + NodeNameTooltipPipe, + IsIncludedPipe ], exports: [ - NodeNameTooltipPipe + NodeNameTooltipPipe, + IsIncludedPipe ] }) export class ContentPipeModule { diff --git a/lib/content-services/src/lib/pipes/is-included.pipe.spec.ts b/lib/content-services/src/lib/pipes/is-included.pipe.spec.ts new file mode 100644 index 0000000000..6a61657146 --- /dev/null +++ b/lib/content-services/src/lib/pipes/is-included.pipe.spec.ts @@ -0,0 +1,44 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * 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 { IsIncludedPipe } from './is-included.pipe'; + +describe('IsIncludedPipe', () => { + + let pipe: IsIncludedPipe; + const array = [1, 2, 'test', [null], {}]; + + beforeEach(() => { + pipe = new IsIncludedPipe(); + }); + + it('should return true if the string is contained in an array', () => { + expect(pipe.transform('test', array)).toBeTruthy(); + }); + + it('should return false if the string is not contained in an array', () => { + expect(pipe.transform('test 1', array)).toBeFalsy(); + }); + + it('should return true if the number is in the array', () => { + expect(pipe.transform(2, array)).toBeTruthy(); + }); + + it('should return false if the number is not contained in an array', () => { + expect(pipe.transform(50, array)).toBeFalsy(); + }); +}); diff --git a/lib/content-services/src/lib/pipes/is-included.pipe.ts b/lib/content-services/src/lib/pipes/is-included.pipe.ts new file mode 100644 index 0000000000..114b4592d3 --- /dev/null +++ b/lib/content-services/src/lib/pipes/is-included.pipe.ts @@ -0,0 +1,27 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * 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 { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'adfIsIncluded' +}) +export class IsIncludedPipe implements PipeTransform { + transform(value: T, array: T[]): boolean { + return array.includes(value); + } +} diff --git a/lib/content-services/src/lib/pipes/public-api.ts b/lib/content-services/src/lib/pipes/public-api.ts index 966c6a0232..2ab199eee7 100644 --- a/lib/content-services/src/lib/pipes/public-api.ts +++ b/lib/content-services/src/lib/pipes/public-api.ts @@ -16,4 +16,5 @@ */ export * from './node-name-tooltip.pipe'; +export * from './is-included.pipe'; export * from './content-pipe.module'; diff --git a/lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.html b/lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.html new file mode 100644 index 0000000000..967aefa7b1 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.html @@ -0,0 +1,28 @@ + + + + {{option}} + + + + + + + {{option}} + + + diff --git a/lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.scss b/lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.scss new file mode 100644 index 0000000000..73b647a990 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.scss @@ -0,0 +1,46 @@ +adf-search-chip-autocomplete-input { + .adf-chip-list { + width: 100%; + } + + .mat-form-field-appearance-outline .mat-form-field-outline { + top: 0; + } + + .mat-form-field-infix { + border: none; + } + + .mat-chip.adf-option-chips { + border: 1px solid var(--theme-text-color); + border-radius: 10px; + background-color: var(--theme-primary-color-default-contrast); + height: auto; + word-break: break-word; + } + + .mat-chip-remove.adf-option-chips-delete-button { + font-size: 13px; + height: 13px; + width: 13px; + + .adf-option-chips-delete-icon.mat-icon { + font-size: 13px; + height: 13px; + width: 13px; + } + } + + .mat-chip-list-wrapper { + min-height: 40px; + } + + .mat-form-field-wrapper { + padding: 0; + } +} + +.mat-option.adf-autocomplete-added-option { + background: var(--adf-theme-mat-grey-color-a200); + color: var(--adf-theme-primary-300); +} diff --git a/lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.spec.ts b/lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.spec.ts new file mode 100644 index 0000000000..d8be21e38d --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.spec.ts @@ -0,0 +1,177 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * 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 { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatChip, MatChipRemove } from '@angular/material/chips'; +import { By } from '@angular/platform-browser'; +import { TranslateModule } from '@ngx-translate/core'; +import { Subject } from 'rxjs'; +import { ContentTestingModule } from '../../../testing/content.testing.module'; +import { SearchChipAutocompleteInputComponent } from './search-chip-autocomplete-input.component'; + +describe('SearchChipAutocompleteInputComponent', () => { + let component: SearchChipAutocompleteInputComponent; + let fixture: ComponentFixture; + const onResetSubject = new Subject(); + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [SearchChipAutocompleteInputComponent], + imports: [ + TranslateModule.forRoot(), + ContentTestingModule + ] + }); + + fixture = TestBed.createComponent(SearchChipAutocompleteInputComponent); + component = fixture.componentInstance; + component.onReset$ = onResetSubject.asObservable(); + fixture.detectChanges(); + component.autocompleteOptions = ['option1', 'option2']; + }); + + function enterNewInputValue(value: string) { + const inputElement = fixture.debugElement.query(By.css('input')); + inputElement.nativeElement.dispatchEvent(new Event('focusin')); + inputElement.nativeElement.value = value; + inputElement.nativeElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); + } + + function addNewOption(value: string) { + const inputElement = fixture.debugElement.query(By.css('input')).nativeElement; + inputElement.value = value; + fixture.detectChanges(); + inputElement.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 13})); + fixture.detectChanges(); + } + + function getChipList(): MatChip[] { + return fixture.debugElement.queryAll(By.css('mat-chip')).map((chip) => chip.nativeElement); + } + + function getChipValue(index: number): string { + return fixture.debugElement.queryAll(By.css('mat-chip span')).map((chip) => chip.nativeElement)[index].innerText; + } + + it('should add new option only if value is predefined when allowOnlyPredefinedValues = true', () => { + addNewOption('test'); + addNewOption('option1'); + expect(getChipList().length).toBe(1); + expect(getChipValue(0)).toBe('option1'); + }); + + it('should add new option even if value is not predefined when allowOnlyPredefinedValues = false', () => { + component.allowOnlyPredefinedValues = false; + addNewOption('test'); + addNewOption('option1'); + expect(getChipList().length).toBe(2); + expect(getChipValue(0)).toBe('test'); + }); + + it('should add new option upon clicking on option from autocomplete', async () => { + const optionsChangedSpy = spyOn(component.optionsChanged, 'emit'); + enterNewInputValue('op'); + await fixture.whenStable(); + + const matOptions = document.querySelectorAll('mat-option'); + expect(matOptions.length).toBe(2); + + const optionToClick = matOptions[0] as HTMLElement; + optionToClick.click(); + + expect(optionsChangedSpy).toHaveBeenCalledOnceWith(['option1']); + expect(component.selectedOptions).toEqual(['option1']); + expect(getChipList().length).toBe(1); + }); + + it('should apply class to already selected options', async () => { + addNewOption('option1'); + enterNewInputValue('op'); + + const addedOptions = fixture.debugElement.queryAll(By.css('.adf-autocomplete-added-option')); + + await fixture.whenStable(); + + expect(addedOptions[0]).toBeTruthy(); + expect(addedOptions.length).toBe(1); + }); + + it('should limit autocomplete list to 15 values max', () => { + component.autocompleteOptions = ['a1','a2','a3','a4','a5','a6','a7','a8','a9','a10','a11','a12','a13','a14','a15','a16']; + enterNewInputValue('a'); + + const matOptions = document.querySelectorAll('mat-option'); + expect(matOptions.length).toBe(15); + }); + + it('should not add a value if same value has already been added', () => { + addNewOption('option1'); + addNewOption('option1'); + expect(getChipList().length).toBe(1); + }); + + it('should show autocomplete list if similar predefined values exists', () => { + enterNewInputValue('op'); + const matOptions = document.querySelectorAll('mat-option'); + expect(matOptions.length).toBe(2); + }); + + it('should not show autocomplete list if there are no similar predefined values', () => { + enterNewInputValue('test'); + const matOptions = document.querySelectorAll('mat-option'); + expect(matOptions.length).toBe(0); + }); + + it('should emit new value when selected options changed', () => { + const optionsChangedSpy = spyOn(component.optionsChanged, 'emit'); + addNewOption('option1'); + expect(optionsChangedSpy).toHaveBeenCalledOnceWith(['option1']); + expect(getChipList().length).toBe(1); + expect(getChipValue(0)).toBe('option1'); + }); + + it('should clear the input after a new value is added', () => { + const input = fixture.debugElement.query(By.css('input')).nativeElement; + addNewOption('option1'); + expect(input.value).toBe(''); + }); + + it('should reset all options when onReset$ event is emitted', () => { + addNewOption('option1'); + addNewOption('option2'); + const optionsChangedSpy = spyOn(component.optionsChanged, 'emit'); + onResetSubject.next(); + fixture.detectChanges(); + + expect(optionsChangedSpy).toHaveBeenCalledOnceWith([]); + expect(getChipList()).toEqual([]); + expect(component.selectedOptions).toEqual([]); + }); + + it('should remove option upon clicking remove button', () => { + addNewOption('option1'); + addNewOption('option2'); + const optionsChangedSpy = spyOn(component.optionsChanged, 'emit'); + + fixture.debugElement.query(By.directive(MatChipRemove)).nativeElement.click(); + fixture.detectChanges(); + + expect(optionsChangedSpy).toHaveBeenCalledOnceWith(['option2']); + expect(getChipList().length).toEqual(1); + }); +}); diff --git a/lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.ts b/lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.ts new file mode 100644 index 0000000000..d03a653a7c --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-chip-autocomplete-input/search-chip-autocomplete-input.component.ts @@ -0,0 +1,120 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * 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, ViewEncapsulation, ElementRef, ViewChild, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core'; +import { ENTER } from '@angular/cdk/keycodes'; +import { FormControl } from '@angular/forms'; +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { MatChipInputEvent } from '@angular/material/chips'; +import { Observable, Subject } from 'rxjs'; +import { map, startWith, takeUntil } from 'rxjs/operators'; + +@Component({ + selector: 'adf-search-chip-autocomplete-input', + templateUrl: './search-chip-autocomplete-input.component.html', + styleUrls: ['./search-chip-autocomplete-input.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy { + @ViewChild('optionInput') + optionInput: ElementRef; + + @Input() + autocompleteOptions: string[] = []; + + @Input() + onReset$: Observable; + + @Input() + allowOnlyPredefinedValues = true; + + @Output() + optionsChanged: EventEmitter = new EventEmitter(); + + readonly separatorKeysCodes = [ENTER] as const; + formCtrl = new FormControl(''); + filteredOptions$: Observable; + selectedOptions: string[] = []; + private onDestroy$ = new Subject(); + + constructor() { + this.filteredOptions$ = this.formCtrl.valueChanges.pipe( + startWith(null), + map((value: string | null) => (value ? this.filter(value) : [])) + ); + } + + ngOnInit() { + this.onReset$?.pipe(takeUntil(this.onDestroy$)).subscribe(() => this.reset()); + } + + ngOnDestroy() { + this.onDestroy$.next(); + this.onDestroy$.complete(); + } + + add(event: MatChipInputEvent) { + const value = (event.value || '').trim(); + + if (value && this.isExists(value) && !this.isAdded(value)) { + this.selectedOptions.push(value); + this.optionsChanged.emit(this.selectedOptions); + event.chipInput.clear(); + this.formCtrl.setValue(null); + } + } + + remove(value: string) { + const index = this.selectedOptions.indexOf(value); + + if (index >= 0) { + this.selectedOptions.splice(index, 1); + this.optionsChanged.emit(this.selectedOptions); + } + } + + selected(event: MatAutocompleteSelectedEvent) { + if (!this.isAdded(event.option.viewValue)) { + this.selectedOptions.push(event.option.viewValue); + this.optionInput.nativeElement.value = ''; + this.formCtrl.setValue(null); + this.optionsChanged.emit(this.selectedOptions); + } + } + + private filter(value: string): string[] { + const filterValue = value.toLowerCase(); + return this.autocompleteOptions.filter(option => option.toLowerCase().includes(filterValue)).slice(0, 15); + } + + private isAdded(value: string): boolean { + return this.selectedOptions.includes(value); + } + + private isExists(value: string): boolean { + return this.allowOnlyPredefinedValues + ? this.autocompleteOptions.map(option => option.toLowerCase()).includes(value.toLowerCase()) + : true; + } + + private reset() { + this.selectedOptions = []; + this.optionsChanged.emit(this.selectedOptions); + this.formCtrl.setValue(null); + this.optionInput.nativeElement.value = ''; + } +} diff --git a/lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.html b/lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.html new file mode 100644 index 0000000000..7e95495bd0 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.html @@ -0,0 +1,15 @@ + + + +
+ + +
diff --git a/lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.spec.ts b/lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.spec.ts new file mode 100644 index 0000000000..8d3d17b342 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.spec.ts @@ -0,0 +1,122 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * 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 { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { ContentTestingModule } from '../../../testing/content.testing.module'; +import { TranslateModule } from '@ngx-translate/core'; +import { SearchFilterAutocompleteChipsComponent } from './search-filter-autocomplete-chips.component'; +import { TagService } from '@alfresco/adf-content-services'; +import { EMPTY, of } from 'rxjs'; + +describe('SearchFilterAutocompleteChipsComponent', () => { + let component: SearchFilterAutocompleteChipsComponent; + let fixture: ComponentFixture; + let tagService: TagService; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [SearchFilterAutocompleteChipsComponent], + imports: [ + TranslateModule.forRoot(), + ContentTestingModule + ], + providers: [{ + provide: TagService, + useValue: { getAllTheTags: () => EMPTY } + }] + }); + + fixture = TestBed.createComponent(SearchFilterAutocompleteChipsComponent); + component = fixture.componentInstance; + tagService = TestBed.inject(TagService); + component.id = 'test-id'; + component.context = { + queryFragments: {}, + update: () => EMPTY + } as any; + component.settings = { + field: 'test', allowUpdateOnChange: true, hideDefaultAction: false, allowOnlyPredefinedValues: false, + options: ['option1', 'option2'] + }; + fixture.detectChanges(); + }); + + function addNewOption(value: string) { + const inputElement = fixture.debugElement.query(By.css('adf-search-chip-autocomplete-input input')).nativeElement; + inputElement.value = value; + inputElement.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 13})); + fixture.detectChanges(); + } + + it('should set autocomplete options on init', () => { + component.settings.options = ['test 1', 'test 2']; + component.ngOnInit(); + expect(component.autocompleteOptions).toEqual(['test 1', 'test 2']); + }); + + it('should load tags if field = TAG', () => { + const tagPagingMock = { + list: { + pagination: {}, + entries: [{entry: {tag: 'tag1', id: 'id1'}}, {entry: {tag: 'tag2', id: 'id2'}}] + } + }; + + component.settings.field = 'TAG'; + spyOn(tagService, 'getAllTheTags').and.returnValue(of(tagPagingMock)); + component.ngOnInit(); + expect(component.autocompleteOptions).toEqual(['tag1', 'tag2']); + }); + + it('should update display value when options changes', () => { + const newOption = 'option1'; + spyOn(component, 'onOptionsChange').and.callThrough(); + spyOn(component.displayValue$, 'next'); + addNewOption(newOption); + + expect(component.onOptionsChange).toHaveBeenCalled(); + expect(component.displayValue$.next).toHaveBeenCalledOnceWith(newOption); + }); + + it('should reset value and display value when reset button is clicked', () => { + component.setValue(['option1', 'option2']); + fixture.detectChanges(); + expect(component.selectedOptions).toEqual(['option1', 'option2']); + spyOn(component.context, 'update'); + spyOn(component.displayValue$, 'next'); + const clearBtn: HTMLButtonElement = fixture.debugElement.query(By.css('[data-automation-id="adf-search-chip-autocomplete-btn-clear"]')).nativeElement; + clearBtn.click(); + + expect(component.context.queryFragments[component.id]).toBe(''); + expect(component.context.update).toHaveBeenCalled(); + expect(component.selectedOptions).toEqual( [] ); + expect(component.displayValue$.next).toHaveBeenCalledWith(''); + }); + + it('should correctly compose the search query', () => { + spyOn(component.context, 'update'); + addNewOption('option2'); + addNewOption('option1'); + const applyBtn: HTMLButtonElement = fixture.debugElement.query(By.css('[data-automation-id="adf-search-chip-autocomplete-btn-apply"]')).nativeElement; + applyBtn.click(); + fixture.detectChanges(); + + expect(component.context.update).toHaveBeenCalled(); + expect(component.context.queryFragments[component.id]).toBe('test: "option2" OR test: "option1"'); + }); +}); diff --git a/lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.ts b/lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.ts new file mode 100644 index 0000000000..09eb499474 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component.ts @@ -0,0 +1,108 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * 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, ViewEncapsulation, OnInit } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; +import { SearchWidget } from '../../models/search-widget.interface'; +import { SearchWidgetSettings } from '../../models/search-widget-settings.interface'; +import { SearchQueryBuilderService } from '../../services/search-query-builder.service'; +import { SearchFilterList } from '../../models/search-filter-list.model'; +import { TagService } from '../../../tag/services/tag.service'; + +@Component({ + selector: 'adf-search-filter-autocomplete-chips', + templateUrl: './search-filter-autocomplete-chips.component.html', + encapsulation: ViewEncapsulation.None +}) +export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnInit { + id: string; + settings?: SearchWidgetSettings; + context?: SearchQueryBuilderService; + options: SearchFilterList; + startValue: string[] = null; + displayValue$ = new Subject(); + + private resetSubject$ = new Subject(); + reset$: Observable = this.resetSubject$.asObservable(); + autocompleteOptions: string[] = []; + selectedOptions: string[] = []; + enableChangeUpdate: boolean; + + constructor( private tagService: TagService ) { + this.options = new SearchFilterList(); + } + + ngOnInit() { + if (this.settings) { + this.setOptions(); + if (this.startValue) { + this.setValue(this.startValue); + } + this.enableChangeUpdate = this.settings.allowUpdateOnChange ?? true; + } + } + + reset() { + this.selectedOptions = []; + this.resetSubject$.next(); + this.updateQuery(); + } + + submitValues() { + this.updateQuery(); + } + + hasValidValue(): boolean { + return !!this.selectedOptions; + } + + getCurrentValue(): string[]{ + return this.selectedOptions; + } + + onOptionsChange(selectedOptions: string[]) { + this.selectedOptions = selectedOptions; + if (this.enableChangeUpdate) { + this.updateQuery(); + this.context.update(); + } + } + + setValue(value: string[]) { + this.selectedOptions = value; + this.displayValue$.next(this.selectedOptions.join(', ')); + this.submitValues(); + } + + private updateQuery() { + this.displayValue$.next(this.selectedOptions.join(', ')); + if (this.context && this.settings && this.settings.field) { + this.context.queryFragments[this.id] = this.selectedOptions.map(val => `${this.settings.field}: "${val}"`).join(' OR '); + this.context.update(); + } + } + + private setOptions() { + if (this.settings.field === 'TAG') { + this.tagService.getAllTheTags().subscribe(res => { + this.autocompleteOptions = res.list.entries.map(tag => tag.entry.tag); + }); + } else { + this.autocompleteOptions = this.settings.options; + } + } +} diff --git a/lib/content-services/src/lib/search/models/search-widget-settings.interface.ts b/lib/content-services/src/lib/search/models/search-widget-settings.interface.ts index 5893754ed4..4885fd2bbf 100644 --- a/lib/content-services/src/lib/search/models/search-widget-settings.interface.ts +++ b/lib/content-services/src/lib/search/models/search-widget-settings.interface.ts @@ -25,6 +25,8 @@ export interface SearchWidgetSettings { unit?: string; /* describes query format */ format?: string; + /* allow the user to search only within predefined options */ + allowOnlyPredefinedValues?: boolean; [indexer: string]: any; } diff --git a/lib/content-services/src/lib/search/public-api.ts b/lib/content-services/src/lib/search/public-api.ts index 9c799db124..9fa4f9e752 100644 --- a/lib/content-services/src/lib/search/public-api.ts +++ b/lib/content-services/src/lib/search/public-api.ts @@ -63,5 +63,7 @@ export * from './components/search-facet-field/search-facet-field.component'; export * from './components/search-chip-input/search-chip-input.component'; export * from './components/search-logical-filter/search-logical-filter.component'; export * from './components/reset-search.directive'; +export * from './components/search-chip-autocomplete-input/search-chip-autocomplete-input.component'; +export * from './components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component'; export * from './search.module'; diff --git a/lib/content-services/src/lib/search/search.module.ts b/lib/content-services/src/lib/search/search.module.ts index 0cc7f1ab0b..b28271cb9d 100644 --- a/lib/content-services/src/lib/search/search.module.ts +++ b/lib/content-services/src/lib/search/search.module.ts @@ -19,6 +19,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MaterialModule } from '../material.module'; +import { ContentPipeModule } from '../pipes/content-pipe.module'; import { CoreModule, SearchTextModule } from '@alfresco/adf-core'; @@ -29,6 +30,8 @@ import { SearchWidgetContainerComponent } from './components/search-widget-conta import { SearchFilterComponent } from './components/search-filter/search-filter.component'; import { SearchChipListComponent } from './components/search-chip-list/search-chip-list.component'; import { SearchTextComponent } from './components/search-text/search-text.component'; +import { SearchChipAutocompleteInputComponent } from './components/search-chip-autocomplete-input/search-chip-autocomplete-input.component'; +import { SearchFilterAutocompleteChipsComponent } from './components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component'; import { SearchRadioComponent } from './components/search-radio/search-radio.component'; import { SearchSliderComponent } from './components/search-slider/search-slider.component'; import { SearchNumberRangeComponent } from './components/search-number-range/search-number-range.component'; @@ -53,6 +56,7 @@ import { ResetSearchDirective } from './components/reset-search.directive'; @NgModule({ imports: [ CommonModule, + ContentPipeModule, FormsModule, ReactiveFormsModule, MaterialModule, @@ -67,6 +71,8 @@ import { ResetSearchDirective } from './components/reset-search.directive'; SearchChipListComponent, SearchWidgetContainerComponent, SearchTextComponent, + SearchChipAutocompleteInputComponent, + SearchFilterAutocompleteChipsComponent, SearchRadioComponent, SearchSliderComponent, SearchNumberRangeComponent, @@ -94,6 +100,8 @@ import { ResetSearchDirective } from './components/reset-search.directive'; SearchChipListComponent, SearchWidgetContainerComponent, SearchTextComponent, + SearchChipAutocompleteInputComponent, + SearchFilterAutocompleteChipsComponent, SearchRadioComponent, SearchSliderComponent, SearchNumberRangeComponent, diff --git a/lib/content-services/src/lib/search/services/search-filter.service.ts b/lib/content-services/src/lib/search/services/search-filter.service.ts index 70b5fa1444..9ffe6cc671 100644 --- a/lib/content-services/src/lib/search/services/search-filter.service.ts +++ b/lib/content-services/src/lib/search/services/search-filter.service.ts @@ -24,6 +24,7 @@ import { SearchCheckListComponent } from '../components/search-check-list/search import { SearchDateRangeComponent } from '../components/search-date-range/search-date-range.component'; import { SearchDatetimeRangeComponent } from '../components/search-datetime-range/search-datetime-range.component'; import { SearchLogicalFilterComponent } from '../components/search-logical-filter/search-logical-filter.component'; +import { SearchFilterAutocompleteChipsComponent } from '../components/search-filter-autocomplete-chips/search-filter-autocomplete-chips.component'; @Injectable({ providedIn: 'root' @@ -41,7 +42,8 @@ export class SearchFilterService { 'check-list': SearchCheckListComponent, 'date-range': SearchDateRangeComponent, 'datetime-range': SearchDatetimeRangeComponent, - 'logical-filter': SearchLogicalFilterComponent + 'logical-filter': SearchLogicalFilterComponent, + 'autocomplete-chips': SearchFilterAutocompleteChipsComponent }; }