[ACS-5183] properties facet file size and file type (#8766)

* ACS-5183 Created component which displays form to search nodes by file type and size

* ACS-5183 Validate proper value in number input and allow to use custom file types

* ACS-5183 Corrected problem with styles, case insensitive compare for extensions

* ACS-5183 Added translations, styles for selecting type, proper comparator for types

* ACS-5183 Prevent adding custom file type when selecting existing one

* ACS-5183 Corrected bytes for each file size unit and clear number input when value is incorrect

* ACS-5183 Added documentation for search properties component, updated documentation for search chip autocomplete input and taking values from settings

* ACS-5183 Unit tests

* ACS-5183 Added automation ids

* ACS-5183 Added missing license header

* ACS-5183 Fixed lint issues

* ACS-5183 Fixed build issue
This commit is contained in:
AleksanderSklorz 2023-07-18 15:45:00 +02:00 committed by GitHub
parent cffbdd51e4
commit d70f689e06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1093 additions and 39 deletions

View File

@ -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) |

View File

@ -29,6 +29,10 @@ Represents an input with autocomplete options.
| autocompleteOptions | `string[]` | [] | Options for autocomplete |
| onReset$ | [`Observable`](https://rxjs.dev/guide/observable)`<void>` | | 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

View File

@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -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
<!--v620 start-->
- [Search Properties component](content-services/components/search-properties.component.md)
<!--v620 end-->
## v6.1.0
<!--v610 start-->

View File

@ -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": {

View File

@ -21,7 +21,7 @@ import { Pipe, PipeTransform } from '@angular/core';
name: 'adfIsIncluded'
})
export class IsIncludedPipe<T> 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);
}
}

View File

@ -10,19 +10,22 @@
</button>
</mat-chip>
<input
placeholder="{{ 'SEARCH.FILTER.ACTIONS.ADD_OPTION' | translate }}"
placeholder="{{ placeholder | translate }}"
aria-controls="adf-search-chip-autocomplete"
#optionInput
[formControl]="formCtrl"
[matAutocomplete]="auto"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[attr.aria-label]="'SEARCH.FILTER.ACTIONS.ADD_OPTION' | translate"
(matChipInputTokenEnd)="add($event)">
[attr.aria-label]="placeholder | translate"
(matChipInputTokenEnd)="add($event)"
(blur)="activeAnyOption = false"
data-automation-id="adf-search-chip-autocomplete-input">
</mat-chip-list>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" id="adf-search-chip-autocomplete">
<mat-option [disabled]="option | adfIsIncluded: selectedOptions" *ngFor="let option of filteredOptions$ | async"
[ngClass]="(option | adfIsIncluded: selectedOptions) && 'adf-autocomplete-added-option'">
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" id="adf-search-chip-autocomplete"
(optionActivated)="activeAnyOption = true" (closed)="activeAnyOption = false">
<mat-option [disabled]="option | adfIsIncluded: selectedOptions : compareOption" *ngFor="let option of filteredOptions$ | async"
[ngClass]="(option | adfIsIncluded: selectedOptions : compareOption) && 'adf-autocomplete-added-option'">
{{option}}
</mat-option>
</mat-autocomplete>

View File

@ -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');

View File

@ -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<string[]> = new EventEmitter();
@ -50,11 +65,17 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy {
filteredOptions$: Observable<string[]>;
selectedOptions: string[] = [];
private onDestroy$ = new Subject<void>();
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);
}

View File

@ -19,7 +19,7 @@
<mat-icon>{{ chipIcon }}</mat-icon>
</mat-chip>
<mat-menu #menu="matMenu" backdropClass="adf-search-filter-chip-menu" (closed)="onClosed()">
<mat-menu #menu="matMenu" backdropClass="adf-search-filter-chip-menu" [class]="'adf-search-filter-chip-menu-panel-' + category.id" (closed)="onClosed()">
<div #menuContainer [attr.data-automation-id]="'search-field-' + category.name">
<adf-search-filter-menu-card (click)="$event.stopPropagation()"
(keydown.tab)="$event.stopPropagation();"

