mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[ACS-5266] Advanced Search - New component for Category facet (#8764)
* [ACS-5266] new component for category facet * [ACS-5266] fixed tests & docs * [ACS-5266] some fixes * [ACS-5266] linting * [ACS-5266] some improvements * [ACS-5266] reduced observable from child component * [ACS-5266] fixed docs * [ACS-5266] rebase & improvements * [ACS-5266] typo
This commit is contained in:
committed by
GitHub
parent
1ebac21251
commit
2a4507d529
@@ -15,30 +15,32 @@ Represents an input with autocomplete options.
|
|||||||
|
|
||||||
```html
|
```html
|
||||||
<adf-search-chip-autocomplete-input
|
<adf-search-chip-autocomplete-input
|
||||||
[autocompleteOptions]="allOptions"
|
[autocompleteOptions]="autocompleteOptions"
|
||||||
[onReset$]="onResetObservable$"
|
[onReset$]="onResetObservable$"
|
||||||
[allowOnlyPredefinedValues]="allowOnlyPredefinedValues"
|
[allowOnlyPredefinedValues]="allowOnlyPredefinedValues"
|
||||||
|
(inputChanged)="onInputChange($event)"
|
||||||
(optionsChanged)="onOptionsChange($event)">
|
(optionsChanged)="onOptionsChange($event)">
|
||||||
</adf-search-chip-autocomplete-input>
|
</adf-search-chip-autocomplete-input>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Properties
|
### Properties
|
||||||
|
|
||||||
| Name | Type | Default value | Description |
|
| Name | Type | Default value | Description |
|
||||||
|---------------------------|--------------------------|----|-----------------------------------------------------------------------------------------------|
|
|---------------------------|--------------------------|----|-----------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| autocompleteOptions | `string[]` | [] | Options for autocomplete |
|
| autocompleteOptions | `AutocompleteOption[]` | [] | 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 |
|
| 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 |
|
| 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. |
|
| 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. |
|
| compareOption | (option1: AutocompleteOption, option2: AutocompleteOption) => 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. |
|
| 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. |
|
| filter | (options: AutocompleteOption[], value: string) => AutocompleteOption[] | | Function which is used to filter out possible options from hint. By default it checks if option includes typed value and is case insensitive. |
|
||||||
|
|
||||||
### Events
|
### Events
|
||||||
|
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| ---- | ---- |-----------------------------------------------|
|
| ---- | ---- |-----------------------------------------------|
|
||||||
| optionsChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string[]>` | Emitted when the selected options are changed |
|
| inputChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string>` | Emitted when the input changes |
|
||||||
|
| optionsChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<AutocompleteOption[]>` | Emitted when the selected options are changed |
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
|
|
||||||
|
@@ -28,7 +28,7 @@ Implements a [search widget](../../../lib/content-services/src/lib/search/models
|
|||||||
"hideDefaultAction": true,
|
"hideDefaultAction": true,
|
||||||
"allowOnlyPredefinedValues": false,
|
"allowOnlyPredefinedValues": false,
|
||||||
"field": "SITE",
|
"field": "SITE",
|
||||||
"options": [ "Option 1", "Option 2" ]
|
"autocompleteOptions": [ {"value": "Option 1"}, {"value": "Option 2"} ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ Implements a [search widget](../../../lib/content-services/src/lib/search/models
|
|||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| ---- |----------|--------------------------------------------------------------------------------------------------------------------|
|
| ---- |----------|--------------------------------------------------------------------------------------------------------------------|
|
||||||
| field | `string` | Field to apply the query to. Required value |
|
| field | `string` | Field to apply the query to. Required value |
|
||||||
| options | `string[]` | Predefined options for autocomplete |
|
| autocompleteOptions | `AutocompleteOption[]` | Predefined options for autocomplete |
|
||||||
| allowOnlyPredefinedValues | `boolean` | Specifies whether the input values should only be from predefined |
|
| allowOnlyPredefinedValues | `boolean` | Specifies whether the input values should only be from predefined |
|
||||||
| allowUpdateOnChange | `boolean` | Enable/Disable the update fire event when text has been changed. By default is true |
|
| allowUpdateOnChange | `boolean` | Enable/Disable the update fire event when text has been changed. By default is true |
|
||||||
| hideDefaultAction | `boolean` | Show/hide the widget actions. By default is false |
|
| hideDefaultAction | `boolean` | Show/hide the widget actions. By default is false |
|
||||||
|
@@ -14,7 +14,7 @@ Checks if the provided value is contained in the provided array.
|
|||||||
<!-- {% raw %} -->
|
<!-- {% raw %} -->
|
||||||
|
|
||||||
```HTML
|
```HTML
|
||||||
<mat-option [disabled]="value | adfIsIncluded: arrayOfValues"</mat-option>
|
<mat-option [disabled]="value | adfIsIncluded: arrayOfValues : comparator"></mat-option>
|
||||||
```
|
```
|
||||||
|
|
||||||
<!-- {% endraw %} -->
|
<!-- {% endraw %} -->
|
||||||
|
@@ -284,6 +284,7 @@
|
|||||||
"SUMMARY": "{{numResults}} result found for {{searchTerm}}",
|
"SUMMARY": "{{numResults}} result found for {{searchTerm}}",
|
||||||
"NONE": "No results found for {{searchTerm}}",
|
"NONE": "No results found for {{searchTerm}}",
|
||||||
"ERROR": "We hit a problem during the search - try again.",
|
"ERROR": "We hit a problem during the search - try again.",
|
||||||
|
"WILL_CONTAIN": "Results will contain '{{searchTerm}}'",
|
||||||
"COLUMNS": {
|
"COLUMNS": {
|
||||||
"NAME": "Display name",
|
"NAME": "Display name",
|
||||||
"MODIFIED_BY": "Modified by",
|
"MODIFIED_BY": "Modified by",
|
||||||
|
@@ -19,6 +19,7 @@ import { NgModule } from '@angular/core';
|
|||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
import { MatChipsModule } from '@angular/material/chips';
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
import { MatOptionModule, MatRippleModule } from '@angular/material/core';
|
import { MatOptionModule, MatRippleModule } from '@angular/material/core';
|
||||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||||
@@ -40,6 +41,7 @@ import { MatBadgeModule } from '@angular/material/badge';
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatAutocompleteModule,
|
||||||
MatChipsModule,
|
MatChipsModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
@@ -63,6 +65,7 @@ import { MatBadgeModule } from '@angular/material/badge';
|
|||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatAutocompleteModule,
|
||||||
MatChipsModule,
|
MatChipsModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
|
@@ -41,4 +41,11 @@ describe('IsIncludedPipe', () => {
|
|||||||
it('should return false if the number is not contained in an array', () => {
|
it('should return false if the number is not contained in an array', () => {
|
||||||
expect(pipe.transform(50, array)).toBeFalsy();
|
expect(pipe.transform(50, array)).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use provided comparator to check if value contains in the provided array', () => {
|
||||||
|
const arrayOfObjects = [{id: 'id-1', value: 'value-1'}, {id: 'id-2', value: 'value-2'}];
|
||||||
|
const filterFunction = (extension1, extension2) => extension1.value === extension2.value;
|
||||||
|
expect(pipe.transform({id: 'id-1', value: 'value-1'}, arrayOfObjects, filterFunction)).toBeTruthy();
|
||||||
|
expect(pipe.transform({id: 'id-1', value: 'value-3'}, arrayOfObjects, filterFunction)).toBeFalsy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -4,8 +4,14 @@
|
|||||||
class="adf-option-chips"
|
class="adf-option-chips"
|
||||||
*ngFor="let option of selectedOptions"
|
*ngFor="let option of selectedOptions"
|
||||||
(removed)="remove(option)">
|
(removed)="remove(option)">
|
||||||
<span>{{option}}</span>
|
<span [matTooltip]="'SEARCH.RESULTS.WILL_CONTAIN' | translate:{searchTerm: option.fullPath}"
|
||||||
<button matChipRemove class="adf-option-chips-delete-button" [attr.aria-label]="('SEARCH.FILTER.BUTTONS.REMOVE' | translate) + ' ' + option">
|
[matTooltipDisabled]="!option.fullPath" [matTooltipShowDelay]="tooltipShowDelay">
|
||||||
|
{{ option.value }}
|
||||||
|
</span>
|
||||||
|
<button matChipRemove class="adf-option-chips-delete-button" [matTooltipDisabled]="!option.fullPath"
|
||||||
|
[matTooltip]="('SEARCH.FILTER.BUTTONS.REMOVE' | translate) + ' \'' + option.fullPath + '\''"
|
||||||
|
[matTooltipShowDelay]="tooltipShowDelay"
|
||||||
|
[attr.aria-label]="('SEARCH.FILTER.BUTTONS.REMOVE' | translate) + ' ' + option.value">
|
||||||
<mat-icon class="adf-option-chips-delete-icon">close</mat-icon>
|
<mat-icon class="adf-option-chips-delete-icon">close</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</mat-chip>
|
</mat-chip>
|
||||||
@@ -24,9 +30,15 @@
|
|||||||
</mat-chip-list>
|
</mat-chip-list>
|
||||||
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" id="adf-search-chip-autocomplete"
|
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" id="adf-search-chip-autocomplete"
|
||||||
(optionActivated)="activeAnyOption = true" (closed)="activeAnyOption = false">
|
(optionActivated)="activeAnyOption = true" (closed)="activeAnyOption = false">
|
||||||
<mat-option [disabled]="option | adfIsIncluded: selectedOptions : compareOption" *ngFor="let option of filteredOptions$ | async"
|
<ng-container *ngIf="optionInput.value.length > 0">
|
||||||
[ngClass]="(option | adfIsIncluded: selectedOptions : compareOption) && 'adf-autocomplete-added-option'">
|
<mat-option
|
||||||
{{option}}
|
[disabled]="option | adfIsIncluded: selectedOptions : compareOption"
|
||||||
</mat-option>
|
*ngFor="let option of filteredOptions" [value]="option" [matTooltipShowDelay]="tooltipShowDelay"
|
||||||
|
[matTooltipDisabled]="!option.fullPath" matTooltipPosition="right"
|
||||||
|
[matTooltip]="'SEARCH.RESULTS.WILL_CONTAIN' | translate:{searchTerm: option.fullPath || option.value}"
|
||||||
|
[ngClass]="(option | adfIsIncluded: selectedOptions : compareOption) && 'adf-autocomplete-added-option'">
|
||||||
|
{{ option.fullPath || option.value }}
|
||||||
|
</mat-option>
|
||||||
|
</ng-container>
|
||||||
</mat-autocomplete>
|
</mat-autocomplete>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
@@ -40,6 +40,10 @@ adf-search-chip-autocomplete-input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mat-tooltip-hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.mat-option.adf-autocomplete-added-option {
|
.mat-option.adf-autocomplete-added-option {
|
||||||
background: var(--adf-theme-mat-grey-color-a200);
|
background: var(--adf-theme-mat-grey-color-a200);
|
||||||
color: var(--adf-theme-primary-300);
|
color: var(--adf-theme-primary-300);
|
||||||
|
@@ -41,8 +41,8 @@ describe('SearchChipAutocompleteInputComponent', () => {
|
|||||||
fixture = TestBed.createComponent(SearchChipAutocompleteInputComponent);
|
fixture = TestBed.createComponent(SearchChipAutocompleteInputComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
component.onReset$ = onResetSubject.asObservable();
|
component.onReset$ = onResetSubject.asObservable();
|
||||||
|
component.autocompleteOptions = [{value: 'option1'}, {value: 'option2'}];
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
component.autocompleteOptions = ['option1', 'option2'];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function getInput(): HTMLInputElement {
|
function getInput(): HTMLInputElement {
|
||||||
@@ -110,6 +110,7 @@ describe('SearchChipAutocompleteInputComponent', () => {
|
|||||||
const optionsChangedSpy = spyOn(component.optionsChanged, 'emit');
|
const optionsChangedSpy = spyOn(component.optionsChanged, 'emit');
|
||||||
enterNewInputValue('op');
|
enterNewInputValue('op');
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
const matOptions = getOptionElements();
|
const matOptions = getOptionElements();
|
||||||
expect(matOptions.length).toBe(2);
|
expect(matOptions.length).toBe(2);
|
||||||
@@ -117,8 +118,8 @@ describe('SearchChipAutocompleteInputComponent', () => {
|
|||||||
const optionToClick = matOptions[0].nativeElement as HTMLElement;
|
const optionToClick = matOptions[0].nativeElement as HTMLElement;
|
||||||
optionToClick.click();
|
optionToClick.click();
|
||||||
|
|
||||||
expect(optionsChangedSpy).toHaveBeenCalledOnceWith(['option1']);
|
expect(optionsChangedSpy).toHaveBeenCalledOnceWith([{value: 'option1'}]);
|
||||||
expect(component.selectedOptions).toEqual(['option1']);
|
expect(component.selectedOptions).toEqual([{value: 'option1'}]);
|
||||||
expect(getChipList().length).toBe(1);
|
expect(getChipList().length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -126,31 +127,35 @@ describe('SearchChipAutocompleteInputComponent', () => {
|
|||||||
addNewOption('option1');
|
addNewOption('option1');
|
||||||
enterNewInputValue('op');
|
enterNewInputValue('op');
|
||||||
|
|
||||||
const addedOptions = getAddedOptionElements();
|
|
||||||
|
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const addedOptions = getAddedOptionElements();
|
||||||
expect(addedOptions[0]).toBeTruthy();
|
expect(addedOptions[0]).toBeTruthy();
|
||||||
expect(addedOptions.length).toBe(1);
|
expect(addedOptions.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should apply class to already selected options based on custom compareOption function', async () => {
|
it('should apply class to already selected options based on custom compareOption function', async () => {
|
||||||
component.allowOnlyPredefinedValues = false;
|
component.allowOnlyPredefinedValues = false;
|
||||||
component.autocompleteOptions = ['.test1', 'test3', '.test2', 'test1.'];
|
component.autocompleteOptions = [{value: '.test1'}, {value: 'test3'}, {value: '.test2.'}, {value: 'test1'}];
|
||||||
component.compareOption = (option1, option2) => option1.split('.')[1] === option2;
|
component.compareOption = (option1, option2) => option1.value.split('.')[1] === option2.value;
|
||||||
|
|
||||||
fixture.detectChanges();
|
|
||||||
addNewOption('test1');
|
addNewOption('test1');
|
||||||
enterNewInputValue('t');
|
enterNewInputValue('t');
|
||||||
|
|
||||||
const addedOptions = getAddedOptionElements();
|
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
expect(addedOptions.length).toBe(1);
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(getAddedOptionElements().length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should limit autocomplete list to 15 values max', () => {
|
it('should limit autocomplete list to 15 values max', async () => {
|
||||||
component.autocompleteOptions = ['a1','a2','a3','a4','a5','a6','a7','a8','a9','a10','a11','a12','a13','a14','a15','a16'];
|
component.autocompleteOptions = Array.from({length: 16}, (_, i) => ({value: `a${i}`}));
|
||||||
enterNewInputValue('a');
|
enterNewInputValue('a');
|
||||||
|
|
||||||
|
await fixture.whenStable();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(getOptionElements().length).toBe(15);
|
expect(getOptionElements().length).toBe(15);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -160,27 +165,33 @@ describe('SearchChipAutocompleteInputComponent', () => {
|
|||||||
expect(getChipList().length).toBe(1);
|
expect(getChipList().length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show autocomplete list if similar predefined values exists', () => {
|
it('should show autocomplete list if similar predefined values exists', async () => {
|
||||||
enterNewInputValue('op');
|
enterNewInputValue('op');
|
||||||
|
await fixture.whenStable();
|
||||||
|
fixture.detectChanges();
|
||||||
expect(getOptionElements().length).toBe(2);
|
expect(getOptionElements().length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show autocomplete list based on custom filtering', () => {
|
it('should show autocomplete list based on custom filtering', async () => {
|
||||||
component.autocompleteOptions = ['.test1', 'test1', 'test1.', '.test2', '.test12'];
|
component.autocompleteOptions = [{value: '.test1'}, {value: 'test1'}, {value: 'test1.'}, {value: '.test2'}, {value: '.test12'}];
|
||||||
component.filter = (options, value) => options.filter((option) => option.split('.')[1] === value);
|
component.filter = (options, value) => options.filter((option) => option.value.split('.')[1] === value);
|
||||||
enterNewInputValue('test1');
|
enterNewInputValue('test1');
|
||||||
|
await fixture.whenStable();
|
||||||
|
fixture.detectChanges();
|
||||||
expect(getOptionElements().length).toBe(1);
|
expect(getOptionElements().length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show autocomplete list if there are no similar predefined values', () => {
|
it('should not show autocomplete list if there are no similar predefined values', async () => {
|
||||||
enterNewInputValue('test');
|
enterNewInputValue('test');
|
||||||
|
await fixture.whenStable();
|
||||||
|
fixture.detectChanges();
|
||||||
expect(getOptionElements().length).toBe(0);
|
expect(getOptionElements().length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit new value when selected options changed', () => {
|
it('should emit new value when selected options changed', () => {
|
||||||
const optionsChangedSpy = spyOn(component.optionsChanged, 'emit');
|
const optionsChangedSpy = spyOn(component.optionsChanged, 'emit');
|
||||||
addNewOption('option1');
|
addNewOption('option1');
|
||||||
expect(optionsChangedSpy).toHaveBeenCalledOnceWith(['option1']);
|
expect(optionsChangedSpy).toHaveBeenCalledOnceWith([{value: 'option1'}]);
|
||||||
expect(getChipList().length).toBe(1);
|
expect(getChipList().length).toBe(1);
|
||||||
expect(getChipValue(0)).toBe('option1');
|
expect(getChipValue(0)).toBe('option1');
|
||||||
});
|
});
|
||||||
@@ -221,7 +232,23 @@ describe('SearchChipAutocompleteInputComponent', () => {
|
|||||||
fixture.debugElement.query(By.directive(MatChipRemove)).nativeElement.click();
|
fixture.debugElement.query(By.directive(MatChipRemove)).nativeElement.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(optionsChangedSpy).toHaveBeenCalledOnceWith(['option2']);
|
expect(optionsChangedSpy).toHaveBeenCalledOnceWith([{value: 'option2'}]);
|
||||||
expect(getChipList().length).toEqual(1);
|
expect(getChipList().length).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show full category path when fullPath provided', () => {
|
||||||
|
component.filteredOptions = [{id: 'test-id', value: 'test-value', fullPath: 'test-full-path'}];
|
||||||
|
enterNewInputValue('test-value');
|
||||||
|
const matOption = fixture.debugElement.query(By.css('.mat-option span')).nativeElement;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(matOption.innerHTML).toEqual(' test-full-path ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit input value when input changed', async () => {
|
||||||
|
const inputChangedSpy = spyOn(component.inputChanged, 'emit');
|
||||||
|
enterNewInputValue('test-value');
|
||||||
|
await fixture.whenStable();
|
||||||
|
expect(inputChangedSpy).toHaveBeenCalledOnceWith('test-value');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -15,13 +15,26 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, ViewEncapsulation, ElementRef, ViewChild, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
ViewEncapsulation,
|
||||||
|
ElementRef,
|
||||||
|
ViewChild,
|
||||||
|
OnInit,
|
||||||
|
OnDestroy,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
EventEmitter,
|
||||||
|
SimpleChanges,
|
||||||
|
OnChanges
|
||||||
|
} from '@angular/core';
|
||||||
import { ENTER } from '@angular/cdk/keycodes';
|
import { ENTER } from '@angular/cdk/keycodes';
|
||||||
import { FormControl } from '@angular/forms';
|
import { FormControl } from '@angular/forms';
|
||||||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||||
import { MatChipInputEvent } from '@angular/material/chips';
|
import { MatChipInputEvent } from '@angular/material/chips';
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { EMPTY, Observable, Subject, timer } from 'rxjs';
|
||||||
import { map, startWith, takeUntil, tap } from 'rxjs/operators';
|
import { debounce, startWith, takeUntil, tap } from 'rxjs/operators';
|
||||||
|
import { AutocompleteOption } from '../../models/autocomplete-option.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'adf-search-chip-autocomplete-input',
|
selector: 'adf-search-chip-autocomplete-input',
|
||||||
@@ -29,12 +42,12 @@ import { map, startWith, takeUntil, tap } from 'rxjs/operators';
|
|||||||
styleUrls: ['./search-chip-autocomplete-input.component.scss'],
|
styleUrls: ['./search-chip-autocomplete-input.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy {
|
export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy, OnChanges {
|
||||||
@ViewChild('optionInput')
|
@ViewChild('optionInput')
|
||||||
optionInput: ElementRef<HTMLInputElement>;
|
optionInput: ElementRef<HTMLInputElement>;
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
autocompleteOptions: string[] = [];
|
autocompleteOptions: AutocompleteOption[] = [];
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
onReset$: Observable<void>;
|
onReset$: Observable<void>;
|
||||||
@@ -46,24 +59,28 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy {
|
|||||||
placeholder = 'SEARCH.FILTER.ACTIONS.ADD_OPTION';
|
placeholder = 'SEARCH.FILTER.ACTIONS.ADD_OPTION';
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
compareOption?: (option1: string, option2: string) => boolean;
|
compareOption?: (option1: AutocompleteOption, option2: AutocompleteOption) => boolean;
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
formatChipValue?: (option: string) => string;
|
formatChipValue?: (option: string) => string;
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
filter = (options: string[], value: string): string[] => {
|
filter = (options: AutocompleteOption[], value: string): AutocompleteOption[] => {
|
||||||
const filterValue = value.toLowerCase();
|
const filterValue = value.toLowerCase();
|
||||||
return options.filter(option => option.toLowerCase().includes(filterValue));
|
return options.filter(option => option.value.toLowerCase().includes(filterValue)).slice(0, 15);
|
||||||
};
|
};
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
optionsChanged: EventEmitter<string[]> = new EventEmitter();
|
optionsChanged = new EventEmitter<AutocompleteOption[]>();
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
inputChanged = new EventEmitter<string>();
|
||||||
|
|
||||||
readonly separatorKeysCodes = [ENTER] as const;
|
readonly separatorKeysCodes = [ENTER] as const;
|
||||||
formCtrl = new FormControl('');
|
formCtrl = new FormControl('');
|
||||||
filteredOptions$: Observable<string[]>;
|
filteredOptions: AutocompleteOption[] = [];
|
||||||
selectedOptions: string[] = [];
|
selectedOptions: AutocompleteOption[] = [];
|
||||||
|
tooltipShowDelay = 800;
|
||||||
private onDestroy$ = new Subject<void>();
|
private onDestroy$ = new Subject<void>();
|
||||||
private _activeAnyOption = false;
|
private _activeAnyOption = false;
|
||||||
|
|
||||||
@@ -71,16 +88,25 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy {
|
|||||||
this._activeAnyOption = active;
|
this._activeAnyOption = active;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
ngOnInit() {
|
||||||
this.filteredOptions$ = this.formCtrl.valueChanges.pipe(
|
this.formCtrl.valueChanges
|
||||||
startWith(null),
|
.pipe(
|
||||||
tap(() => this.activeAnyOption = false),
|
startWith(''),
|
||||||
map((value: string | null) => (value ? this.filter(this.autocompleteOptions, value).slice(0, 15) : []))
|
tap(() => this.activeAnyOption = false),
|
||||||
);
|
debounce((value: string) => (value ? timer(300) : EMPTY)),
|
||||||
|
takeUntil(this.onDestroy$)
|
||||||
|
)
|
||||||
|
.subscribe((value: string) => {
|
||||||
|
this.filteredOptions = value ? this.filter(this.autocompleteOptions, value) : [];
|
||||||
|
this.inputChanged.emit(value);
|
||||||
|
});
|
||||||
|
this.onReset$?.pipe(takeUntil(this.onDestroy$)).subscribe(() => this.reset());
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnChanges(changes: SimpleChanges) {
|
||||||
this.onReset$?.pipe(takeUntil(this.onDestroy$)).subscribe(() => this.reset());
|
if (changes.autocompleteOptions) {
|
||||||
|
this.filteredOptions = changes.autocompleteOptions.currentValue.length > 0 ? this.filter(changes.autocompleteOptions.currentValue, this.formCtrl.value) : [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@@ -96,15 +122,20 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (value && this.isExists(value) && !this.isAdded(value)) {
|
if (value && this.isExists(value) && !this.isAdded(value)) {
|
||||||
this.selectedOptions.push(value);
|
if (this.allowOnlyPredefinedValues) {
|
||||||
|
const index = this.autocompleteOptions.findIndex(option => option.value.toLowerCase() === value.toLowerCase());
|
||||||
|
this.selectedOptions.push(this.autocompleteOptions[index]);
|
||||||
|
} else {
|
||||||
|
this.selectedOptions.push({value});
|
||||||
|
}
|
||||||
this.optionsChanged.emit(this.selectedOptions);
|
this.optionsChanged.emit(this.selectedOptions);
|
||||||
event.chipInput.clear();
|
event.chipInput.clear();
|
||||||
this.formCtrl.setValue(null);
|
this.formCtrl.setValue('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(value: string) {
|
remove(value: AutocompleteOption) {
|
||||||
const index = this.selectedOptions.indexOf(value);
|
const index = this.selectedOptions.indexOf(value);
|
||||||
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
@@ -114,28 +145,28 @@ export class SearchChipAutocompleteInputComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selected(event: MatAutocompleteSelectedEvent) {
|
selected(event: MatAutocompleteSelectedEvent) {
|
||||||
if (!this.isAdded(event.option.viewValue)) {
|
this.selectedOptions.push(event.option.value);
|
||||||
this.selectedOptions.push(event.option.viewValue);
|
this.optionInput.nativeElement.value = '';
|
||||||
this.optionInput.nativeElement.value = '';
|
this.formCtrl.setValue('');
|
||||||
this.formCtrl.setValue(null);
|
this.optionsChanged.emit(this.selectedOptions);
|
||||||
this.optionsChanged.emit(this.selectedOptions);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private isAdded(value: string): boolean {
|
private isAdded(value: string): boolean {
|
||||||
return this.selectedOptions.includes(value);
|
const valueLowerCase = value.toLowerCase();
|
||||||
|
return this.selectedOptions.some(option => option.value.toLowerCase() === valueLowerCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
private isExists(value: string): boolean {
|
private isExists(value: string): boolean {
|
||||||
|
const valueLowerCase = value.toLowerCase();
|
||||||
return this.allowOnlyPredefinedValues
|
return this.allowOnlyPredefinedValues
|
||||||
? this.autocompleteOptions.map(option => option.toLowerCase()).includes(value.toLowerCase())
|
? this.autocompleteOptions.some(option => option.value.toLowerCase() === valueLowerCase)
|
||||||
: true;
|
: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private reset() {
|
private reset() {
|
||||||
this.selectedOptions = [];
|
this.selectedOptions = [];
|
||||||
this.optionsChanged.emit(this.selectedOptions);
|
this.optionsChanged.emit(this.selectedOptions);
|
||||||
this.formCtrl.setValue(null);
|
this.formCtrl.setValue('');
|
||||||
this.optionInput.nativeElement.value = '';
|
this.optionInput.nativeElement.value = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
<adf-search-chip-autocomplete-input
|
<adf-search-chip-autocomplete-input
|
||||||
[autocompleteOptions]="autocompleteOptions"
|
[autocompleteOptions]="autocompleteOptions$ | async"
|
||||||
[onReset$]="reset$"
|
[onReset$]="reset$"
|
||||||
[allowOnlyPredefinedValues]="settings.allowOnlyPredefinedValues"
|
[allowOnlyPredefinedValues]="settings.allowOnlyPredefinedValues"
|
||||||
|
(inputChanged)="onInputChange($event)"
|
||||||
|
[compareOption]="optionComparator"
|
||||||
(optionsChanged)="onOptionsChange($event)">
|
(optionsChanged)="onOptionsChange($event)">
|
||||||
</adf-search-chip-autocomplete-input>
|
</adf-search-chip-autocomplete-input>
|
||||||
|
|
||||||
|
@@ -22,6 +22,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||||||
import { SearchFilterAutocompleteChipsComponent } from './search-filter-autocomplete-chips.component';
|
import { SearchFilterAutocompleteChipsComponent } from './search-filter-autocomplete-chips.component';
|
||||||
import { TagService } from '@alfresco/adf-content-services';
|
import { TagService } from '@alfresco/adf-content-services';
|
||||||
import { EMPTY, of } from 'rxjs';
|
import { EMPTY, of } from 'rxjs';
|
||||||
|
import { AutocompleteField } from '../../models/autocomplete-option.interface';
|
||||||
|
|
||||||
describe('SearchFilterAutocompleteChipsComponent', () => {
|
describe('SearchFilterAutocompleteChipsComponent', () => {
|
||||||
let component: SearchFilterAutocompleteChipsComponent;
|
let component: SearchFilterAutocompleteChipsComponent;
|
||||||
@@ -51,7 +52,7 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
|
|||||||
} as any;
|
} as any;
|
||||||
component.settings = {
|
component.settings = {
|
||||||
field: 'test', allowUpdateOnChange: true, hideDefaultAction: false, allowOnlyPredefinedValues: false,
|
field: 'test', allowUpdateOnChange: true, hideDefaultAction: false, allowOnlyPredefinedValues: false,
|
||||||
options: ['option1', 'option2']
|
autocompleteOptions: [{value: 'option1'}, {value: 'option2'}]
|
||||||
};
|
};
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -63,13 +64,16 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should set autocomplete options on init', () => {
|
it('should set autocomplete options on init', (done) => {
|
||||||
component.settings.options = ['test 1', 'test 2'];
|
component.settings.autocompleteOptions = [{value: 'test 1'}, {value: 'test 2'}];
|
||||||
component.ngOnInit();
|
component.ngOnInit();
|
||||||
expect(component.autocompleteOptions).toEqual(['test 1', 'test 2']);
|
component.autocompleteOptions$.subscribe(result => {
|
||||||
|
expect(result).toEqual([{value: 'test 1'}, {value: 'test 2'}]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load tags if field = TAG', () => {
|
it('should load tags if field = TAG', (done) => {
|
||||||
const tagPagingMock = {
|
const tagPagingMock = {
|
||||||
list: {
|
list: {
|
||||||
pagination: {},
|
pagination: {},
|
||||||
@@ -77,10 +81,13 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
component.settings.field = 'TAG';
|
component.settings.field = AutocompleteField.TAG;
|
||||||
spyOn(tagService, 'getAllTheTags').and.returnValue(of(tagPagingMock));
|
spyOn(tagService, 'getAllTheTags').and.returnValue(of(tagPagingMock));
|
||||||
component.ngOnInit();
|
component.ngOnInit();
|
||||||
expect(component.autocompleteOptions).toEqual(['tag1', 'tag2']);
|
component.autocompleteOptions$.subscribe(result => {
|
||||||
|
expect(result).toEqual([{value: 'tag1'},{value: 'tag2'}]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update display value when options changes', () => {
|
it('should update display value when options changes', () => {
|
||||||
@@ -94,9 +101,9 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should reset value and display value when reset button is clicked', () => {
|
it('should reset value and display value when reset button is clicked', () => {
|
||||||
component.setValue(['option1', 'option2']);
|
component.setValue([{value: 'option1'}, {value: 'option2'}]);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(component.selectedOptions).toEqual(['option1', 'option2']);
|
expect(component.selectedOptions).toEqual([{value: 'option1'}, {value: 'option2'}]);
|
||||||
spyOn(component.context, 'update');
|
spyOn(component.context, 'update');
|
||||||
spyOn(component.displayValue$, 'next');
|
spyOn(component.displayValue$, 'next');
|
||||||
const clearBtn: HTMLButtonElement = fixture.debugElement.query(By.css('[data-automation-id="adf-search-chip-autocomplete-btn-clear"]')).nativeElement;
|
const clearBtn: HTMLButtonElement = fixture.debugElement.query(By.css('[data-automation-id="adf-search-chip-autocomplete-btn-clear"]')).nativeElement;
|
||||||
@@ -110,13 +117,18 @@ describe('SearchFilterAutocompleteChipsComponent', () => {
|
|||||||
|
|
||||||
it('should correctly compose the search query', () => {
|
it('should correctly compose the search query', () => {
|
||||||
spyOn(component.context, 'update');
|
spyOn(component.context, 'update');
|
||||||
addNewOption('option2');
|
component.selectedOptions = [{value: 'option2'}, {value: 'option1'}];
|
||||||
addNewOption('option1');
|
|
||||||
const applyBtn: HTMLButtonElement = fixture.debugElement.query(By.css('[data-automation-id="adf-search-chip-autocomplete-btn-apply"]')).nativeElement;
|
const applyBtn: HTMLButtonElement = fixture.debugElement.query(By.css('[data-automation-id="adf-search-chip-autocomplete-btn-apply"]')).nativeElement;
|
||||||
applyBtn.click();
|
applyBtn.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(component.context.update).toHaveBeenCalled();
|
expect(component.context.update).toHaveBeenCalled();
|
||||||
expect(component.context.queryFragments[component.id]).toBe('test: "option2" OR test: "option1"');
|
expect(component.context.queryFragments[component.id]).toBe('test:"option2" OR test:"option1"');
|
||||||
|
|
||||||
|
component.settings.field = AutocompleteField.CATEGORIES;
|
||||||
|
component.selectedOptions = [{id: 'test-id', value: 'test'}];
|
||||||
|
applyBtn.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(component.context.queryFragments[component.id]).toBe('cm:categories:"workspace://SpacesStore/test-id"');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -16,12 +16,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, ViewEncapsulation, OnInit } from '@angular/core';
|
import { Component, ViewEncapsulation, OnInit } from '@angular/core';
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
||||||
import { SearchWidget } from '../../models/search-widget.interface';
|
import { SearchWidget } from '../../models/search-widget.interface';
|
||||||
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
|
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
|
||||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||||
import { SearchFilterList } from '../../models/search-filter-list.model';
|
import { SearchFilterList } from '../../models/search-filter-list.model';
|
||||||
import { TagService } from '../../../tag/services/tag.service';
|
import { TagService } from '../../../tag/services/tag.service';
|
||||||
|
import { CategoryService } from '../../../category/services/category.service';
|
||||||
|
import { AutocompleteField, AutocompleteOption } from '../../models/autocomplete-option.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'adf-search-filter-autocomplete-chips',
|
selector: 'adf-search-filter-autocomplete-chips',
|
||||||
@@ -32,18 +34,19 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
|
|||||||
id: string;
|
id: string;
|
||||||
settings?: SearchWidgetSettings;
|
settings?: SearchWidgetSettings;
|
||||||
context?: SearchQueryBuilderService;
|
context?: SearchQueryBuilderService;
|
||||||
options: SearchFilterList<string[]>;
|
options: SearchFilterList<AutocompleteOption[]>;
|
||||||
startValue: string[] = null;
|
startValue: AutocompleteOption[] = [];
|
||||||
displayValue$ = new Subject<string>();
|
displayValue$ = new Subject<string>();
|
||||||
|
selectedOptions: AutocompleteOption[] = [];
|
||||||
|
enableChangeUpdate: boolean;
|
||||||
|
|
||||||
private resetSubject$ = new Subject<void>();
|
private resetSubject$ = new Subject<void>();
|
||||||
reset$: Observable<void> = this.resetSubject$.asObservable();
|
reset$: Observable<void> = this.resetSubject$.asObservable();
|
||||||
autocompleteOptions: string[] = [];
|
private autocompleteOptionsSubject$ = new BehaviorSubject<AutocompleteOption[]>([]);
|
||||||
selectedOptions: string[] = [];
|
autocompleteOptions$: Observable<AutocompleteOption[]> = this.autocompleteOptionsSubject$.asObservable();
|
||||||
enableChangeUpdate: boolean;
|
|
||||||
|
|
||||||
constructor( private tagService: TagService ) {
|
constructor(private tagService: TagService, private categoryService: CategoryService) {
|
||||||
this.options = new SearchFilterList<string[]>();
|
this.options = new SearchFilterList<AutocompleteOption[]>();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -70,11 +73,11 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
|
|||||||
return !!this.selectedOptions;
|
return !!this.selectedOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentValue(): string[]{
|
getCurrentValue(): AutocompleteOption[] {
|
||||||
return this.selectedOptions;
|
return this.selectedOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
onOptionsChange(selectedOptions: string[]) {
|
onOptionsChange(selectedOptions: AutocompleteOption[]) {
|
||||||
this.selectedOptions = selectedOptions;
|
this.selectedOptions = selectedOptions;
|
||||||
if (this.enableChangeUpdate) {
|
if (this.enableChangeUpdate) {
|
||||||
this.updateQuery();
|
this.updateQuery();
|
||||||
@@ -82,27 +85,62 @@ export class SearchFilterAutocompleteChipsComponent implements SearchWidget, OnI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setValue(value: string[]) {
|
setValue(value: AutocompleteOption[]) {
|
||||||
this.selectedOptions = value;
|
this.selectedOptions = value;
|
||||||
this.displayValue$.next(this.selectedOptions.join(', '));
|
this.displayValue$.next(this.selectedOptions.join(', '));
|
||||||
this.submitValues();
|
this.submitValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onInputChange(value: string) {
|
||||||
|
if (this.settings.field === AutocompleteField.CATEGORIES && value) {
|
||||||
|
this.searchForExistingCategories(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
optionComparator(option1: AutocompleteOption, option2: AutocompleteOption): boolean {
|
||||||
|
return option1.id
|
||||||
|
? option1.id.toUpperCase() === option2.id.toUpperCase()
|
||||||
|
: option1.value.toUpperCase() === option2.value.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
private updateQuery() {
|
private updateQuery() {
|
||||||
this.displayValue$.next(this.selectedOptions.join(', '));
|
this.displayValue$.next(this.selectedOptions.map(option => option.value).join(', '));
|
||||||
if (this.context && this.settings && this.settings.field) {
|
if (this.context && this.settings && this.settings.field) {
|
||||||
this.context.queryFragments[this.id] = this.selectedOptions.map(val => `${this.settings.field}: "${val}"`).join(' OR ');
|
let queryFragments;
|
||||||
|
if (this.settings.field === AutocompleteField.CATEGORIES) {
|
||||||
|
queryFragments = this.selectedOptions.map(val => `${this.settings.field}:"workspace://SpacesStore/${val.id}"`);
|
||||||
|
} else {
|
||||||
|
queryFragments = this.selectedOptions.map(val => `${this.settings.field}:"${val.value}"`);
|
||||||
|
}
|
||||||
|
this.context.queryFragments[this.id] = queryFragments.join(' OR ');
|
||||||
this.context.update();
|
this.context.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setOptions() {
|
private setOptions() {
|
||||||
if (this.settings.field === 'TAG') {
|
switch (this.settings.field) {
|
||||||
this.tagService.getAllTheTags().subscribe(res => {
|
case AutocompleteField.TAG:
|
||||||
this.autocompleteOptions = res.list.entries.map(tag => tag.entry.tag);
|
this.tagService.getAllTheTags().subscribe(tagPaging => {
|
||||||
});
|
this.autocompleteOptionsSubject$.next(tagPaging.list.entries.map(tag => ({
|
||||||
} else {
|
value: tag.entry.tag
|
||||||
this.autocompleteOptions = this.settings.options;
|
})));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case AutocompleteField.CATEGORIES:
|
||||||
|
this.autocompleteOptionsSubject$.next([]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.autocompleteOptionsSubject$.next(this.settings.autocompleteOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private searchForExistingCategories(searchTerm: string) {
|
||||||
|
this.categoryService.searchCategories(searchTerm, 0, 15).subscribe((existingCategoriesResult) => {
|
||||||
|
this.autocompleteOptionsSubject$.next(existingCategoriesResult.list.entries.map((rowEntry) => {
|
||||||
|
const path = rowEntry.entry.path.name.split('/').splice(3).join('/');
|
||||||
|
const fullPath = path ? `${path}/${rowEntry.entry.name}` : rowEntry.entry.name;
|
||||||
|
return {id: rowEntry.entry.id, value: rowEntry.entry.name, fullPath};
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -42,7 +42,7 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<p class="adf-search-properties-file-type-label">{{ 'SEARCH.SEARCH_PROPERTIES.FILE_TYPE' | translate }}</p>
|
<p class="adf-search-properties-file-type-label">{{ 'SEARCH.SEARCH_PROPERTIES.FILE_TYPE' | translate }}</p>
|
||||||
<adf-search-chip-autocomplete-input
|
<adf-search-chip-autocomplete-input
|
||||||
[autocompleteOptions]="settings?.fileExtensions"
|
[autocompleteOptions]="autocompleteOptions"
|
||||||
(optionsChanged)="selectedExtensions = $event"
|
(optionsChanged)="selectedExtensions = $event"
|
||||||
[onReset$]="reset$"
|
[onReset$]="reset$"
|
||||||
[allowOnlyPredefinedValues]="false"
|
[allowOnlyPredefinedValues]="false"
|
||||||
|
@@ -140,9 +140,10 @@ describe('SearchPropertiesComponent', () => {
|
|||||||
field: 'field',
|
field: 'field',
|
||||||
fileExtensions: ['pdf', 'doc', 'txt']
|
fileExtensions: ['pdf', 'doc', 'txt']
|
||||||
};
|
};
|
||||||
|
component.ngOnInit();
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(searchChipAutocompleteInputComponent.autocompleteOptions).toBe(component.settings.fileExtensions);
|
expect(searchChipAutocompleteInputComponent.autocompleteOptions).toEqual([{value: 'pdf'}, {value: 'doc'}, {value: 'txt'}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set onReset$ for SearchChipAutocompleteInputComponent to correct value', () => {
|
it('should set onReset$ for SearchChipAutocompleteInputComponent to correct value', () => {
|
||||||
@@ -154,10 +155,10 @@ describe('SearchPropertiesComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should compare file extensions case insensitive after calling compareOption on SearchChipAutocompleteInputComponent', () => {
|
it('should compare file extensions case insensitive after calling compareOption on SearchChipAutocompleteInputComponent', () => {
|
||||||
const option1 = 'pdf';
|
const option1 = {value: 'pdf'};
|
||||||
const option2 = 'PdF';
|
const option2 = {value: 'PdF'};
|
||||||
expect(searchChipAutocompleteInputComponent.compareOption(option1, option2)).toBeTrue();
|
expect(searchChipAutocompleteInputComponent.compareOption(option1, option2)).toBeTrue();
|
||||||
expect(searchChipAutocompleteInputComponent.compareOption(option1, `${option2}1`)).toBeFalse();
|
expect(searchChipAutocompleteInputComponent.compareOption(option1, {value: `${option2.value}1`})).toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove preceding dot after calling formatChipValue on SearchChipAutocompleteInputComponent', () => {
|
it('should remove preceding dot after calling formatChipValue on SearchChipAutocompleteInputComponent', () => {
|
||||||
@@ -167,11 +168,11 @@ describe('SearchPropertiesComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should filter file extensions case insensitive without dots after calling filter on SearchChipAutocompleteInputComponent', () => {
|
it('should filter file extensions case insensitive without dots after calling filter on SearchChipAutocompleteInputComponent', () => {
|
||||||
const extensions = ['pdf', 'jpg', 'txt', 'png'];
|
const extensions = [{value: 'pdf'}, {value: 'jpg'}, {value: 'txt'}, {value: 'png'}];
|
||||||
const searchValue = 'p';
|
const searchValue = 'p';
|
||||||
|
|
||||||
expect(searchChipAutocompleteInputComponent.filter(extensions, searchValue)).toEqual(['pdf', 'jpg', 'png']);
|
expect(searchChipAutocompleteInputComponent.filter(extensions, searchValue)).toEqual([{value:'pdf'}, {value:'jpg'}, {value:'png'}]);
|
||||||
expect(searchChipAutocompleteInputComponent.filter(extensions, `.${searchValue}`)).toEqual(['pdf', 'png']);
|
expect(searchChipAutocompleteInputComponent.filter(extensions, `.${searchValue}`)).toEqual([{value:'pdf'}, {value:'png'}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set placeholder for SearchChipAutocompleteInputComponent to correct value', () => {
|
it('should set placeholder for SearchChipAutocompleteInputComponent to correct value', () => {
|
||||||
@@ -259,17 +260,17 @@ describe('SearchPropertiesComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should search by single file type', () => {
|
it('should search by single file type', () => {
|
||||||
const extension = 'pdf';
|
const extension = {value: 'pdf'};
|
||||||
getSearchChipAutocompleteInputComponent().optionsChanged.emit([extension]);
|
getSearchChipAutocompleteInputComponent().optionsChanged.emit([extension]);
|
||||||
|
|
||||||
component.submitValues();
|
component.submitValues();
|
||||||
expect(component.displayValue$.next).toHaveBeenCalledWith(extension);
|
expect(component.displayValue$.next).toHaveBeenCalledWith('pdf');
|
||||||
expect(component.context.queryFragments[component.id]).toBe(`${nameField}:("*.${extension}")`);
|
expect(component.context.queryFragments[component.id]).toBe(`${nameField}:("*.${extension.value}")`);
|
||||||
expect(component.context.update).toHaveBeenCalled();
|
expect(component.context.update).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should search by multiple file types', () => {
|
it('should search by multiple file types', () => {
|
||||||
getSearchChipAutocompleteInputComponent().optionsChanged.emit(['pdf', 'txt']);
|
getSearchChipAutocompleteInputComponent().optionsChanged.emit([{value:'pdf'}, {value:'txt'}]);
|
||||||
|
|
||||||
component.submitValues();
|
component.submitValues();
|
||||||
expect(component.displayValue$.next).toHaveBeenCalledWith('pdf, txt');
|
expect(component.displayValue$.next).toHaveBeenCalledWith('pdf, txt');
|
||||||
@@ -279,7 +280,7 @@ describe('SearchPropertiesComponent', () => {
|
|||||||
|
|
||||||
it('should search by file size and type', () => {
|
it('should search by file size and type', () => {
|
||||||
typeInFileSizeInput();
|
typeInFileSizeInput();
|
||||||
getSearchChipAutocompleteInputComponent().optionsChanged.emit(['pdf', 'txt']);
|
getSearchChipAutocompleteInputComponent().optionsChanged.emit([{value:'pdf'}, {value:'txt'}]);
|
||||||
|
|
||||||
component.submitValues();
|
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.displayValue$.next).toHaveBeenCalledWith('SEARCH.SEARCH_PROPERTIES.FILE_SIZE_OPERATOR.AT_LEAST 321 SEARCH.SEARCH_PROPERTIES.FILE_SIZE_UNIT_ABBREVIATION.KB, pdf, txt');
|
||||||
@@ -315,7 +316,7 @@ describe('SearchPropertiesComponent', () => {
|
|||||||
clickFileSizeUnitsSelect();
|
clickFileSizeUnitsSelect();
|
||||||
getSelectOptions()[1].nativeElement.click();
|
getSelectOptions()[1].nativeElement.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const extensions = ['pdf', 'txt'];
|
const extensions = [{value: 'pdf'}, {value: 'txt'}];
|
||||||
getSearchChipAutocompleteInputComponent().optionsChanged.emit(extensions);
|
getSearchChipAutocompleteInputComponent().optionsChanged.emit(extensions);
|
||||||
|
|
||||||
expect(component.getCurrentValue()).toEqual({
|
expect(component.getCurrentValue()).toEqual({
|
||||||
@@ -324,7 +325,7 @@ describe('SearchPropertiesComponent', () => {
|
|||||||
fileSizeUnit: FileSizeUnit.MB,
|
fileSizeUnit: FileSizeUnit.MB,
|
||||||
fileSizeOperator: FileSizeOperator.AT_MOST
|
fileSizeOperator: FileSizeOperator.AT_MOST
|
||||||
},
|
},
|
||||||
fileExtensions: extensions
|
fileExtensions: ['pdf', 'txt']
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -342,7 +343,7 @@ describe('SearchPropertiesComponent', () => {
|
|||||||
getSelectOptions()[1].nativeElement.click();
|
getSelectOptions()[1].nativeElement.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
searchChipAutocompleteInputComponent = getSearchChipAutocompleteInputComponent();
|
searchChipAutocompleteInputComponent = getSearchChipAutocompleteInputComponent();
|
||||||
searchChipAutocompleteInputComponent.optionsChanged.emit(['pdf', 'txt']);
|
searchChipAutocompleteInputComponent.optionsChanged.emit([{value: 'pdf'}, {value: 'txt'}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reset form', () => {
|
it('should reset form', () => {
|
||||||
|
@@ -26,6 +26,7 @@ import { SearchQueryBuilderService } from '../../services/search-query-builder.s
|
|||||||
import { SearchProperties } from './search-properties';
|
import { SearchProperties } from './search-properties';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { SearchWidget } from '../../models/search-widget.interface';
|
import { SearchWidget } from '../../models/search-widget.interface';
|
||||||
|
import { AutocompleteOption } from '../../models/autocomplete-option.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'adf-search-properties',
|
selector: 'adf-search-properties',
|
||||||
@@ -39,6 +40,7 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
|
|||||||
context?: SearchQueryBuilderService;
|
context?: SearchQueryBuilderService;
|
||||||
startValue: SearchProperties;
|
startValue: SearchProperties;
|
||||||
displayValue$ = new Subject<string>();
|
displayValue$ = new Subject<string>();
|
||||||
|
autocompleteOptions: AutocompleteOption[] = [];
|
||||||
|
|
||||||
private _form = this.formBuilder.nonNullable.group<FileSizeCondition>({
|
private _form = this.formBuilder.nonNullable.group<FileSizeCondition>({
|
||||||
fileSizeOperator: FileSizeOperator.AT_LEAST,
|
fileSizeOperator: FileSizeOperator.AT_LEAST,
|
||||||
@@ -77,8 +79,8 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
|
|||||||
return this._reset$;
|
return this._reset$;
|
||||||
}
|
}
|
||||||
|
|
||||||
set selectedExtensions(extensions: string[]) {
|
set selectedExtensions(extensions: AutocompleteOption[]) {
|
||||||
this._selectedExtensions = extensions;
|
this._selectedExtensions = this.parseFromAutocompleteOptions(extensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private formBuilder: FormBuilder, private translateService: TranslateService) {}
|
constructor(private formBuilder: FormBuilder, private translateService: TranslateService) {}
|
||||||
@@ -88,6 +90,7 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
|
|||||||
if (!this.settings.fileExtensions) {
|
if (!this.settings.fileExtensions) {
|
||||||
this.settings.fileExtensions = [];
|
this.settings.fileExtensions = [];
|
||||||
}
|
}
|
||||||
|
this.autocompleteOptions = this.parseToAutocompleteOptions(this.settings.fileExtensions);
|
||||||
[this.sizeField, this.nameField] = this.settings.field.split(',');
|
[this.sizeField, this.nameField] = this.settings.field.split(',');
|
||||||
}
|
}
|
||||||
if (this.startValue) {
|
if (this.startValue) {
|
||||||
@@ -127,8 +130,8 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
|
|||||||
return event.key !== '-' && event.key !== 'e' && event.key !== '+';
|
return event.key !== '-' && event.key !== 'e' && event.key !== '+';
|
||||||
}
|
}
|
||||||
|
|
||||||
compareFileExtensions(extension1: string, extension2: string): boolean {
|
compareFileExtensions(extension1: AutocompleteOption, extension2: AutocompleteOption): boolean {
|
||||||
return extension1.toUpperCase() === extension2.toUpperCase();
|
return extension1.value.toUpperCase() === extension2.value.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
getExtensionWithoutDot(extension: string): string {
|
getExtensionWithoutDot(extension: string): string {
|
||||||
@@ -136,11 +139,11 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
|
|||||||
return extensionSplitByDot[extensionSplitByDot.length - 1];
|
return extensionSplitByDot[extensionSplitByDot.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
filterExtensions = (extensions: string[], filterValue: string): string[] => {
|
filterExtensions = (extensions: AutocompleteOption[], filterValue: string): AutocompleteOption[] => {
|
||||||
const filterValueLowerCase = this.getExtensionWithoutDot(filterValue).toLowerCase();
|
const filterValueLowerCase = this.getExtensionWithoutDot(filterValue).toLowerCase();
|
||||||
const extensionWithDot = filterValue.startsWith('.');
|
const extensionWithDot = filterValue.startsWith('.');
|
||||||
return extensions.filter((option) => {
|
return extensions.filter((option) => {
|
||||||
const optionLowerCase = option.toLowerCase();
|
const optionLowerCase = option.value.toLowerCase();
|
||||||
return extensionWithDot && filterValueLowerCase ? optionLowerCase.startsWith(filterValueLowerCase) : optionLowerCase.includes(filterValue);
|
return extensionWithDot && filterValueLowerCase ? optionLowerCase.startsWith(filterValueLowerCase) : optionLowerCase.includes(filterValue);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -196,10 +199,18 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear
|
|||||||
|
|
||||||
setValue(searchProperties: SearchProperties) {
|
setValue(searchProperties: SearchProperties) {
|
||||||
this.form.patchValue(searchProperties.fileSizeCondition);
|
this.form.patchValue(searchProperties.fileSizeCondition);
|
||||||
this.selectedExtensions = searchProperties.fileExtensions;
|
this.selectedExtensions = this.parseToAutocompleteOptions(searchProperties.fileExtensions);
|
||||||
this.submitValues();
|
this.submitValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parseToAutocompleteOptions(array: string[]): AutocompleteOption[] {
|
||||||
|
return array.map(value => ({value}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseFromAutocompleteOptions(array: AutocompleteOption[]): string[] {
|
||||||
|
return array.flatMap(option => option.value);
|
||||||
|
}
|
||||||
|
|
||||||
private getOperatorNameWidth(operator: string, font: string): number {
|
private getOperatorNameWidth(operator: string, font: string): number {
|
||||||
const context = this.canvas.getContext('2d');
|
const context = this.canvas.getContext('2d');
|
||||||
context.font = font;
|
context.font = font;
|
||||||
|
@@ -0,0 +1,27 @@
|
|||||||
|
/*!
|
||||||
|
* @license
|
||||||
|
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface AutocompleteOption {
|
||||||
|
value: string;
|
||||||
|
id?: string;
|
||||||
|
fullPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AutocompleteField {
|
||||||
|
TAG = 'TAG',
|
||||||
|
CATEGORIES = 'cm:categories'
|
||||||
|
}
|
@@ -15,6 +15,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { AutocompleteOption } from './autocomplete-option.interface';
|
||||||
|
|
||||||
export interface SearchWidgetSettings {
|
export interface SearchWidgetSettings {
|
||||||
field: string;
|
field: string;
|
||||||
/* allow the user to update search in every change */
|
/* allow the user to update search in every change */
|
||||||
@@ -27,6 +29,8 @@ export interface SearchWidgetSettings {
|
|||||||
format?: string;
|
format?: string;
|
||||||
/* allow the user to search only within predefined options */
|
/* allow the user to search only within predefined options */
|
||||||
allowOnlyPredefinedValues?: boolean;
|
allowOnlyPredefinedValues?: boolean;
|
||||||
|
/* allow the user to predefine autocomplete options */
|
||||||
|
autocompleteOptions?: AutocompleteOption[];
|
||||||
|
|
||||||
[indexer: string]: any;
|
[indexer: string]: any;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user