diff --git a/docs/README.md b/docs/README.md index cc9081ec5e..3e3cdb7320 100644 --- a/docs/README.md +++ b/docs/README.md @@ -297,6 +297,7 @@ for more information about installing and using the source code. | [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) | | [Search Logical Filter component](content-services/components/search-logical-filter.component.md) | Displays 3 chip inputs each representing different logical condition for search query. | [Source](../lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.ts) | +| [Search Properties component](content-services/components/search-properties.component.md) | Allows to search by file size and type.| [Source](../lib/content-services/src/lib/search/components/search-properties/search-properties.component.ts) | | [Search number range component](content-services/components/search-number-range.component.md) | Implements a number range widget for the Search Filter component. | [Source](../lib/content-services/src/lib/search/components/search-number-range/search-number-range.component.ts) | | [Search radio component](content-services/components/search-radio.component.md) | Implements a radio button list widget for the Search Filter component. | [Source](../lib/content-services/src/lib/search/components/search-radio/search-radio.component.ts) | | [Search slider component](content-services/components/search-slider.component.md) | Implements a numeric slider widget for the Search Filter component. | [Source](../lib/content-services/src/lib/search/components/search-slider/search-slider.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 index c39dbb6ea2..e66560273e 100644 --- a/docs/content-services/components/search-chip-autocomplete-input.component.md +++ b/docs/content-services/components/search-chip-autocomplete-input.component.md @@ -29,6 +29,10 @@ Represents an input with autocomplete options. | 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 | +| placeholder | string | 'SEARCH.FILTER.ACTIONS.ADD_OPTION' | Placeholder which should be displayed in input. | +| compareOption | (option1: string, option2: string) => boolean | | Function which is used to selected options with all options so it allows to detect which options are already selected. | +| formatChipValue | (option: string) => string | | Function which is used to format custom typed options. | +| filter | (options: string[], value: string) => string[] | | Function which is used to filter out possibile options from hint. By default it checks if option includes typed value and is case insensitive. | ### Events diff --git a/docs/content-services/components/search-properties.component.md b/docs/content-services/components/search-properties.component.md new file mode 100644 index 0000000000..24865a5be6 --- /dev/null +++ b/docs/content-services/components/search-properties.component.md @@ -0,0 +1,61 @@ +--- +Title: Search Properties component +Added: v6.2.0 +Status: Active +Last reviewed: 2023-07-13 +--- + +# [Search Properties component](../../../lib/content-services/src/lib/search/components/search-properties/search-properties.component.ts "Defined in search-properties.component.ts") + +Allows to search by file size and type. + +![Search Properties](../../docassets/images/search-properties.png) + +## Basic usage + +```json +{ + "id": "properties", + "name": "Properties", + "enabled": true, + "component": { + "selector": "properties", + "settings": { + "field": "content.size,cm:name", + "fileExtensions": [ + "3g2", "3gp", "acp", "aep", "ai", "aiff", "apk", "arw", "avi", "bin", "bmp", "cgm", "class", "cr2", + "css", "csv", "dita", "dng", "doc", "docm", "docx", "dotm","dwg", "dwt", "eps", "flac", "flv", "fm", + "fodg", "gif", "gtar", "gz", "htm", "html", "icns", "ics", "ief", "indd", "jar", "java", "jp2", "jpeg", + "jpg", "js", "json", "jsp", "m4v", "man", "md", "mov", "mp3", "mp4", "mpeg", "mpp", "mrw", "msg", "nef", + "numbers", "odb", "odf", "odg", "odi", "odm", "odp", "ods", "odt", "oga", "ogg", "ogv", "ogx", "orf", + "ott", "pages", "pbm", "pdf", "pef", "pgm", "pmd", "png", "pnm", "pot", "potx", "ppam", "ppj", "pps", + "ppsm", "ppt", "pptm", "pptx", "ps", "psd", "rad", "raf", "rar", "rgb", "rss", "rtf", "rw2", "rwl", + "sda", "sdc", "sdd", "sdp", "sds", "sdw", "sgi", "sgl", "sgml", "sh", "sldm", "smf", "stw", "svg", + "swf", "sxi", "tar", "tex", "texi", "tif", "tiff", "ts", "tsv", "txt", "vsd", "vsdm", "vsdx", "vssm", + "vstm", "vstx", "wav", "webm", "wma", "wmv", "wpd", "wrl", "x3f", "xdp", "xhtml", "xla", "xlam", "xls", + "xlsb", "xlsm", "xlsx", "xltm", "xml", "xpm", "xwd", "z", "zip" + ] + } + } +} +``` + +### Settings + +| Name | Type | Description | +|----------------|----------|-------------------------------------------------------------------------------------------| +| field | string | Field/fields to apply the query to. First field for size, second for name. Required value | +| fileExtensions | string[] | List of preconfigured hints for extensions. | + + +## 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 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) diff --git a/docs/docassets/images/search-properties.png b/docs/docassets/images/search-properties.png new file mode 100644 index 0000000000..df99fdd28c Binary files /dev/null and b/docs/docassets/images/search-properties.png differ diff --git a/docs/versionIndex.md b/docs/versionIndex.md index 616d443a37..6b7614fc30 100644 --- a/docs/versionIndex.md +++ b/docs/versionIndex.md @@ -12,6 +12,7 @@ backend services have been tested with each released version of ADF. ## Versions +- [v6.2.0](#v620) - [v6.1.0](#v610) - [v6.0.0](#v600) - [v5.1.0](#v510) @@ -42,6 +43,14 @@ backend services have been tested with each released version of ADF. - [v2.1.0](#v210) - [v2.0.0](#v200) +## v6.2.0 + + + +- [Search Properties component](content-services/components/search-properties.component.md) + + + ## v6.1.0 diff --git a/lib/content-services/src/lib/i18n/en.json b/lib/content-services/src/lib/i18n/en.json index 511c1f9032..30eb3e166d 100644 --- a/lib/content-services/src/lib/i18n/en.json +++ b/lib/content-services/src/lib/i18n/en.json @@ -413,6 +413,22 @@ "EXCLUDE": "EXCLUDE", "EXCLUDE_LABEL": "EXCLUDE these words", "EXCLUDE_HINT": "Results will exclude matches with these words" + }, + "SEARCH_PROPERTIES": { + "FILE_SIZE": "File Size", + "FILE_SIZE_PLACEHOLDER": "Enter file size", + "FILE_SIZE_OPERATOR": { + "AT_LEAST": "At Least", + "AT_MOST": "At Most", + "EXACTLY": "Exactly" + }, + "FILE_SIZE_UNIT_ABBREVIATION": { + "KB": "KB", + "MB": "MB", + "GB": "GB" + }, + "FILE_TYPE": "File Type", + "FILE_TYPE_PLACEHOLDER": "Add file type" } }, "PERMISSION": { diff --git a/lib/content-services/src/lib/pipes/is-included.pipe.ts b/lib/content-services/src/lib/pipes/is-included.pipe.ts index 114b4592d3..ca5ded4d19 100644 --- a/lib/content-services/src/lib/pipes/is-included.pipe.ts +++ b/lib/content-services/src/lib/pipes/is-included.pipe.ts @@ -21,7 +21,7 @@ import { Pipe, PipeTransform } from '@angular/core'; name: 'adfIsIncluded' }) export class IsIncludedPipe implements PipeTransform { - transform(value: T, array: T[]): boolean { - return array.includes(value); + transform(value: T, array: T[], compare?: (value1: T, value2: T) => boolean): boolean { + return compare ? array.some((arrayValue) => compare(value, arrayValue)) : array.includes(value); } } 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 index 81e088691f..d0de2f038d 100644 --- 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 @@ -10,19 +10,22 @@ + [attr.aria-label]="placeholder | translate" + (matChipInputTokenEnd)="add($event)" + (blur)="activeAnyOption = false" + data-automation-id="adf-search-chip-autocomplete-input"> - - + + {{option}} 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 index d8be21e38d..5b50e70d0e 100644 --- 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 @@ -22,6 +22,7 @@ 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'; +import { DebugElement } from '@angular/core'; describe('SearchChipAutocompleteInputComponent', () => { let component: SearchChipAutocompleteInputComponent; @@ -44,16 +45,20 @@ describe('SearchChipAutocompleteInputComponent', () => { component.autocompleteOptions = ['option1', 'option2']; }); + function getInput(): HTMLInputElement { + return fixture.debugElement.query(By.css('input')).nativeElement; + } + 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')); + const inputElement = getInput(); + inputElement.dispatchEvent(new Event('focusin')); + inputElement.value = value; + inputElement.dispatchEvent(new Event('input')); fixture.detectChanges(); } function addNewOption(value: string) { - const inputElement = fixture.debugElement.query(By.css('input')).nativeElement; + const inputElement = getInput(); inputElement.value = value; fixture.detectChanges(); inputElement.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 13})); @@ -68,6 +73,14 @@ describe('SearchChipAutocompleteInputComponent', () => { return fixture.debugElement.queryAll(By.css('mat-chip span')).map((chip) => chip.nativeElement)[index].innerText; } + function getOptionElements(): DebugElement[] { + return fixture.debugElement.queryAll(By.css('mat-option')); + } + + function getAddedOptionElements(): DebugElement[] { + return fixture.debugElement.queryAll(By.css('.adf-autocomplete-added-option')); + } + it('should add new option only if value is predefined when allowOnlyPredefinedValues = true', () => { addNewOption('test'); addNewOption('option1'); @@ -83,15 +96,25 @@ describe('SearchChipAutocompleteInputComponent', () => { expect(getChipValue(0)).toBe('test'); }); + it('should add new formatted option based on formatChipValue', () => { + component.allowOnlyPredefinedValues = false; + const option = 'abc'; + component.formatChipValue = (value) => value.replace('.', ''); + + addNewOption(`.${option}`); + expect(getChipList().length).toBe(1); + expect(getChipValue(0)).toBe(option); + }); + 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'); + const matOptions = getOptionElements(); expect(matOptions.length).toBe(2); - const optionToClick = matOptions[0] as HTMLElement; + const optionToClick = matOptions[0].nativeElement as HTMLElement; optionToClick.click(); expect(optionsChangedSpy).toHaveBeenCalledOnceWith(['option1']); @@ -103,7 +126,7 @@ describe('SearchChipAutocompleteInputComponent', () => { addNewOption('option1'); enterNewInputValue('op'); - const addedOptions = fixture.debugElement.queryAll(By.css('.adf-autocomplete-added-option')); + const addedOptions = getAddedOptionElements(); await fixture.whenStable(); @@ -111,12 +134,24 @@ describe('SearchChipAutocompleteInputComponent', () => { expect(addedOptions.length).toBe(1); }); + it('should apply class to already selected options based on custom compareOption function', async () => { + component.allowOnlyPredefinedValues = false; + component.autocompleteOptions = ['.test1', 'test3', '.test2', 'test1.']; + component.compareOption = (option1, option2) => option1.split('.')[1] === option2; + + fixture.detectChanges(); + addNewOption('test1'); + enterNewInputValue('t'); + + const addedOptions = getAddedOptionElements(); + await fixture.whenStable(); + 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); + expect(getOptionElements().length).toBe(15); }); it('should not add a value if same value has already been added', () => { @@ -127,14 +162,19 @@ describe('SearchChipAutocompleteInputComponent', () => { it('should show autocomplete list if similar predefined values exists', () => { enterNewInputValue('op'); - const matOptions = document.querySelectorAll('mat-option'); - expect(matOptions.length).toBe(2); + expect(getOptionElements().length).toBe(2); + }); + + it('should show autocomplete list based on custom filtering', () => { + component.autocompleteOptions = ['.test1', 'test1', 'test1.', '.test2', '.test12']; + component.filter = (options, value) => options.filter((option) => option.split('.')[1] === value); + enterNewInputValue('test1'); + expect(getOptionElements().length).toBe(1); }); 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); + expect(getOptionElements().length).toBe(0); }); it('should emit new value when selected options changed', () => { @@ -146,11 +186,21 @@ describe('SearchChipAutocompleteInputComponent', () => { }); it('should clear the input after a new value is added', () => { - const input = fixture.debugElement.query(By.css('input')).nativeElement; + const input = getInput(); addNewOption('option1'); expect(input.value).toBe(''); }); + it('should use correct default placeholder for input', () => { + expect(getInput().placeholder).toBe('SEARCH.FILTER.ACTIONS.ADD_OPTION'); + }); + + it('should use placeholder for input passed as component input', () => { + component.placeholder = 'Some placeholder'; + fixture.detectChanges(); + expect(getInput().placeholder).toBe(component.placeholder); + }); + it('should reset all options when onReset$ event is emitted', () => { addNewOption('option1'); addNewOption('option2'); 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 index d03a653a7c..ac69518f69 100644 --- 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 @@ -21,7 +21,7 @@ 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'; +import { map, startWith, takeUntil, tap } from 'rxjs/operators'; @Component({ selector: 'adf-search-chip-autocomplete-input', @@ -42,6 +42,21 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy { @Input() allowOnlyPredefinedValues = true; + @Input() + placeholder = 'SEARCH.FILTER.ACTIONS.ADD_OPTION'; + + @Input() + compareOption?: (option1: string, option2: string) => boolean; + + @Input() + formatChipValue?: (option: string) => string; + + @Input() + filter = (options: string[], value: string): string[] => { + const filterValue = value.toLowerCase(); + return options.filter(option => option.toLowerCase().includes(filterValue)); + }; + @Output() optionsChanged: EventEmitter = new EventEmitter(); @@ -50,11 +65,17 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy { filteredOptions$: Observable; selectedOptions: string[] = []; private onDestroy$ = new Subject(); + private _activeAnyOption = false; + + set activeAnyOption(active: boolean) { + this._activeAnyOption = active; + } constructor() { this.filteredOptions$ = this.formCtrl.valueChanges.pipe( startWith(null), - map((value: string | null) => (value ? this.filter(value) : [])) + tap(() => this.activeAnyOption = false), + map((value: string | null) => (value ? this.filter(this.autocompleteOptions, value).slice(0, 15) : [])) ); } @@ -68,13 +89,18 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy { } add(event: MatChipInputEvent) { - const value = (event.value || '').trim(); + if (!this._activeAnyOption) { + let value = (event.value || '').trim(); + if (this.formatChipValue) { + value = this.formatChipValue(value); + } - if (value && this.isExists(value) && !this.isAdded(value)) { - this.selectedOptions.push(value); - this.optionsChanged.emit(this.selectedOptions); - event.chipInput.clear(); - this.formCtrl.setValue(null); + if (value && this.isExists(value) && !this.isAdded(value)) { + this.selectedOptions.push(value); + this.optionsChanged.emit(this.selectedOptions); + event.chipInput.clear(); + this.formCtrl.setValue(null); + } } } @@ -96,11 +122,6 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy { } } - 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); } diff --git a/lib/content-services/src/lib/search/components/search-filter-chips/search-widget-chip/search-widget-chip.component.html b/lib/content-services/src/lib/search/components/search-filter-chips/search-widget-chip/search-widget-chip.component.html index 382f581d7d..779e40803a 100644 --- a/lib/content-services/src/lib/search/components/search-filter-chips/search-widget-chip/search-widget-chip.component.html +++ b/lib/content-services/src/lib/search/components/search-filter-chips/search-widget-chip/search-widget-chip.component.html @@ -19,7 +19,7 @@ {{ chipIcon }} - +
+ + + + + {{ fileSizeOperator | translate }} + + + + + + + + {{ fileSizeUnit.abbreviation | translate }} + + + +

{{ 'SEARCH.SEARCH_PROPERTIES.FILE_TYPE' | translate }}

+ + + diff --git a/lib/content-services/src/lib/search/components/search-properties/search-properties.component.scss b/lib/content-services/src/lib/search/components/search-properties/search-properties.component.scss new file mode 100644 index 0000000000..c9f27a09c1 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-properties/search-properties.component.scss @@ -0,0 +1,89 @@ +adf-search-properties { + .adf-search-properties-file-size-label { + display: block; + margin-top: 4px; + } + + input { + height: 25px; + border: 1px solid var(--adf-theme-mat-grey-color-a400); + border-radius: 5px; + margin-top: 5px; + padding: 5px; + font-size: 14px; + color: var(--adf-theme-foreground-text-color); + margin-left: 8px; + margin-right: 8px; + } + + .adf-search-properties-file-size-operator, + .adf-search-properties-file-size-unit { + .mat-form-field-infix { + border: 1px solid var(--adf-theme-mat-grey-color-a400); + border-radius: 5px; + padding: 9px; + } + + &.mat-focused { + .mat-form-field-infix { + outline: 2px auto -webkit-focus-ring-color; + } + } + } + + .mat-form-field-underline { + display: none; + } + + .adf-search-properties-file-size-unit { + width: 78px; + } + + adf-search-chip-autocomplete-input { + display: block; + + .mat-form-field-outline { + .mat-form-field-outline { + &-start, + &-end { + border-top: 1px solid var(--adf-theme-mat-grey-color-a400); + border-bottom: 1px solid var(--adf-theme-mat-grey-color-a400); + } + + &-start { + border-left: 1px solid var(--adf-theme-mat-grey-color-a400); + } + + &-end { + border-right: 1px solid var(--adf-theme-mat-grey-color-a400); + } + } + } + + .mat-focused { + .mat-form-field-outline { + outline: 2px auto -webkit-focus-ring-color; + } + } + + .mat-form-field-appearance-outline { + .mat-form-field-outline-thick { + .mat-form-field-outline { + &-start, + &-end { + border-width: 1px; + } + } + } + } + } + + .adf-search-properties-file-type-label { + margin-top: 10px; + margin-bottom: 6px; + } +} + +.adf-search-filter-chip-menu-panel-properties { + max-width: unset; +} diff --git a/lib/content-services/src/lib/search/components/search-properties/search-properties.component.spec.ts b/lib/content-services/src/lib/search/components/search-properties/search-properties.component.spec.ts new file mode 100644 index 0000000000..b561e1eb94 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-properties/search-properties.component.spec.ts @@ -0,0 +1,432 @@ +/*! + * @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 { SearchPropertiesComponent } from './search-properties.component'; +import { ContentTestingModule } from '../../../testing/content.testing.module'; +import { TranslateModule } from '@ngx-translate/core'; +import { By } from '@angular/platform-browser'; +import { MatOption } from '@angular/material/core'; +import { SearchChipAutocompleteInputComponent, SearchQueryBuilderService } from '@alfresco/adf-content-services'; +import { FileSizeUnit } from './file-size-unit.enum'; +import { FileSizeOperator } from './file-size-operator.enum'; +import { SearchProperties } from './search-properties'; + +describe('SearchPropertiesComponent', () => { + let component: SearchPropertiesComponent; + let fixture: ComponentFixture; + + const clickFileSizeOperatorsSelect = () => { + fixture.debugElement.query(By.css('[data-automation-id=adf-search-properties-file-size-operator-select]')).nativeElement.click(); + fixture.detectChanges(); + }; + + const clickFileSizeUnitsSelect = () => { + fixture.debugElement.query(By.css('[data-automation-id=adf-search-properties-file-size-unit-select]')).nativeElement.click(); + fixture.detectChanges(); + }; + + const getSelectOptions = () => fixture.debugElement.queryAll(By.directive(MatOption)); + + const getFileSizeInput = () => fixture.debugElement.query(By.css('#adf-search-properties-file-size')); + + const typeInFileSizeInput = (value = '321', data = '1'): HTMLInputElement => { + const fileSizeElement = getFileSizeInput(); + fileSizeElement.nativeElement.value = value; + const event = new InputEvent('input', { + data + }); + spyOnProperty(event, 'target').and.returnValue(fileSizeElement.nativeElement); + fileSizeElement.triggerEventHandler('input', event); + fixture.detectChanges(); + return fileSizeElement.nativeElement; + }; + + const getSearchChipAutocompleteInputComponent = (): SearchChipAutocompleteInputComponent => fixture.debugElement.query( + By.directive(SearchChipAutocompleteInputComponent) + ).componentInstance; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ SearchPropertiesComponent ], + imports: [ ContentTestingModule, TranslateModule.forRoot() ] + }).compileComponents(); + + fixture = TestBed.createComponent(SearchPropertiesComponent); + component = fixture.componentInstance; + }); + + describe('File size', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should display correct operators for file size after opening select', () => { + clickFileSizeOperatorsSelect(); + + const options = getSelectOptions(); + expect(options.length).toBe(3); + expect(options[0].nativeElement.innerText).toBe('SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_LEAST'); + expect(options[1].nativeElement.innerText).toBe('SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_MOST'); + expect(options[2].nativeElement.innerText).toBe('SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.EXACTLY'); + }); + + it('should display correct units for file size after opening select', () => { + clickFileSizeUnitsSelect(); + + const options = getSelectOptions(); + expect(options.length).toBe(3); + expect(options[0].nativeElement.innerText).toBe('SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.KB'); + expect(options[1].nativeElement.innerText).toBe('SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.MB'); + expect(options[2].nativeElement.innerText).toBe('SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.GB'); + }); + + it('should allow type digit value in file size input', () => { + const fileSizeElement = typeInFileSizeInput(); + expect(fileSizeElement.value).toBe('321'); + }); + + it('should allow type decimal value in file size input', () => { + const value = '3.21'; + const fileSizeElement = typeInFileSizeInput(value); + expect(fileSizeElement.value).toBe(value); + }); + + it('should allow clear value in file size input', () => { + const value = ''; + const fileSizeElement = typeInFileSizeInput(value); + expect(fileSizeElement.value).toBe(value); + }); + + it('should not allow type non digit value in file size input', () => { + const fileSizeElement = typeInFileSizeInput('e'); + expect(fileSizeElement.value).toBe(''); + }); + + it('should call preventIncorrectNumberCharacters on keydown event for file size input', () => { + spyOn(component, 'preventIncorrectNumberCharacters'); + const event = new KeyboardEvent('keydown'); + const fileSizeElement = getFileSizeInput(); + + fileSizeElement.triggerEventHandler('keydown', event); + expect(component.preventIncorrectNumberCharacters).toHaveBeenCalledWith(event); + }); + }); + + describe('File type', () => { + let searchChipAutocompleteInputComponent: SearchChipAutocompleteInputComponent; + + beforeEach(() => { + fixture.detectChanges(); + searchChipAutocompleteInputComponent = getSearchChipAutocompleteInputComponent(); + }); + + it('should set autocompleteOptions for SearchChipAutocompleteInputComponent from settings', () => { + component.settings = { + field: 'field', + fileExtensions: ['pdf', 'doc', 'txt'] + }; + + fixture.detectChanges(); + expect(searchChipAutocompleteInputComponent.autocompleteOptions).toBe(component.settings.fileExtensions); + }); + + it('should set onReset$ for SearchChipAutocompleteInputComponent to correct value', () => { + expect(searchChipAutocompleteInputComponent.onReset$).toBe(component.reset$); + }); + + it('should set allowOnlyPredefinedValues for SearchChipAutocompleteInputComponent to false', () => { + expect(searchChipAutocompleteInputComponent.allowOnlyPredefinedValues).toBeFalse(); + }); + + it('should compare file extensions case insensitive after calling compareOption on SearchChipAutocompleteInputComponent', () => { + const option1 = 'pdf'; + const option2 = 'PdF'; + expect(searchChipAutocompleteInputComponent.compareOption(option1, option2)).toBeTrue(); + expect(searchChipAutocompleteInputComponent.compareOption(option1, `${option2}1`)).toBeFalse(); + }); + + it('should remove preceding dot after calling formatChipValue on SearchChipAutocompleteInputComponent', () => { + const extension = 'pdf'; + expect(searchChipAutocompleteInputComponent.formatChipValue(`.${extension}`)).toBe(extension); + expect(searchChipAutocompleteInputComponent.formatChipValue(extension)).toBe(extension); + }); + + it('should filter file extensions case insensitive without dots after calling filter on SearchChipAutocompleteInputComponent', () => { + const extensions = ['pdf', 'jpg', 'txt', 'png']; + const searchValue = 'p'; + + expect(searchChipAutocompleteInputComponent.filter(extensions, searchValue)).toEqual(['pdf', 'jpg', 'png']); + expect(searchChipAutocompleteInputComponent.filter(extensions, `.${searchValue}`)).toEqual(['pdf', 'png']); + }); + + it('should set placeholder for SearchChipAutocompleteInputComponent to correct value', () => { + expect(searchChipAutocompleteInputComponent.placeholder).toBe('SEARCH.SEARCH_PROPERTIES.FILE_TYPE'); + }); + }); + + describe('submitValues', () => { + const sizeField = 'content.size'; + const nameField = 'cm:name'; + + beforeEach(() => { + component.id = 'properties'; + component.settings = { + field: `${sizeField},${nameField}` + }; + component.context = TestBed.inject(SearchQueryBuilderService); + fixture.detectChanges(); + spyOn(component.displayValue$, 'next'); + spyOn(component.context, 'update'); + }); + + it('should not search when settings is not set', () => { + component.settings = undefined; + typeInFileSizeInput(); + + component.submitValues(); + expect(component.displayValue$.next).not.toHaveBeenCalled(); + expect(component.context.queryFragments[component.id]).toBeUndefined(); + expect(component.context.update).not.toHaveBeenCalled(); + }); + + it('should not search when context is not set', () => { + component.context = undefined; + typeInFileSizeInput(); + + component.submitValues(); + expect(component.displayValue$.next).not.toHaveBeenCalled(); + }); + + it('should set empty search when nothing is set', () => { + component.submitValues(); + expect(component.displayValue$.next).toHaveBeenCalledWith(''); + expect(component.context.queryFragments[component.id]).toBe(''); + expect(component.context.update).toHaveBeenCalled(); + }); + + it('should search by at least KB by default when any size is typed', () => { + typeInFileSizeInput(); + + component.submitValues(); + expect(component.displayValue$.next).toHaveBeenCalledWith('SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_LEAST 321 SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.KB'); + expect(component.context.queryFragments[component.id]).toBe(`${sizeField}:[328704 TO MAX]`); + expect(component.context.update).toHaveBeenCalled(); + }); + + it('should search by at most MB after selecting proper options', () => { + typeInFileSizeInput(); + clickFileSizeOperatorsSelect(); + getSelectOptions()[1].nativeElement.click(); + fixture.detectChanges(); + clickFileSizeUnitsSelect(); + getSelectOptions()[1].nativeElement.click(); + fixture.detectChanges(); + + component.submitValues(); + expect(component.displayValue$.next).toHaveBeenCalledWith('SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_MOST 321 SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.MB'); + expect(component.context.queryFragments[component.id]).toBe(`${sizeField}:[0 TO 336592896]`); + expect(component.context.update).toHaveBeenCalled(); + }); + + it('should search by exactly GB after selecting proper options', () => { + typeInFileSizeInput(); + clickFileSizeOperatorsSelect(); + getSelectOptions()[2].nativeElement.click(); + fixture.detectChanges(); + clickFileSizeUnitsSelect(); + getSelectOptions()[2].nativeElement.click(); + fixture.detectChanges(); + + component.submitValues(); + expect(component.displayValue$.next).toHaveBeenCalledWith('SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.EXACTLY 321 SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.GB'); + expect(component.context.queryFragments[component.id]).toBe(`${sizeField}:[344671125504 TO 344671125504]`); + expect(component.context.update).toHaveBeenCalled(); + }); + + it('should search by single file type', () => { + const extension = 'pdf'; + getSearchChipAutocompleteInputComponent().optionsChanged.emit([extension]); + + component.submitValues(); + expect(component.displayValue$.next).toHaveBeenCalledWith(extension); + expect(component.context.queryFragments[component.id]).toBe(`${nameField}:("*.${extension}")`); + expect(component.context.update).toHaveBeenCalled(); + }); + + it('should search by multiple file types', () => { + getSearchChipAutocompleteInputComponent().optionsChanged.emit(['pdf', 'txt']); + + component.submitValues(); + expect(component.displayValue$.next).toHaveBeenCalledWith('pdf, txt'); + expect(component.context.queryFragments[component.id]).toBe(`${nameField}:("*.pdf" OR "*.txt")`); + expect(component.context.update).toHaveBeenCalled(); + }); + + it('should search by file size and type', () => { + typeInFileSizeInput(); + getSearchChipAutocompleteInputComponent().optionsChanged.emit(['pdf', 'txt']); + + component.submitValues(); + expect(component.displayValue$.next).toHaveBeenCalledWith('SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_LEAST 321 SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.KB, pdf, txt'); + expect(component.context.queryFragments[component.id]).toBe(`${sizeField}:[328704 TO MAX] AND ${nameField}:("*.pdf" OR "*.txt")`); + expect(component.context.update).toHaveBeenCalled(); + }); + }); + + describe('hasValidValue', () => { + it('should return true', () => { + expect(component.hasValidValue()).toBeTrue(); + }); + }); + + describe('getCurrentValue', () => { + it('should return correct value when nothing changed', () => { + expect(component.getCurrentValue()).toEqual({ + fileSizeCondition: { + fileSize: null, + fileSizeUnit: FileSizeUnit.KB, + fileSizeOperator: FileSizeOperator.AT_LEAST + }, + fileExtensions: undefined + }); + }); + + it('should return correct value when inputs changed', () => { + fixture.detectChanges(); + typeInFileSizeInput(); + clickFileSizeOperatorsSelect(); + getSelectOptions()[1].nativeElement.click(); + fixture.detectChanges(); + clickFileSizeUnitsSelect(); + getSelectOptions()[1].nativeElement.click(); + fixture.detectChanges(); + const extensions = ['pdf', 'txt']; + getSearchChipAutocompleteInputComponent().optionsChanged.emit(extensions); + + expect(component.getCurrentValue()).toEqual({ + fileSizeCondition: { + fileSize: 321, + fileSizeUnit: FileSizeUnit.MB, + fileSizeOperator: FileSizeOperator.AT_MOST + }, + fileExtensions: extensions + }); + }); + }); + + describe('reset', () => { + let searchChipAutocompleteInputComponent: SearchChipAutocompleteInputComponent; + + beforeEach(() => { + fixture.detectChanges(); + typeInFileSizeInput(); + clickFileSizeOperatorsSelect(); + getSelectOptions()[1].nativeElement.click(); + fixture.detectChanges(); + clickFileSizeUnitsSelect(); + getSelectOptions()[1].nativeElement.click(); + fixture.detectChanges(); + searchChipAutocompleteInputComponent = getSearchChipAutocompleteInputComponent(); + searchChipAutocompleteInputComponent.optionsChanged.emit(['pdf', 'txt']); + }); + + it('should reset form', () => { + spyOn(searchChipAutocompleteInputComponent.optionsChanged, 'emit'); + + component.reset(); + fixture.detectChanges(); + expect(component.form.value).toEqual({ + fileSize: null, + fileSizeUnit: FileSizeUnit.KB, + fileSizeOperator: FileSizeOperator.AT_LEAST + }); + expect(searchChipAutocompleteInputComponent.optionsChanged.emit).toHaveBeenCalledWith([]); + }); + + it('should display empty value', () => { + spyOn(component.displayValue$, 'next'); + + component.reset(); + expect(component.displayValue$.next).toHaveBeenCalledWith(''); + }); + }); + + describe('setValue', () => { + let searchProperties: SearchProperties; + + beforeEach(() => { + searchProperties = { + fileSizeCondition: { + fileSize: 321, + fileSizeUnit: FileSizeUnit.MB, + fileSizeOperator: FileSizeOperator.AT_MOST + }, + fileExtensions: ['pdf', 'txt'] + }; + }); + + it('should fill form', () => { + component.setValue(searchProperties); + expect(component.form.value).toEqual(searchProperties.fileSizeCondition); + }); + + it('should search based on passed value', () => { + const sizeField = 'content.size'; + const nameField = 'cm:name'; + component.id = 'properties'; + component.settings = { + field: `${sizeField},${nameField}` + }; + component.context = TestBed.inject(SearchQueryBuilderService); + component.ngOnInit(); + spyOn(component.displayValue$, 'next'); + spyOn(component.context, 'update'); + + component.setValue(searchProperties); + expect(component.displayValue$.next).toHaveBeenCalledWith('SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_MOST 321 SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.MB, pdf, txt'); + expect(component.context.queryFragments[component.id]).toBe(`${sizeField}:[0 TO 336592896] AND ${nameField}:("*.pdf" OR "*.txt")`); + expect(component.context.update).toHaveBeenCalled(); + }); + }); + + describe('preventIncorrectNumberCharacters', () => { + it('should prevent typing - character', () => { + expect(component.preventIncorrectNumberCharacters(new KeyboardEvent('keydown', { + key: '-' + }))).toBeFalse(); + }); + + it('should prevent typing e character', () => { + expect(component.preventIncorrectNumberCharacters(new KeyboardEvent('keydown', { + key: 'e' + }))).toBeFalse(); + }); + + it('should prevent typing + character', () => { + expect(component.preventIncorrectNumberCharacters(new KeyboardEvent('keydown', { + key: '+' + }))).toBeFalse(); + }); + + it('should allow typing digit', () => { + expect(component.preventIncorrectNumberCharacters(new KeyboardEvent('keydown', { + key: '1' + }))).toBeTrue(); + }); + }); +}); diff --git a/lib/content-services/src/lib/search/components/search-properties/search-properties.component.ts b/lib/content-services/src/lib/search/components/search-properties/search-properties.component.ts new file mode 100644 index 0000000000..16770e5341 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-properties/search-properties.component.ts @@ -0,0 +1,216 @@ +/*! + * @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 { AfterViewChecked, Component, ElementRef, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { FileSizeCondition } from './file-size-condition'; +import { FileSizeOperator } from './file-size-operator.enum'; +import { FileSizeUnit } from './file-size-unit.enum'; +import { Subject } from 'rxjs'; +import { SearchWidgetSettings } from '../../models/search-widget-settings.interface'; +import { SearchQueryBuilderService } from '../../services/search-query-builder.service'; +import { SearchProperties } from './search-properties'; +import { TranslateService } from '@ngx-translate/core'; +import { SearchWidget } from '../../models/search-widget.interface'; + +@Component({ + selector: 'adf-search-properties', + templateUrl: './search-properties.component.html', + styleUrls: ['./search-properties.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class SearchPropertiesComponent implements OnInit, AfterViewChecked, SearchWidget { + id: string; + settings?: SearchWidgetSettings; + context?: SearchQueryBuilderService; + startValue: SearchProperties; + displayValue$ = new Subject(); + + private _form = this.formBuilder.nonNullable.group({ + fileSizeOperator: FileSizeOperator.AT_LEAST, + fileSize: undefined, + fileSizeUnit: FileSizeUnit.KB + }); + private _fileSizeOperators = Object.keys(FileSizeOperator).map(key => FileSizeOperator[key]); + private _fileSizeUnits = [FileSizeUnit.KB, FileSizeUnit.MB, FileSizeUnit.GB]; + private canvas = document.createElement('canvas'); + private _fileSizeOperatorsMaxWidth: number; + private _selectedExtensions: string[]; + private _reset$ = new Subject(); + private sizeField: string; + private nameField: string; + + @ViewChild('fileSizeOperatorSelect', {read: ElementRef}) + fileSizeOperatorSelectElement: ElementRef; + + get form(): SearchPropertiesComponent['_form'] { + return this._form; + } + + get fileSizeOperators(): string[] { + return this._fileSizeOperators; + } + + get fileSizeUnits(): FileSizeUnit[] { + return this._fileSizeUnits; + } + + get fileSizeOperatorsMaxWidth(): number { + return this._fileSizeOperatorsMaxWidth; + } + + get reset$(): Subject { + return this._reset$; + } + + set selectedExtensions(extensions: string[]) { + this._selectedExtensions = extensions; + } + + constructor(private formBuilder: FormBuilder, private translateService: TranslateService) {} + + ngOnInit() { + if (this.settings) { + if (!this.settings.fileExtensions) { + this.settings.fileExtensions = []; + } + [this.sizeField, this.nameField] = this.settings.field.split(','); + } + if (this.startValue) { + this.setValue(this.startValue); + } + } + + ngAfterViewChecked() { + if (this.fileSizeOperatorSelectElement?.nativeElement.clientWidth && !this._fileSizeOperatorsMaxWidth) { + setTimeout(() => { + const extraFreeSpace = 20; + this._fileSizeOperatorsMaxWidth = Math.max(...this._fileSizeOperators.map((operator) => + this.getOperatorNameWidth(operator, this.getCanvasFont(this.fileSizeOperatorSelectElement.nativeElement)))) + + this.fileSizeOperatorSelectElement.nativeElement.querySelector('.mat-select-arrow-wrapper').clientWidth + + extraFreeSpace; + }); + } + } + + narrowDownAllowedCharacters(event: Event) { + const value = (event.target as HTMLInputElement).value; + if (!(event.target as HTMLInputElement).value) { + return; + } + if ((event as InputEvent).data !== ',' && (event as InputEvent).data !== '.') { + (event.target as HTMLInputElement).value = value.replace(/[^0-9.,]/g, ''); + } + } + + clearNumberFieldWhenInvalid(event: FocusEvent) { + if (!(event.target as HTMLInputElement).validity.valid) { + this.form.controls.fileSize.setValue(undefined); + } + } + + preventIncorrectNumberCharacters(event: KeyboardEvent): boolean { + return event.key !== '-' && event.key !== 'e' && event.key !== '+'; + } + + compareFileExtensions(extension1: string, extension2: string): boolean { + return extension1.toUpperCase() === extension2.toUpperCase(); + } + + getExtensionWithoutDot(extension: string): string { + const extensionSplitByDot = extension.split('.'); + return extensionSplitByDot[extensionSplitByDot.length - 1]; + } + + filterExtensions = (extensions: string[], filterValue: string): string[] => { + const filterValueLowerCase = this.getExtensionWithoutDot(filterValue).toLowerCase(); + const extensionWithDot = filterValue.startsWith('.'); + return extensions.filter((option) => { + const optionLowerCase = option.toLowerCase(); + return extensionWithDot && filterValueLowerCase ? optionLowerCase.startsWith(filterValueLowerCase) : optionLowerCase.includes(filterValue); + }); + }; + + reset() { + this.form.reset(); + this.reset$.next(); + this.displayValue$.next(''); + } + + submitValues() { + if (this.settings && this.context) { + let query = ''; + let displayedValue = ''; + if (this.form.value.fileSize !== undefined && this.form.value.fileSize !== null) { + displayedValue = `${this.translateService.instant(this.form.value.fileSizeOperator)} ${this.form.value.fileSize} ${this.translateService.instant(this.form.value.fileSizeUnit.abbreviation)}`; + const size = this.form.value.fileSize * this.form.value.fileSizeUnit.bytes; + switch (this.form.value.fileSizeOperator) { + case FileSizeOperator.AT_MOST: + query = `${this.sizeField}:[0 TO ${size}]`; + break; + case FileSizeOperator.AT_LEAST: + query = `${this.sizeField}:[${size} TO MAX]`; + break; + default: + query = `${this.sizeField}:[${size} TO ${size}]`; + } + } + if (this._selectedExtensions?.length) { + if (query) { + query += ' AND '; + displayedValue += ', '; + } + query += `${this.nameField}:("*.${this._selectedExtensions.join('" OR "*.')}")`; + displayedValue += this._selectedExtensions.join(', '); + } + this.displayValue$.next(displayedValue); + this.context.queryFragments[this.id] = query; + this.context.update(); + } + } + + hasValidValue(): boolean { + return true; + } + + getCurrentValue(): SearchProperties { + return { + fileSizeCondition: this.form.getRawValue(), + fileExtensions: this._selectedExtensions + }; + } + + setValue(searchProperties: SearchProperties) { + this.form.patchValue(searchProperties.fileSizeCondition); + this.selectedExtensions = searchProperties.fileExtensions; + this.submitValues(); + } + + private getOperatorNameWidth(operator: string, font: string): number { + const context = this.canvas.getContext('2d'); + context.font = font; + return context.measureText(this.translateService.instant(operator)).width; + } + + private getCssStyle(element: HTMLElement, property: string): string { + return window.getComputedStyle(element, null).getPropertyValue(property); + } + + private getCanvasFont(el: HTMLElement): string { + return `${this.getCssStyle(el, 'font-weight')} ${this.getCssStyle(el, 'font-size')} ${this.getCssStyle(el, 'font-family')}`; + } +} diff --git a/lib/content-services/src/lib/search/components/search-properties/search-properties.ts b/lib/content-services/src/lib/search/components/search-properties/search-properties.ts new file mode 100644 index 0000000000..2d4543d500 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-properties/search-properties.ts @@ -0,0 +1,23 @@ +/*! + * @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 { FileSizeCondition } from './file-size-condition'; + +export interface SearchProperties { + fileSizeCondition: FileSizeCondition; + fileExtensions: string[]; +} diff --git a/lib/content-services/src/lib/search/search.module.ts b/lib/content-services/src/lib/search/search.module.ts index a52b017b58..7c74d625bd 100644 --- a/lib/content-services/src/lib/search/search.module.ts +++ b/lib/content-services/src/lib/search/search.module.ts @@ -51,6 +51,7 @@ import { SearchWidgetChipComponent } from './components/search-filter-chips/sear import { SearchFacetChipComponent } from './components/search-filter-chips/search-facet-chip/search-facet-chip.component'; import { SearchLogicalFilterComponent } from './components/search-logical-filter/search-logical-filter.component'; import { ResetSearchDirective } from './components/reset-search.directive'; +import { SearchPropertiesComponent } from './components/search-properties/search-properties.component'; @NgModule({ imports: [ @@ -88,7 +89,8 @@ import { ResetSearchDirective } from './components/reset-search.directive'; SearchWidgetChipComponent, SearchFacetChipComponent, SearchLogicalFilterComponent, - ResetSearchDirective + ResetSearchDirective, + SearchPropertiesComponent ], exports: [ SearchComponent, 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 9ffe6cc671..a1cc1e9053 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 @@ -25,6 +25,7 @@ import { SearchDateRangeComponent } from '../components/search-date-range/search 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'; +import { SearchPropertiesComponent } from '../components/search-properties/search-properties.component'; @Injectable({ providedIn: 'root' @@ -38,6 +39,7 @@ export class SearchFilterService { text: SearchTextComponent, radio: SearchRadioComponent, slider: SearchSliderComponent, + properties: SearchPropertiesComponent, 'number-range': SearchNumberRangeComponent, 'check-list': SearchCheckListComponent, 'date-range': SearchDateRangeComponent,