View File

@ -0,0 +1,25 @@
/*!
* @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 { FileSizeOperator } from './file-size-operator.enum';
import { FileSizeUnit } from './file-size-unit.enum';
export interface FileSizeCondition {
fileSizeOperator: FileSizeOperator;
fileSize?: number;
fileSizeUnit: FileSizeUnit;
}

View File

@ -0,0 +1,22 @@
/*!
* @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.
*/
export enum FileSizeOperator {
AT_LEAST = 'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_LEAST',
AT_MOST = 'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_MOST',
EXACTLY = 'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.EXACTLY'
}

View File

@ -0,0 +1,24 @@
/*!
* @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.
*/
export class FileSizeUnit {
static readonly KB = new FileSizeUnit('SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.KB', 1024);
static readonly MB = new FileSizeUnit('SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.MB', 1048576);
static readonly GB = new FileSizeUnit('SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.GB', 1073741824);
private constructor(readonly abbreviation: string, readonly bytes: number) {}
}

View File

@ -0,0 +1,54 @@
<form [formGroup]="form">
<label
for="adf-search-properties-file-size"
class="adf-search-properties-file-size-label">
{{ 'SEARCH.SEARCH_PROPERTIES.FILE_SIZE' | translate }}
</label>
<mat-form-field
[style.width.px]="fileSizeOperatorsMaxWidth"
class="adf-search-properties-file-size-operator">
<mat-select
data-automation-id="adf-search-properties-file-size-operator-select"
[formControl]="form.controls.fileSizeOperator"
#fileSizeOperatorSelect>
<mat-option
*ngFor="let fileSizeOperator of fileSizeOperators"
[value]="fileSizeOperator">
{{ fileSizeOperator | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<input
[formControl]="form.controls.fileSize"
type="number"
min="0"
step="any"
(input)="narrowDownAllowedCharacters($event)"
(keydown)="preventIncorrectNumberCharacters($event)"
id="adf-search-properties-file-size"
[placeholder]="'SEARCH.SEARCH_PROPERTIES.FILE_SIZE_PLACEHOLDER' | translate"
(blur)="clearNumberFieldWhenInvalid($event)"
data-automation-id="adf-search-properties-file-size-input" />
<mat-form-field class="adf-search-properties-file-size-unit">
<mat-select
[formControl]="form.controls.fileSizeUnit"
data-automation-id="adf-search-properties-file-size-unit-select">
<mat-option
*ngFor="let fileSizeUnit of fileSizeUnits"
[value]="fileSizeUnit">
{{ fileSizeUnit.abbreviation | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<p class="adf-search-properties-file-type-label">{{ 'SEARCH.SEARCH_PROPERTIES.FILE_TYPE' | translate }}</p>
<adf-search-chip-autocomplete-input
[autocompleteOptions]="settings?.fileExtensions"
(optionsChanged)="selectedExtensions = $event"
[onReset$]="reset$"
[allowOnlyPredefinedValues]="false"
[compareOption]="compareFileExtensions"
[formatChipValue]="getExtensionWithoutDot"
[filter]="filterExtensions"
placeholder="SEARCH.SEARCH_PROPERTIES.FILE_TYPE">
</adf-search-chip-autocomplete-input>
</form>

View File

@ -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;
}

View File

@ -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<SearchPropertiesComponent>;
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();
});
});
});

View File

@ -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<string>();
private _form = this.formBuilder.nonNullable.group<FileSizeCondition>({
fileSizeOperator: FileSizeOperator.AT_LEAST,
fileSize: undefined,
fileSizeUnit: FileSizeUnit.KB
});
private _fileSizeOperators = Object.keys(FileSizeOperator).map<string>(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<void>();
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<void> {
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')}`;
}
}

View File

@ -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[];
}

View File

@ -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,

View File

@ -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,