Skip to content

Commit

Permalink
[ACS-4130] Added autocomplete to folder rules 'Has Category' condition (
Browse files Browse the repository at this point in the history
#3464)

* [ACS-4130] Added autocomplete for 'Has Category' option in manage rules

* [ACS-4130] Added loading spinner and 'No options found' template for Has Category rule condition. Options are now fetched as soon as user selected 'Has Category' option

* [ACS-4130] Added code to fetch category name when viewing/editing existing rule with has category option selected

* [ACS-4130] Resolved issues related to editing existing rules with 'Has Category' condition

* [ACS-4130] Added safety checks and minor code refactoring

* [ACS-4130] Added unit tests for new autocomplete functionality

* [ACS-4130] Added feature to auto select first option from autocomplete dropdown when user focuses out of autocomplete input field

* [ACS-4130] Minor code refactoring. Moved constants from global scope to local scope

* [ACS-4130] Moved mock data to conditions.mock.ts. Removed redundant return type

* [ACS-4130] Resolved PR review comments - AutoCompleteOption is now an interface. Changed occurences of autocomplete with auto-complete. Removed/Added types

* [ACS-4130] Resolved PR review comments - AutoCompleteOption is now built using a single common helper method

* [ACS-4130] Added missed types
  • Loading branch information
swapnil-verma-gl authored Oct 11, 2023
1 parent c7e2912 commit ec18f6b
Show file tree
Hide file tree
Showing 7 changed files with 350 additions and 23 deletions.
3 changes: 3 additions & 0 deletions projects/aca-content/folder-rules/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@
},
"ERRORS": {
"DELETE_RULE_SET_LINK_FAILED": "Error while trying to delete a link from a rule set"
},
"AUTOCOMPLETE": {
"NO_OPTIONS_FOUND": "No options found"
}
}
}
57 changes: 55 additions & 2 deletions projects/aca-content/folder-rules/src/mock/conditions.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,65 @@ export const mimeTypeMock: RuleSimpleCondition = {
parameter: ''
};

