Skip to content

Commit

Permalink
[ACS-5181] Logical search components (#8616)
Browse files Browse the repository at this point in the history
* [ACS-5181] Add logical search component

* [ACS-5181] Add unit tests to logical search components

* [ACS-5181] Logical filter components docs

* [ACS-5181] CR fixes

* [ACS-5181] CR and accessibility fixes
  • Loading branch information
MichalKinas committed Jun 5, 2023
1 parent 9845b1e commit 85fd988
Show file tree
Hide file tree
Showing 18 changed files with 756 additions and 1 deletion.
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,13 +286,15 @@ for more information about installing and using the source code.
| [Permission List Component](content-services/components/permission-list.component.md) | Shows node permissions as a table. | [Source](../lib/content-services/src/lib/permission-manager/components/permission-list/permission-list.component.ts) |
| [Rating component](content-services/components/rating.component.md) | Allows a user to add and remove rating to an item. | [Source](../lib/content-services/src/lib/social/rating.component.ts) |
| [Search check list component](content-services/components/search-check-list.component.md) | Implements a checklist widget for the Search Filter component. | [Source](../lib/content-services/src/lib/search/components/search-check-list/search-check-list.component.ts) |
| [Search Chip Input Component](content-services/components/search-chip-input.component.md) | Displays input for providing phrases display as "chips". | [Source](../lib/content-services/src/lib/search/components/search-chip-input/search-chip-input.component.ts) |
| [Search Chip List Component](content-services/components/search-chip-list.component.md) | Displays search criteria as a set of "chips". | [Source](../lib/content-services/src/lib/search/components/search-chip-list/search-chip-list.component.ts) |
| [Search control component](content-services/components/search-control.component.md) | Displays a input text that shows find-as-you-type suggestions. | [Source](../lib/content-services/src/lib/search/components/search-control.component.ts) |
| [Search date range component](content-services/components/search-date-range.component.md) | Implements a search widget for the Search Filter component. | [Source](../lib/content-services/src/lib/search/components/search-date-range/search-date-range.component.ts) |
| [Search datetime range component](content-services/components/search-datetime-range.component.md) | Implements a search widget for the Search Filter component. | [Source](../lib/content-services/src/lib/search/components/search-datetime-range/search-datetime-range.component.ts) |
| [Search Filter Chips component](content-services/components/search-filter-chips.component.md) | Represents a chip based container component for custom search and faceted search settings. | [Source](../lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.ts) |
| [Search Filter component](content-services/components/search-filter.component.md) | Represents a main container component for custom search and faceted search settings. | [Source](../lib/content-services/src/lib/search/components/search-filter/search-filter.component.ts) |
| [Search Form component](content-services/components/search-form.component.md) | Search Form screenshot | [Source](../lib/content-services/src/lib/search/components/search-form/search-form.component.ts) |
| [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 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) |
Expand Down
49 changes: 49 additions & 0 deletions docs/content-services/components/search-chip-input.component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
Title: Search Chip Input component
Added: v6.1.0
Status: Active
Last reviewed: 2023-06-01
---

# [Search Chip Input component](../../../lib/content-services/src/lib/search/components/search-chip-input/search-chip-input.component.ts "Defined in search-chip-input.component.ts")

Represents an input with stacked list of chips as phrases added through input.

![Search Chip Input](../../docassets/images/search-chip-input.png)

## Basic usage

```html
<adf-search-chip-input
[label]="'Some label'"
[onReset]="onResetObservable"
(phrasesChanged)="handlePhraseChanged($event)">
</adf-search-chip-input>
```

### Properties

| Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- |
| label | `string` | | Label that will be associated with the input |
| addOnBlur | `boolean` | true | Specifies whether new phrase will be added when input blurs |
| onReset | [`Observable`](https://rxjs.dev/guide/observable)`<void>` | | Observable that will listen to any reset event causing component to clear the chips and input |

### Events

| Name | Type | Description |
| ---- | ---- | ----------- |
| phrasesChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string[]>` | Emitted when new phrase is entered |

## 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 Logical Filter component](search-logical-filter.component.md)
- [Search check list component](search-check-list.component.md)
- [Search date range component](search-date-range.component.md)
- [Search number range component](search-number-range.component.md)
- [Search radio component](search-radio.component.md)
- [Search slider component](search-slider.component.md)
- [Search text component](search-text.component.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
Title: Search Logical Filter component
Added: v6.1.0
Status: Active
Last reviewed: 2023-06-01
---

# [Search Logical Filter component](../../../lib/content-services/src/lib/search/components/search-logical-filter/search-logical-filter.component.ts "Defined in search-logical-filter.component.ts")

Implements a [search widget](../../../lib/content-services/src/lib/search/models/search-widget.interface.ts) consisting of 3 chip inputs representing logical conditions to form search query from.

![Search Logical Filter](../../docassets/images/search-logical-filter.png)

## Basic usage

```json
{
"search": {
"categories": [
{
"id": "logic",
"name": "Logic",
"enabled": true,
"component": {
"selector": "logical-filter",
"settings": {
"allowUpdateOnChange": false,
"hideDefaultAction": true,
"field": "cm:name,cm:title,TEXT"
}
}
}
]
}
}
```

### Settings

| Name | Type | Description |
| ---- | ---- | ----------- |
| field | string | Field/fields to apply the query to. Required value |
| hideDefaultAction | boolean | Show/hide the [widget](../../../lib/testing/src/lib/protractor/core/pages/form/widgets/widget.ts) actions. By default is false. |

## Details

This component lets the user provide logical conditions to apply to each `field` in the search query.
See the [Search chip input component](search-chip-input.component.md) for full details of how to use chip inputs.

## See also

- [Search Configuration Guide](../../user-guide/search-configuration-guide.md)
- [Search Query Builder service](../services/search-query-builder.service.md)
- [Search Widget Interface](../interfaces/search-widget.interface.md)
- [Search Chip Input component](search-chip-input.component.md)
- [Search check list component](search-check-list.component.md)
- [Search date range component](search-date-range.component.md)
- [Search number range component](search-number-range.component.md)
- [Search radio component](search-radio.component.md)
- [Search slider component](search-slider.component.md)
- [Search text component](search-text.component.md)
Binary file added docs/docassets/images/search-chip-input.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/docassets/images/search-logical-filter.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions docs/versionIndex.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ backend services have been tested with each released version of ADF.
<!--v610 start-->

- [Use none component view encapsulation](eslint-angular/rules/use-none-component-view-encapsulation.md)
- [Search Chip Input component](content-services/components/search-chip-input.component.md)
- [Search Logical Filter component](content-services/components/search-logical-filter.component.md)

<!--v610 end-->

Expand Down
11 changes: 11 additions & 0 deletions lib/content-services/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,17 @@
"TITLE" : "Created Date (range)"
}
}
},
"LOGICAL_SEARCH": {
"SEARCH_CHIP_INPUT": {
"ADD_PHRASE": "Add phrase..."
},
"MATCH_ALL": "ALL",
"MATCH_ALL_LABEL": "Match ALL of these phrases",
"MATCH_ANY": "ANY",
"MATCH_ANY_LABEL": "Match ANY of these phrases",
"EXCLUDE": "EXCLUDE",
"EXCLUDE_LABEL": "EXCLUDE these phrases"
}
},
"PERMISSION": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<mat-label>{{label | translate}}</mat-label>
<mat-chip-list #chipList [attr.aria-label]="label | translate">
<mat-chip *ngFor="let phrase of phrases; let i = index" (removed)="removePhrase(i)">
<span>{{phrase}}</span>
<button matChipRemove [attr.aria-label]="('SEARCH.FILTER.BUTTONS.REMOVE' | translate) + ' ' + phrase">
<mat-icon>cancel</mat-icon>
</button>
</mat-chip>
<input placeholder="{{ 'SEARCH.LOGICAL_SEARCH.SEARCH_CHIP_INPUT.ADD_PHRASE' | translate }}"
[attr.aria-label]="'SEARCH.LOGICAL_SEARCH.SEARCH_CHIP_INPUT.ADD_PHRASE' | translate"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="addOnBlur"
(matChipInputTokenEnd)="addPhrase($event)" />
</mat-chip-list>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.adf-search-chip-input {
padding-bottom: 15px;

.mat-chip-list-wrapper {
border: 1px solid var(--adf-theme-mat-grey-color-a400);
border-radius: 5px;
margin-top: 5px;

.mat-chip {
word-break: break-all;
height: unset;
}

input {
height: 25px;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*!
* @license
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatChip, MatChipRemove } from '@angular/material/chips';
import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
import { Subject } from 'rxjs';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { SearchChipInputComponent } from './search-chip-input.component';

describe('SearchChipInputComponent', () => {
let component: SearchChipInputComponent;
let fixture: ComponentFixture<SearchChipInputComponent>;
const onResetSubject = new Subject<void>();

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [SearchChipInputComponent],
imports: [
TranslateModule.forRoot(),
ContentTestingModule
]
});

fixture = TestBed.createComponent(SearchChipInputComponent);
component = fixture.componentInstance;
component.onReset = onResetSubject.asObservable();
fixture.detectChanges();
});

afterEach(() => removeAllChips());

function getChipInput(): HTMLInputElement {
return fixture.debugElement.query(By.css('input')).nativeElement;
}

function getChipList(): MatChip[] {
return fixture.debugElement.queryAll(By.css('mat-chip')).map((chip) => chip.nativeElement);
}

function getChipValue(index: number): string {
return fixture.debugElement.queryAll(By.css('mat-chip span')).map((chip) => chip.nativeElement)[index].innerText;
}

function enterNewChip(value: string) {
const input = getChipInput();
input.value = value;
fixture.detectChanges();
input.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 13}));
fixture.detectChanges();
}

function removeAllChips() {
const chips = getChipList();
if (!!chips && chips.length > 0) {
chips.forEach((chip) => chip.remove());
}
}

function removeChip(index: number) {
const removeBtns = fixture.debugElement.queryAll(By.directive(MatChipRemove)).map((removeBtn) => removeBtn.nativeElement);
removeBtns[index].click();
fixture.detectChanges();
}

it('should display label provided as component input', () => {
const label = 'Test';
component.label = label;
fixture.detectChanges();
const matLabel = fixture.debugElement.query(By.css('mat-label')).nativeElement.innerText;
expect(matLabel).toBe(label);
});

it('should display proper placeholder for chip input', () => {
const input = getChipInput();
expect(input.placeholder).toBe('SEARCH.LOGICAL_SEARCH.SEARCH_CHIP_INPUT.ADD_PHRASE');
});

it('should not display any chips initially', () => {
const chips = getChipList();
expect(chips).toEqual([]);
});

it('should add new chip when input has value and enter was hit', () => {
const phrasesChangedSpy = spyOn(component.phrasesChanged, 'emit');
enterNewChip('test');
expect(phrasesChangedSpy).toHaveBeenCalledOnceWith(['test']);
expect(getChipList().length).toBe(1);
expect(getChipValue(0)).toBe('test');
});

it('should add input value as whole phrase even if it contains whitespaces and special signs', () => {
const phrase = 'test another world &*,.;""!@#$$%^*()[]-+=';
enterNewChip(phrase);
expect(getChipList().length).toBe(1);
expect(getChipValue(0)).toBe(phrase);
});

it('should add new chip when input is blurred', () => {
const input = getChipInput();
input.value = 'test';
fixture.detectChanges();
input.dispatchEvent(new InputEvent('blur'));
fixture.detectChanges();
expect(input.value).toBe('');
expect(getChipList().length).toBe(1);
expect(getChipValue(0)).toBe('test');
});

it('should not add new chip when input is blurred if addOnBlur is false', () => {
component.addOnBlur = false;
const input = getChipInput();
input.value = 'test';
fixture.detectChanges();
input.dispatchEvent(new InputEvent('blur'));
fixture.detectChanges();
expect(input.value).toBe('test');
expect(getChipList().length).toBe(0);
});

it('should clear the input after new chip is added', () => {
const input = getChipInput();
enterNewChip('test2');
expect(input.value).toBe('');
});

it('should reset all chips when onReset event is emitted', () => {
enterNewChip('test1');
enterNewChip('test2');
enterNewChip('test3');
const phrasesChangedSpy = spyOn(component.phrasesChanged, 'emit');
onResetSubject.next();
fixture.detectChanges();
expect(phrasesChangedSpy).toHaveBeenCalledOnceWith([]);
expect(getChipList()).toEqual([]);
});

it('should remove chip upon clicking remove button', () => {
enterNewChip('test1');
enterNewChip('test2');
const phrasesChangedSpy = spyOn(component.phrasesChanged, 'emit');
removeChip(0);
expect(phrasesChangedSpy).toHaveBeenCalledOnceWith(['test2']);
expect(getChipList().length).toEqual(1);
});
});

0 comments on commit 85fd988

Please sign in to comment.