mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-05-12 17:04:57 +00:00
[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:
parent
cffbdd51e4
commit
d70f689e06
@ -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) |
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
||||

|
||||
|
||||
## 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)
|
BIN
docs/docassets/images/search-properties.png
Normal file
BIN
docs/docassets/images/search-properties.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 65 KiB |
@ -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-->
|
||||
|
@ -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": {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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');
|
||||
|
@ -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,7 +89,11 @@ 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);
|
||||
@ -77,6 +102,7 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy {
|
||||
this.formCtrl.setValue(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remove(value: string) {
|
||||
const index = this.selectedOptions.indexOf(value);
|
||||
@ -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);
|
||||
}
|
||||
|
@ -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();"
|
||||
|
@ -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;
|
||||
}
|
@ -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'
|
||||
}
|
@ -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) {}
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@ -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')}`;
|
||||
}
|
||||
}
|
@ -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[];
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user