[ACA-4258] Add Task Filter Counters (#6556)

* [ACA-4258] Add Task Filter Counters

* Fix unit test
This commit is contained in:
davidcanonieto
2021-01-26 19:15:30 +01:00
committed by GitHub
parent 5258a34a5d
commit 22da6c2c57
11 changed files with 109 additions and 23 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -56,6 +56,12 @@ as the value of `filterParam` as shown in the table below:
| key | string | The key of the task filter | | key | string | The key of the task filter |
| index | string | The zero-based position of the filter in the array. | | index | string | The zero-based position of the filter in the array. |
### Showing Filter Counters
By default, filter counters are hidden. If you want to display filter counters you will need to add the `showCounter` property set to `true` in your TaskFilterCloudModel.
![](../../docassets/images/task-filter-counter.png)
## See also ## See also
* [Task filter Cloud Service](../services/task-filter-cloud.service.md) * [Task filter Cloud Service](../services/task-filter-cloud.service.md)

View File

@@ -1,14 +1,18 @@
<ng-container *ngIf="filters$ | async as filterList; else loading"> <ng-container *ngIf="filters$ | async as filterList; else loading">
<div *ngFor="let filter of filterList" class="adf-filters__entry" [class.adf-active]="currentFilter === filter"> <div *ngFor="let filter of filterList" class="adf-task-filters__entry">
<button (click)="onFilterClick(filter)" <button (click)="onFilterClick(filter)"
[attr.aria-label]="filter.name | translate" [attr.aria-label]="filter.name | translate"
[id]="filter.id" [id]="filter.id"
[attr.data-automation-id]="filter.key + '_filter'" [attr.data-automation-id]="filter.key + '_filter'"
mat-button mat-button
[class.adf-active]="currentFilter === filter"
class="adf-filter-action-button adf-full-width" fxLayout="row" fxLayoutAlign="space-between center"> class="adf-filter-action-button adf-full-width" fxLayout="row" fxLayoutAlign="space-between center">
<adf-icon data-automation-id="adf-filter-icon" *ngIf="showIcons" [value]="filter.icon"></adf-icon> <adf-icon data-automation-id="adf-filter-icon" *ngIf="showIcons" [value]="filter.icon"></adf-icon>
<span data-automation-id="adf-filter-label" class="adf-filter-action-button__label">{{ filter.name | translate }}</span> <span data-automation-id="adf-filter-label" class="adf-filter-action-button__label">{{ filter.name | translate }}</span>
</button> </button>
<span *ngIf="counters$[filter.key]" class="adf-filter-action-button__counter">
{{ counters$[filter.key] | async }}
</span>
</div> </div>
</ng-container> </ng-container>
<ng-template #loading> <ng-template #loading>

View File

@@ -2,28 +2,45 @@
$primary: map-get($theme, primary); $primary: map-get($theme, primary);
.adf { .adf {
&-filters__entry { &-task-filters__entry {
display: flex;
padding: 12px 0 !important; padding: 12px 0 !important;
height: 24px; height: 24px;
width: 100%; width: 100%;
cursor: pointer; cursor: pointer;
font-size: 14px !important; font-size: 14px !important;
font-weight: bold; font-weight: bold;
opacity: 0.54; opacity: 1;
.adf-full-width { .adf-full-width {
display: flex; display: flex;
width: 100%; width: 100%;
} }
.adf-filter-action-button .adf-filter-action-button__label { .adf-filter-action-button {
opacity: 0.54;
padding: 16px;
.adf-filter-action-button__label {
padding-left: 20px; padding-left: 20px;
margin: 0 8px !important; margin: 0 8px !important;
} }
} }
&-filters__entry {
&.adf-active, .adf-filter-action-button__counter {
opacity: 0.54;
padding-left: 10px;
padding-top: 6px;
}
&:hover { &:hover {
color: mat-color($primary);
.adf-filter-action-button__counter, .adf-filter-action-button {
opacity: 1;
}
}
.adf-active {
color: mat-color($primary); color: mat-color($primary);
opacity: 1; opacity: 1;
} }

View File

@@ -16,7 +16,7 @@
*/ */
import { EventEmitter, Input, Output, OnDestroy, Directive } from '@angular/core'; import { EventEmitter, Input, Output, OnDestroy, Directive } from '@angular/core';
import { Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { FilterParamsModel } from '../models/filter-cloud.model'; import { FilterParamsModel } from '../models/filter-cloud.model';
@Directive() @Directive()
@@ -45,6 +45,8 @@ export abstract class BaseTaskFiltersCloudComponent implements OnDestroy {
@Output() @Output()
error: EventEmitter<any> = new EventEmitter<any>(); error: EventEmitter<any> = new EventEmitter<any>();
counters$: {[key: string]: Observable<number>} = {};
protected onDestroy$ = new Subject<boolean>(); protected onDestroy$ = new Subject<boolean>();
ngOnDestroy() { ngOnDestroy() {

View File

@@ -111,7 +111,7 @@ describe('ServiceTaskFiltersCloudComponent', () => {
component.showIcons = true; component.showIcons = true;
fixture.whenStable().then(() => { fixture.whenStable().then(() => {
fixture.detectChanges(); fixture.detectChanges();
const filters = fixture.debugElement.queryAll(By.css('.adf-filters__entry')); const filters = fixture.debugElement.queryAll(By.css('.adf-task-filters__entry'));
expect(component.filters.length).toBe(3); expect(component.filters.length).toBe(3);
expect(filters.length).toBe(3); expect(filters.length).toBe(3);
expect(filters[0].nativeElement.innerText).toContain('FakeServiceTasks'); expect(filters[0].nativeElement.innerText).toContain('FakeServiceTasks');

View File

@@ -18,7 +18,7 @@
import { SimpleChange } from '@angular/core'; import { SimpleChange } from '@angular/core';
import { ComponentFixture, TestBed, async } from '@angular/core/testing'; import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { setupTestBed } from '@alfresco/adf-core'; import { setupTestBed } from '@alfresco/adf-core';
import { from, Observable } from 'rxjs'; import { from, Observable, of } from 'rxjs';
import { TASK_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.service'; import { TASK_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.service';
import { LocalPreferenceCloudService } from '../../../services/local-preference-cloud.service'; import { LocalPreferenceCloudService } from '../../../services/local-preference-cloud.service';
import { TaskFilterCloudService } from '../services/task-filter-cloud.service'; import { TaskFilterCloudService } from '../services/task-filter-cloud.service';
@@ -68,6 +68,7 @@ describe('TaskFiltersCloudComponent', () => {
component = fixture.componentInstance; component = fixture.componentInstance;
taskFilterService = TestBed.inject(TaskFilterCloudService); taskFilterService = TestBed.inject(TaskFilterCloudService);
spyOn(taskFilterService, 'getTaskFilterCounter').and.returnValue(of(11));
}); });
it('should attach specific icon for each filter if hasIcon is true', async(() => { it('should attach specific icon for each filter if hasIcon is true', async(() => {
@@ -111,7 +112,7 @@ describe('TaskFiltersCloudComponent', () => {
component.showIcons = true; component.showIcons = true;
fixture.whenStable().then(() => { fixture.whenStable().then(() => {
fixture.detectChanges(); fixture.detectChanges();
const filters = fixture.debugElement.queryAll(By.css('.adf-filters__entry')); const filters = fixture.debugElement.queryAll(By.css('.adf-task-filters__entry'));
expect(component.filters.length).toBe(3); expect(component.filters.length).toBe(3);
expect(filters.length).toBe(3); expect(filters.length).toBe(3);
expect(filters[0].nativeElement.innerText).toContain('FakeInvolvedTasks'); expect(filters[0].nativeElement.innerText).toContain('FakeInvolvedTasks');
@@ -363,4 +364,19 @@ describe('TaskFiltersCloudComponent', () => {
component.selectFilter(filter); component.selectFilter(filter);
expect(component.currentFilter).toBe(fakeGlobalFilter[0]); expect(component.currentFilter).toBe(fakeGlobalFilter[0]);
}); });
it('should display filter counter if property set to true', async(() => {
spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(fakeGlobalFilterObservable);
const change = new SimpleChange(undefined, 'my-app-1', true);
component.ngOnChanges({'appName': change});
fixture.detectChanges();
component.showIcons = true;
fixture.whenStable().then(() => {
fixture.detectChanges();
const filterCounters = fixture.debugElement.queryAll(By.css('.adf-filter-action-button__counter'));
expect(component.filters.length).toBe(3);
expect(filterCounters.length).toBe(1);
expect(filterCounters[0].nativeElement.innerText).toContain('11');
});
}));
}); });

View File

@@ -40,6 +40,7 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
filters$: Observable<TaskFilterCloudModel[]>; filters$: Observable<TaskFilterCloudModel[]>;
filters: TaskFilterCloudModel[] = []; filters: TaskFilterCloudModel[] = [];
currentFilter: TaskFilterCloudModel; currentFilter: TaskFilterCloudModel;
counters = {};
constructor(private taskFilterCloudService: TaskFilterCloudService, constructor(private taskFilterCloudService: TaskFilterCloudService,
private translationService: TranslationService) { private translationService: TranslationService) {
@@ -71,6 +72,7 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
this.resetFilter(); this.resetFilter();
this.filters = Object.assign([], res); this.filters = Object.assign([], res);
this.selectFilterAndEmit(this.filterParam); this.selectFilterAndEmit(this.filterParam);
this.initFilterCounters();
this.success.emit(res); this.success.emit(res);
}, },
(err: any) => { (err: any) => {
@@ -79,6 +81,14 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
); );
} }
initFilterCounters() {
this.filters.forEach((filter) => {
if (filter.showCounter) {
this.counters$[filter.key] = this.taskFilterCloudService.getTaskFilterCounter(filter);
}
});
}
public selectFilter(paramFilter: FilterParamsModel) { public selectFilter(paramFilter: FilterParamsModel) {
if (paramFilter) { if (paramFilter) {
this.currentFilter = this.filters.find((filter, index) => this.currentFilter = this.filters.find((filter, index) =>

View File

@@ -24,7 +24,8 @@ export const fakeGlobalFilter = [
icon: 'adjust', icon: 'adjust',
id: '10', id: '10',
status: 'open', status: 'open',
assignee: 'fake-involved' assignee: 'fake-involved',
showCounter: true
}), }),
new TaskFilterCloudModel({ new TaskFilterCloudModel({
name: 'FakeMyTasks1', name: 'FakeMyTasks1',
@@ -32,7 +33,8 @@ export const fakeGlobalFilter = [
icon: 'done', icon: 'done',
id: '11', id: '11',
status: 'open', status: 'open',
assignee: 'fake-assignee' assignee: 'fake-assignee',
showCounter: false
}), }),
new TaskFilterCloudModel({ new TaskFilterCloudModel({
name: 'FakeMyTasks2', name: 'FakeMyTasks2',

View File

@@ -51,6 +51,7 @@ export class TaskFilterCloudModel {
assignmentType: AssignmentType; assignmentType: AssignmentType;
completedDate: Date; completedDate: Date;
completedBy: IdentityUserModel; completedBy: IdentityUserModel;
showCounter: boolean;
private _completedFrom: string; private _completedFrom: string;
private _completedTo: string; private _completedTo: string;
@@ -97,6 +98,7 @@ export class TaskFilterCloudModel {
this.createdFrom = obj._createdFrom || null; this.createdFrom = obj._createdFrom || null;
this.createdTo = obj._createdTo || null; this.createdTo = obj._createdTo || null;
this.candidateGroups = obj.candidateGroups || null; this.candidateGroups = obj.candidateGroups || null;
this.showCounter = obj.showCounter || false;
} }
} }

View File

@@ -15,26 +15,30 @@
* limitations under the License. * limitations under the License.
*/ */
import { IdentityUserService } from '@alfresco/adf-core'; import { AlfrescoApiService, AppConfigService, IdentityUserService } from '@alfresco/adf-core';
import { Injectable, Inject } from '@angular/core'; import { Injectable, Inject } from '@angular/core';
import { Observable, of, BehaviorSubject } from 'rxjs'; import { Observable, of, BehaviorSubject, throwError } from 'rxjs';
import { TaskFilterCloudModel } from '../models/filter-cloud.model'; import { TaskFilterCloudModel } from '../models/filter-cloud.model';
import { switchMap, map } from 'rxjs/operators'; import { switchMap, map } from 'rxjs/operators';
import { BaseCloudService } from '../../../services/base-cloud.service';
import { PreferenceCloudServiceInterface } from '../../../services/preference-cloud.interface'; import { PreferenceCloudServiceInterface } from '../../../services/preference-cloud.interface';
import { TASK_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.service'; import { TASK_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.service';
import { TaskCloudNodePaging } from '../../task-list/models/task-cloud.model';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class TaskFilterCloudService { export class TaskFilterCloudService extends BaseCloudService {
private filtersSubject: BehaviorSubject<TaskFilterCloudModel[]>; private filtersSubject: BehaviorSubject<TaskFilterCloudModel[]>;
filters$: Observable<TaskFilterCloudModel[]>; filters$: Observable<TaskFilterCloudModel[]>;
constructor( constructor(
private identityUserService: IdentityUserService, private identityUserService: IdentityUserService,
@Inject(TASK_FILTERS_SERVICE_TOKEN) @Inject(TASK_FILTERS_SERVICE_TOKEN)
public preferenceService: PreferenceCloudServiceInterface public preferenceService: PreferenceCloudServiceInterface,
) { apiService: AlfrescoApiService,
appConfigService: AppConfigService) {
super(apiService, appConfigService);
this.filtersSubject = new BehaviorSubject([]); this.filtersSubject = new BehaviorSubject([]);
this.filters$ = this.filtersSubject.asObservable(); this.filters$ = this.filtersSubject.asObservable();
} }
@@ -219,6 +223,27 @@ export class TaskFilterCloudService {
return defaultFilters.findIndex((filter) => filterName === filter.name) !== -1; return defaultFilters.findIndex((filter) => filterName === filter.name) !== -1;
} }
/**
* Finds a task using an object with optional query properties.
* @param requestNode Query object
* @returns Task information
*/
getTaskFilterCounter(taskFilter: TaskFilterCloudModel): Observable<any> {
if (taskFilter.appName || taskFilter.appName === '') {
const queryUrl = `${this.getBasePath(taskFilter.appName)}/query/v1/tasks`;
const queryParams = {
assignee: taskFilter.assignee,
status: taskFilter.status,
appName: taskFilter.appName
};
return this.get<TaskCloudNodePaging>(queryUrl, queryParams).pipe(
map((tasks) => tasks.list.pagination.totalItems)
);
} else {
return throwError('Appname not configured');
}
}
/** /**
* Calls update preference api to update task filter * Calls update preference api to update task filter
* @param appName Name of the target app * @param appName Name of the target app
@@ -264,7 +289,8 @@ export class TaskFilterCloudService {
status: 'ASSIGNED', status: 'ASSIGNED',
assignee: this.identityUserService.getCurrentUserInfo().username, assignee: this.identityUserService.getCurrentUserInfo().username,
sort: 'createdDate', sort: 'createdDate',
order: 'DESC' order: 'DESC',
showCounter: true
}), }),
new TaskFilterCloudModel({ new TaskFilterCloudModel({
name: 'ADF_CLOUD_TASK_FILTERS.QUEUED_TASKS', name: 'ADF_CLOUD_TASK_FILTERS.QUEUED_TASKS',
@@ -274,7 +300,8 @@ export class TaskFilterCloudService {
status: 'CREATED', status: 'CREATED',
assignee: '', assignee: '',
sort: 'createdDate', sort: 'createdDate',
order: 'DESC' order: 'DESC',
showCounter: true
}), }),
new TaskFilterCloudModel({ new TaskFilterCloudModel({
name: 'ADF_CLOUD_TASK_FILTERS.COMPLETED_TASKS', name: 'ADF_CLOUD_TASK_FILTERS.COMPLETED_TASKS',