export const categoryMock: RuleSimpleCondition = {
field: 'category',
export const tagMock: RuleSimpleCondition = {
field: 'tag',
comparator: 'equals',
parameter: ''
};

export const categoriesListMock = {
list: {
pagination: {
count: 3,
hasMoreItems: false,
totalItems: 0,
skipCount: 0,
maxItems: 25
},
entries: [
{
entry: {
path: {
name: '/a/fake/category/path/1'
},
hasChildren: false,
name: 'FakeCategory1',
id: 'fake-category-id-1',
nodeType: 'cm:category',
isFile: false,
isFolder: false
}
},
{
entry: {
path: {
name: '/a/fake/category/path/2'
},
hasChildren: false,
name: 'FakeCategory2',
id: 'fake-category-id-2',
nodeType: 'cm:category',
isFile: false,
isFolder: false
}
},
{
entry: {
path: {
name: '/a/fake/category/path/3'
},
hasChildren: false,
name: 'FakeCategory3',
id: 'fake-category-id-3',
nodeType: 'cm:category',
isFile: false,
isFolder: false
}
}
]
}
};

export const simpleConditionUnknownFieldMock: RuleSimpleCondition = {
field: 'unknown-field',
comparator: 'equals',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/

export type RuleConditionFieldType = 'string' | 'number' | 'date' | 'type' | 'special' | 'mimeType';
export type RuleConditionFieldType = 'string' | 'number' | 'date' | 'type' | 'special' | 'mimeType' | 'auto-complete';

export interface RuleConditionField {
name: string;
label: string;
type: RuleConditionFieldType;
}

export const comparatorHiddenForConditionFieldType: string[] = ['special', 'mimeType'];
export const comparatorHiddenForConditionFieldType: string[] = ['special', 'mimeType', 'auto-complete'];

export const ruleConditionFields: RuleConditionField[] = [
{
Expand All @@ -56,7 +56,7 @@ export const ruleConditionFields: RuleConditionField[] = [
{
name: 'category',
label: 'ACA_FOLDER_RULES.RULE_DETAILS.FIELDS.HAS_CATEGORY',
type: 'special'
type: 'auto-complete'
},
{
name: 'tag',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,54 @@
</mat-select>
</mat-form-field>

<mat-form-field class="aca-rule-simple-condition__form__parameter-input">
<mat-select formControlName="parameter" data-automation-id="simple-condition-value-select" *ngIf="selectedField?.type === 'mimeType'; else valueInput">
<mat-form-field class="aca-rule-simple-condition__form__parameter-input" [ngSwitch]="selectedField.type">
<mat-select formControlName="parameter" data-automation-id="simple-condition-value-select" *ngSwitchCase="'mimeType'">
<mat-option *ngFor="let mimeType of mimeTypes"
[value]="mimeType.value">
{{ mimeType.label }}
</mat-option>
</mat-select>
<ng-template #valueInput>
<ng-template [ngSwitchCase]="'auto-complete'">
<input
matInput
[matAutocomplete]="auto"
formControlName="parameter"
(focusout)="autoSelectValidOption()"
data-automation-id="auto-complete-input-field"
/>
<mat-autocomplete
#auto="matAutocomplete"
data-automation-id="folder-rule-auto-complete"
[autoActiveFirstOption]="true"
[autoSelectActiveOption]="true"
[displayWith]="autoCompleteDisplayFunction">
<mat-option disabled *ngIf="showLoadingSpinner; else optionList">
<span class="aca-rule-simple-condition__auto-complete-loading-spinner">
<mat-progress-spinner
color="primary"
mode="indeterminate"
data-automation-id="auto-complete-loading-spinner"
[diameter]="25"
></mat-progress-spinner>
</span>
</mat-option>
<ng-template #optionList>
<ng-container *ngIf="autoCompleteOptions?.length > 0; else noOptionsTemplate">
<mat-option
*ngFor="let option of autoCompleteOptions"
[value]="option.value">
{{ option.displayLabel }}
</mat-option>
</ng-container>
<ng-template #noOptionsTemplate>
<mat-option disabled>
{{ 'ACA_FOLDER_RULES.AUTOCOMPLETE.NO_OPTIONS_FOUND' | translate }}
</mat-option>
</ng-template>
</ng-template>
</mat-autocomplete>
</ng-template>
<ng-template ngSwitchDefault>
<input matInput placeholder="{{ 'ACA_FOLDER_RULES.RULE_DETAILS.PLACEHOLDER.VALUE' | translate }}" type="text" formControlName="parameter" data-automation-id="value-input">
</ng-template>
</mat-form-field>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,11 @@
}
}
}

&__auto-complete-loading-spinner {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,21 @@
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, discardPeriodicTasks, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { RuleSimpleConditionUiComponent } from './rule-simple-condition.ui-component';
import { CoreTestingModule } from '@alfresco/adf-core';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { categoryMock, mimeTypeMock, simpleConditionUnknownFieldMock } from '../../mock/conditions.mock';
import { tagMock, mimeTypeMock, simpleConditionUnknownFieldMock, categoriesListMock } from '../../mock/conditions.mock';
import { MimeType } from './rule-mime-types';
import { CategoryService } from '@alfresco/adf-content-services';
import { of } from 'rxjs';
import { RuleSimpleCondition } from '../../model/rule-simple-condition.model';
import { delay } from 'rxjs/operators';

describe('RuleSimpleConditionUiComponent', () => {
let fixture: ComponentFixture<RuleSimpleConditionUiComponent>;
let categoryService: CategoryService;

const getByDataAutomationId = (dataAutomationId: string): DebugElement =>
fixture.debugElement.query(By.css(`[data-automation-id="${dataAutomationId}"]`));
Expand All @@ -45,12 +50,20 @@ describe('RuleSimpleConditionUiComponent', () => {
fixture.detectChanges();
};

const setValueInInputField = (inputFieldDataAutomationId: string, value: string) => {
const inputField = fixture.debugElement.query(By.css(`[data-automation-id="${inputFieldDataAutomationId}"]`)).nativeElement;
inputField.value = value;
inputField.dispatchEvent(new Event('input'));
fixture.detectChanges();
};

beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreTestingModule, RuleSimpleConditionUiComponent]
});

fixture = TestBed.createComponent(RuleSimpleConditionUiComponent);
categoryService = TestBed.inject(CategoryService);
});

it('should default the field to the name, the comparator to equals and the value empty', () => {
Expand Down Expand Up @@ -87,6 +100,20 @@ describe('RuleSimpleConditionUiComponent', () => {
expect(getComputedStyle(comparatorFormField).display).toBe('none');
});

it('should hide the comparator select box if the type of the field is autoComplete', () => {
const autoCompleteField = 'category';
fixture.detectChanges();
const comparatorFormField = getByDataAutomationId('comparator-form-field').nativeElement;

expect(fixture.componentInstance.isComparatorHidden).toBeFalsy();
expect(getComputedStyle(comparatorFormField).display).not.toBe('none');

changeMatSelectValue('field-select', autoCompleteField);

expect(fixture.componentInstance.isComparatorHidden).toBeTruthy();
expect(getComputedStyle(comparatorFormField).display).toBe('none');
});

it('should set the comparator to equals if the field is set to a type with different comparators', () => {
const onChangeFieldSpy = spyOn(fixture.componentInstance, 'onChangeField').and.callThrough();
fixture.detectChanges();
Expand Down Expand Up @@ -165,9 +192,104 @@ describe('RuleSimpleConditionUiComponent', () => {

expect(getByDataAutomationId('simple-condition-value-select')).toBeTruthy();

fixture.componentInstance.writeValue(categoryMock);
fixture.componentInstance.writeValue(tagMock);
fixture.detectChanges();

expect(getByDataAutomationId('value-input').nativeElement.value).toBe('');
});

it('should provide auto-complete option when category is selected', () => {
fixture.detectChanges();
changeMatSelectValue('field-select', 'category');

expect(getByDataAutomationId('auto-complete-input-field')).toBeTruthy();
expect(fixture.componentInstance.form.get('parameter').value).toEqual('');
});

it('should fetch category list when category option is selected', fakeAsync(() => {
spyOn(categoryService, 'searchCategories').and.returnValue(of(categoriesListMock));

fixture.detectChanges();
changeMatSelectValue('field-select', 'category');
tick(500);

expect(categoryService.searchCategories).toHaveBeenCalledWith('');
}));

it('should fetch new category list with user input when user types into parameter field after category option is select', fakeAsync(() => {
const categoryValue = 'a new category';
spyOn(categoryService, 'searchCategories').and.returnValue(of(categoriesListMock));

fixture.detectChanges();
changeMatSelectValue('field-select', 'category');
tick(500);
expect(categoryService.searchCategories).toHaveBeenCalledWith('');

setValueInInputField('auto-complete-input-field', categoryValue);
tick(500);
expect(categoryService.searchCategories).toHaveBeenCalledWith(categoryValue);
}));

it('should fetch category details when a saved rule with category condition is edited', () => {
const savedCategoryMock: RuleSimpleCondition = {
field: 'category',
comparator: 'equals',
parameter: 'a-fake-category-id'
};

const fakeCategory = {
entry: {
path: '/a/fake/category/path',
hasChildren: false,
name: 'FakeCategory',
id: 'fake-category-id-1'
}
};
spyOn(categoryService, 'getCategory').and.returnValue(of(fakeCategory));

fixture.componentInstance.writeValue(savedCategoryMock);
fixture.detectChanges();

expect(categoryService.getCategory).toHaveBeenCalledWith(savedCategoryMock.parameter, { include: ['path'] });
});

it('should show loading spinner while auto-complete options are fetched, and then remove it once it is received', fakeAsync(() => {
spyOn(categoryService, 'searchCategories').and.returnValue(of(categoriesListMock).pipe(delay(1000)));
fixture.detectChanges();
changeMatSelectValue('field-select', 'category');
tick(500);
getByDataAutomationId('auto-complete-input-field')?.nativeElement?.click();
let loadingSpinner = getByDataAutomationId('auto-complete-loading-spinner');
expect(loadingSpinner).not.toBeNull();
tick(1000);
fixture.detectChanges();
loadingSpinner = getByDataAutomationId('auto-complete-loading-spinner');
expect(loadingSpinner).toBeNull();
discardPeriodicTasks();
}));

it('should display correct label for category when user selects a category from auto-complete dropdown', fakeAsync(() => {
spyOn(categoryService, 'searchCategories').and.returnValue(of(categoriesListMock));
fixture.detectChanges();
changeMatSelectValue('field-select', 'category');
tick(500);
getByDataAutomationId('auto-complete-input-field')?.nativeElement?.click();
changeMatSelectValue('folder-rule-auto-complete', categoriesListMock.list.entries[0].entry.id);
const displayValue = getByDataAutomationId('auto-complete-input-field')?.nativeElement?.value;
expect(displayValue).toBe('category/path/1/FakeCategory1');
discardPeriodicTasks();
}));

it('should automatically select first category when user focuses out of parameter form field with category option selected', fakeAsync(() => {
spyOn(categoryService, 'searchCategories').and.returnValue(of(categoriesListMock));
fixture.detectChanges();
changeMatSelectValue('field-select', 'category');
tick(500);
const autoCompleteInputField = getByDataAutomationId('auto-complete-input-field')?.nativeElement;
autoCompleteInputField.value = 'FakeCat';
autoCompleteInputField.dispatchEvent(new Event('focusout'));
const parameterValue = fixture.componentInstance.form.get('parameter').value;
expect(parameterValue).toEqual(categoriesListMock.list.entries[0].entry.id);
discardPeriodicTasks();
}));
});
Loading

0 comments on commit ec18f6b

Please sign in to comment.