AAE-42804 Cleaning up tasks and process filter components (#11693)

* AAE-42804 Cleaning up tasks and process filter components

* Added loading aria label and code improvements

* AAE-42804 Fixing query param
This commit is contained in:
Ehsan Rezaei
2026-02-26 16:15:22 +00:00
committed by GitHub
parent 3b14351389
commit dd66ecc72a
9 changed files with 198 additions and 342 deletions
@@ -50,7 +50,8 @@
"MAXIMUM_LENGTH": "Length exceeded, {{characters}} characters max.", "MAXIMUM_LENGTH": "Length exceeded, {{characters}} characters max.",
"SPACE_VALIDATOR": "Cannot begin or end with a space." "SPACE_VALIDATOR": "Cannot begin or end with a space."
} }
} },
"LOADING": "Loading"
}, },
"ADF_CLOUD_TASK_LIST": { "ADF_CLOUD_TASK_LIST": {
"START_TASK": { "START_TASK": {
@@ -97,7 +98,8 @@
"SUBTITLE": "Create a new task that you want to easily find later", "SUBTITLE": "Create a new task that you want to easily find later",
"NONE": "No task lists found" "NONE": "No task lists found"
} }
} },
"LOADING": "Loading"
}, },
"ADF_CLOUD_SERVICE_TASK_LIST": { "ADF_CLOUD_SERVICE_TASK_LIST": {
"PROPERTIES": { "PROPERTIES": {
@@ -1,37 +1,40 @@
<mat-action-list class="adf-process-filters" *ngIf="filters$ | async as filterList; else loading"> @if (filters$ | async; as filterList) {
<button <mat-nav-list>
*ngFor="let filter of filterList" @for (filter of filterList; track filter.id) {
mat-list-item <a mat-list-item
(click)="onFilterClick(filter)" [activated]="filter.id === currentRouteFilterId()"
[attr.aria-label]="filter.name | translate" [routerLink]="[PROCESSES_ROUTE]"
[id]="filter.id" [queryParams]="{ filterId: filter.id }"
[attr.data-automation-id]="filter.key + '_filter'" [attr.data-automation-id]="filter.key + '_filter'"
[class.adf-active]="currentFilter === filter" class="adf-process-filters__entry"
> [id]="filter.id"
<div class="adf-process-filters__entry"> [title]="filter.name ?? '' | translate"
<div> [attr.aria-label]="filter.name ?? '' | translate"
<mat-icon data-automation-id="adf-filter-icon" *ngIf="showIcons" [adf-icon]="filter.icon" aria-hidden="true" /> (click)="onFilterClick(filter)">
<span <span matListItemTitle>
data-automation-id="adf-filter-label" {{ filter.name ?? '' | translate }}
class="adf-filter-action-button__label">
{{ filter.name | translate }}
</span> </span>
</div> @if (showIcons) {
<span <mat-icon matListItemIcon
*ngIf="counters[filter.key]" data-automation-id="adf-filter-icon"
[attr.data-automation-id]="filter.key + '_filter-counter'" [adf-icon]="filter.icon ?? ''"
class="adf-process-filters__entry-counter" aria-hidden="true" />
[class.adf-active]="isFilterUpdated(filter.key)" }
> @if (counters[filter.key ?? '']) {
{{ counters[filter.key] }} <span matListItemMeta
</span> [attr.data-automation-id]="filter.key + '_filter-counter'"
</div> class="adf-process-filters__entry-counter"
</button> [class.adf-active]="isFilterUpdated(filter.key ?? '')"
</mat-action-list> >
<ng-template #loading> {{ counters[filter.key ?? ''] }}
<ng-container> </span>
<div class="adf-app-list-spinner"> }
<mat-spinner /> </a>
</div> }
</ng-container> </mat-nav-list>
</ng-template> } @else {
<div class="adf-app-list-spinner">
<mat-spinner [attr.aria-label]="'ADF_CLOUD_PROCESS_LIST.LOADING' | translate" aria-live="polite" />
</div>
}
@@ -1,35 +1,18 @@
@use '@angular/material' as mat;
.adf-process-filters { .adf-process-filters {
margin-right: calc(-1 * var(--adf-theme-spacing)); &__entry-counter {
padding: 0 5px;
border-radius: var(--mat-sys-corner-large, 15px);
&__entry { &.adf-active {
font-size: var(--theme-body-1-font-size); background-color: var(--mat-sys-secondary, var(--theme-accent-color));
color: var(--adf-theme-foreground-text-color-054);
display: flex;
align-items: center;
justify-content: space-between;
flex: 1;
height: 100%;
gap: var(--adf-theme-spacing);
&:hover { @include mat.list-overrides(
color: var(--theme-primary-color); (
} list-item-trailing-supporting-text-color: var(--mat-sys-on-secondary, var(--theme-accent-color-default-contrast))
)
&-counter { );
padding: 0 5px;
border-radius: 15px;
&.adf-active {
background-color: var(--theme-accent-color);
color: var(--theme-accent-color-default-contrast);
font-size: smaller;
}
}
}
.adf-active {
.adf-process-filters__entry {
color: var(--theme-primary-color);
} }
} }
} }
@@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { SimpleChange } from '@angular/core'; import { Component, SimpleChange } from '@angular/core';
import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing'; import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing';
import { first, of, throwError } from 'rxjs'; import { first, of, throwError } from 'rxjs';
import { ProcessFilterCloudService } from '../../services/process-filter-cloud.service'; import { ProcessFilterCloudService } from '../../services/process-filter-cloud.service';
@@ -30,6 +30,11 @@ import { ApolloTestingModule } from 'apollo-angular/testing';
import { HarnessLoader } from '@angular/cdk/testing'; import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatIconHarness } from '@angular/material/icon/testing'; import { MatIconHarness } from '@angular/material/icon/testing';
import { ActivatedRoute, provideRouter, Router } from '@angular/router';
import { RouterTestingHarness } from '@angular/router/testing';
@Component({ selector: 'adf-cloud-dummy', template: '' })
class DummyComponent {}
const ProcessFilterCloudServiceMock = { const ProcessFilterCloudServiceMock = {
getProcessFilters: () => of(mockProcessFilters), getProcessFilters: () => of(mockProcessFilters),
@@ -44,8 +49,9 @@ describe('ProcessFiltersCloudComponent', () => {
let getProcessFiltersSpy: jasmine.Spy; let getProcessFiltersSpy: jasmine.Spy;
let getProcessNotificationSubscriptionSpy: jasmine.Spy; let getProcessNotificationSubscriptionSpy: jasmine.Spy;
let loader: HarnessLoader; let loader: HarnessLoader;
let router: Router;
const configureTestingModule = (searchApiMethod: 'GET' | 'POST') => { const configureTestingModule = async (searchApiMethod: 'GET' | 'POST') => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ProcessFiltersCloudComponent, ApolloTestingModule], imports: [ProcessFiltersCloudComponent, ApolloTestingModule],
providers: [ providers: [
@@ -58,7 +64,21 @@ describe('ProcessFiltersCloudComponent', () => {
getProcessListCount: () => of(10) getProcessListCount: () => of(10)
} }
}, },
{ provide: ProcessFilterCloudService, useValue: ProcessFilterCloudServiceMock } { provide: ProcessFilterCloudService, useValue: ProcessFilterCloudServiceMock },
provideRouter([{ path: 'process-list-cloud', component: DummyComponent }]),
{
provide: ActivatedRoute,
useValue: {
queryParamMap: of({
get: (param: string) => {
if (param === 'filterId') {
return 'fake-process-filter-id';
}
return null;
}
})
}
}
] ]
}); });
fixture = TestBed.createComponent(ProcessFiltersCloudComponent); fixture = TestBed.createComponent(ProcessFiltersCloudComponent);
@@ -67,6 +87,9 @@ describe('ProcessFiltersCloudComponent', () => {
component.searchApiMethod = searchApiMethod; component.searchApiMethod = searchApiMethod;
processFilterService = TestBed.inject(ProcessFilterCloudService); processFilterService = TestBed.inject(ProcessFilterCloudService);
TestBed.inject(ActivatedRoute);
router = TestBed.inject(Router);
await RouterTestingHarness.create();
getProcessFiltersSpy = spyOn(processFilterService, 'getProcessFilters').and.returnValue(of(mockProcessFilters)); getProcessFiltersSpy = spyOn(processFilterService, 'getProcessFilters').and.returnValue(of(mockProcessFilters));
getProcessNotificationSubscriptionSpy = spyOn(processFilterService, 'getProcessNotificationSubscription').and.returnValue(of([])); getProcessNotificationSubscriptionSpy = spyOn(processFilterService, 'getProcessNotificationSubscription').and.returnValue(of([]));
}; };
@@ -76,8 +99,8 @@ describe('ProcessFiltersCloudComponent', () => {
}); });
describe('searchApiMethod set to GET', () => { describe('searchApiMethod set to GET', () => {
beforeEach(() => { beforeEach(async () => {
configureTestingModule('GET'); await configureTestingModule('GET');
}); });
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 () => {
@@ -182,20 +205,6 @@ describe('ProcessFiltersCloudComponent', () => {
describe('Highlight Selected Filter', () => { describe('Highlight Selected Filter', () => {
const allProcessesFilterKey = mockProcessFilters[0].key; const allProcessesFilterKey = mockProcessFilters[0].key;
const runningProcessesFilterKey = mockProcessFilters[1].key;
const completedProcessesFilterKey = mockProcessFilters[2].key;
const getActiveFilterElement = (filterKey: string): Element => {
const activeFilter = fixture.debugElement.query(By.css(`.adf-active`));
return activeFilter.nativeElement.querySelector(`[data-automation-id="${filterKey}_filter"]`);
};
const clickOnFilter = async (filterKey: string) => {
const button = fixture.debugElement.nativeElement.querySelector(`[data-automation-id="${filterKey}_filter"]`);
button.click();
fixture.detectChanges();
await fixture.whenStable();
};
it('should apply active CSS class on filter click', async () => { it('should apply active CSS class on filter click', async () => {
component.enableNotifications = true; component.enableNotifications = true;
@@ -205,36 +214,20 @@ describe('ProcessFiltersCloudComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
await clickOnFilter(allProcessesFilterKey); const link = fixture.debugElement.query(By.css(`[data-automation-id="${allProcessesFilterKey}_filter"]`)).nativeElement;
expect(link.getAttribute('href')).toBe('/process-list-cloud?filterId=10');
link.click();
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
expect(router.url).toBe('/process-list-cloud?filterId=10');
expect(getActiveFilterElement(allProcessesFilterKey)).toBeDefined();
expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull();
expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull();
await clickOnFilter(runningProcessesFilterKey);
fixture.detectChanges();
await fixture.whenStable();
expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull();
expect(getActiveFilterElement(runningProcessesFilterKey)).toBeDefined();
expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull();
await clickOnFilter(completedProcessesFilterKey);
fixture.detectChanges();
await fixture.whenStable();
expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull();
expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull();
expect(getActiveFilterElement(completedProcessesFilterKey)).toBeDefined();
}); });
}); });
}); });
describe('searchApiMethod set to POST', () => { describe('searchApiMethod set to POST', () => {
beforeEach(() => { beforeEach(async () => {
configureTestingModule('POST'); await configureTestingModule('POST');
}); });
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 () => {
@@ -336,62 +329,11 @@ describe('ProcessFiltersCloudComponent', () => {
expect(component.currentFilter).toEqual(mockProcessFilters[0]); expect(component.currentFilter).toEqual(mockProcessFilters[0]);
expect(filterClickedSpy).toHaveBeenCalledWith(mockProcessFilters[0]); expect(filterClickedSpy).toHaveBeenCalledWith(mockProcessFilters[0]);
}); });
describe('Highlight Selected Filter', () => {
const allProcessesFilterKey = mockProcessFilters[0].key;
const runningProcessesFilterKey = mockProcessFilters[1].key;
const completedProcessesFilterKey = mockProcessFilters[2].key;
const getActiveFilterElement = (filterKey: string): Element => {
const activeFilter = fixture.debugElement.query(By.css(`.adf-active`));
return activeFilter.nativeElement.querySelector(`[data-automation-id="${filterKey}_filter"]`);
};
const clickOnFilter = async (filterKey: string) => {
const button = fixture.debugElement.nativeElement.querySelector(`[data-automation-id="${filterKey}_filter"]`);
button.click();
fixture.detectChanges();
await fixture.whenStable();
};
it('should apply active CSS class on filter click', async () => {
component.enableNotifications = true;
component.appName = 'mock-app-name';
const appNameChange = new SimpleChange(null, 'mock-app-name', true);
component.ngOnChanges({ appName: appNameChange });
fixture.detectChanges();
await fixture.whenStable();
await clickOnFilter(allProcessesFilterKey);
fixture.detectChanges();
await fixture.whenStable();
expect(getActiveFilterElement(allProcessesFilterKey)).toBeDefined();
expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull();
expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull();
await clickOnFilter(runningProcessesFilterKey);
fixture.detectChanges();
await fixture.whenStable();
expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull();
expect(getActiveFilterElement(runningProcessesFilterKey)).toBeDefined();
expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull();
await clickOnFilter(completedProcessesFilterKey);
fixture.detectChanges();
await fixture.whenStable();
expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull();
expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull();
expect(getActiveFilterElement(completedProcessesFilterKey)).toBeDefined();
});
});
}); });
describe('API agnostic', () => { describe('API agnostic', () => {
beforeEach(() => { beforeEach(async () => {
configureTestingModule('GET'); await configureTestingModule('GET');
}); });
it('should emit an error with a bad response', () => { it('should emit an error with a bad response', () => {
@@ -549,43 +491,7 @@ describe('ProcessFiltersCloudComponent', () => {
}); });
describe('Highlight Selected Filter', () => { describe('Highlight Selected Filter', () => {
const allProcessesFilterKey = mockProcessFilters[0].key; it('should make subscription', () => {
const runningProcessesFilterKey = mockProcessFilters[1].key;
const completedProcessesFilterKey = mockProcessFilters[2].key;
const getActiveFilterElement = (filterKey: string): Element => {
const activeFilter = fixture.debugElement.query(By.css(`.adf-active`));
return activeFilter.nativeElement.querySelector(`[data-automation-id="${filterKey}_filter"]`);
};
it('Should apply active CSS class when filterParam input changed', async () => {
fixture.detectChanges();
component.ngOnChanges({ filterParam: new SimpleChange(null, { key: allProcessesFilterKey }, true) });
fixture.detectChanges();
await fixture.whenStable();
expect(getActiveFilterElement(allProcessesFilterKey)).toBeDefined();
expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull();
expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull();
component.ngOnChanges({ filterParam: new SimpleChange(null, { key: runningProcessesFilterKey }, true) });
fixture.detectChanges();
await fixture.whenStable();
expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull();
expect(getActiveFilterElement(runningProcessesFilterKey)).toBeDefined();
expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull();
component.ngOnChanges({ filterParam: new SimpleChange(null, { key: completedProcessesFilterKey }, true) });
fixture.detectChanges();
await fixture.whenStable();
expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull();
expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull();
expect(getActiveFilterElement(completedProcessesFilterKey)).toBeDefined();
});
it('should made sbscription', () => {
component.enableNotifications = true; component.enableNotifications = true;
component.appName = 'mock-app-name'; component.appName = 'mock-app-name';
const appNameChange = new SimpleChange(null, 'mock-app-name', true); const appNameChange = new SimpleChange(null, 'mock-app-name', true);
@@ -15,29 +15,30 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component, DestroyRef, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges, ViewEncapsulation } from '@angular/core'; import { Component, DestroyRef, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ProcessFilterCloudService } from '../../services/process-filter-cloud.service'; import { ProcessFilterCloudService } from '../../services/process-filter-cloud.service';
import { ProcessFilterCloudModel } from '../../models/process-filter-cloud.model'; import { ProcessFilterCloudModel } from '../../models/process-filter-cloud.model';
import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core'; import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core';
import { FilterParamsModel } from '../../../../task/task-filters/models/filter-cloud.model'; import { FilterParamsModel } from '../../../../task/task-filters/models/filter-cloud.model';
import { debounceTime, tap } from 'rxjs/operators'; import { debounceTime, map, tap } from 'rxjs/operators';
import { ProcessListCloudService } from '../../../process-list/services/process-list-cloud.service'; import { ProcessListCloudService } from '../../../process-list/services/process-list-cloud.service';
import { ProcessFilterCloudAdapter } from '../../../process-list/models/process-cloud-query-request.model'; import { ProcessFilterCloudAdapter } from '../../../process-list/models/process-cloud-query-request.model';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { TranslatePipe } from '@ngx-translate/core'; import { TranslatePipe } from '@ngx-translate/core';
import { AsyncPipe, NgForOf, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatListModule } from '@angular/material/list'; import { MatListModule } from '@angular/material/list';
import { ActivatedRoute, RouterLink } from '@angular/router';
@Component({ @Component({
selector: 'adf-cloud-process-filters', selector: 'adf-cloud-process-filters',
imports: [TranslatePipe, IconModule, NgIf, MatProgressSpinnerModule, NgForOf, MatListModule, AsyncPipe], imports: [TranslatePipe, IconModule, MatProgressSpinnerModule, MatListModule, AsyncPipe, RouterLink],
templateUrl: './process-filters-cloud.component.html', templateUrl: './process-filters-cloud.component.html',
styleUrls: ['./process-filters-cloud.component.scss'], styleUrls: ['./process-filters-cloud.component.scss']
encapsulation: ViewEncapsulation.None
}) })
export class ProcessFiltersCloudComponent implements OnInit, OnChanges { export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
protected readonly PROCESSES_ROUTE = '/process-list-cloud';
/** (required) The application name */ /** (required) The application name */
@Input() @Input()
appName: string = ''; appName: string = '';
@@ -87,6 +88,8 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
private readonly translationService = inject(TranslationService); private readonly translationService = inject(TranslationService);
private readonly appConfigService = inject(AppConfigService); private readonly appConfigService = inject(AppConfigService);
private readonly processListCloudService = inject(ProcessListCloudService); private readonly processListCloudService = inject(ProcessListCloudService);
private readonly activatedRoute = inject(ActivatedRoute);
protected readonly currentRouteFilterId = toSignal(this.activatedRoute.queryParamMap.pipe(map((params) => params.get('filterId'))));
ngOnInit() { ngOnInit() {
this.enableNotifications = this.appConfigService.get('notifications', true); this.enableNotifications = this.appConfigService.get('notifications', true);
@@ -1,35 +1,39 @@
<mat-action-list class="adf-task-filters" *ngIf="filters$ | async as filterList; else loading"> @if (filters$ | async; as filterList) {
<button <mat-nav-list>
*ngFor="let filter of filterList" @for (filter of filterList; track filter.id) {
mat-list-item <a mat-list-item
(click)="onFilterClick(filter)" [activated]="filter.id === currentRouteFilterId()"
[attr.aria-label]="filter.name | translate" [routerLink]="[TASKS_ROUTE]"
[id]="filter.id" [queryParams]="{ filter: filter.id }"
[attr.data-automation-id]="filter.key + '_filter'" [attr.data-automation-id]="filter.key + '_filter'"
[class.adf-active]="currentFilter === filter" class="adf-task-filters__entry"
> [id]="filter.id"
<div class="adf-task-filters__entry"> [title]="filter.name | translate"
<div class="adf-task-filters__entry-label"> [attr.aria-label]="filter.name | translate"
<mat-icon data-automation-id="adf-filter-icon" *ngIf="showIcons" [adf-icon]="filter.icon" aria-hidden="true" /> (click)="onFilterClick(filter)">
<span data-automation-id="adf-filter-label"> <span matListItemTitle>
{{ filter.name | translate }} {{ filter.name | translate }}
</span>
</div>
<span
*ngIf="counters[filter.key]"
[attr.data-automation-id]="filter.key + '_filter-counter'"
class="adf-task-filters__entry-counter"
[class.adf-active]="wasFilterUpdated(filter.key)"
>
{{ counters[filter.key] }}
</span> </span>
</div> @if (showIcons) {
</button> <mat-icon matListItemIcon
</mat-action-list> data-automation-id="adf-filter-icon"
<ng-template #loading> [adf-icon]="filter.icon"
<ng-container> aria-hidden="true" />
<div class="adf-app-list-spinner"> }
<mat-spinner /> @if (counters[filter.key]) {
</div> <span matListItemMeta
</ng-container> [attr.data-automation-id]="filter.key + '_filter-counter'"
</ng-template> class="adf-task-filters__entry-counter"
[class.adf-active]="wasFilterUpdated(filter.key)"
>
{{ counters[filter.key] }}
</span>
}
</a>
}
</mat-nav-list>
} @else {
<div class="adf-app-list-spinner">
<mat-spinner [attr.aria-label]="'ADF_CLOUD_TASK_LIST.LOADING' | translate" aria-live="polite" />
</div>
}
@@ -1,39 +1,18 @@
@use '@angular/material' as mat;
.adf-task-filters { .adf-task-filters {
margin-right: calc(-1 * var(--adf-theme-spacing));
&__entry {
font-size: var(--theme-body-1-font-size);
color: var(--adf-theme-foreground-text-color-054);
display: flex;
justify-content: space-between;
align-items: center;
flex: 1;
height: 100%;
&:hover {
color: var(--theme-primary-color);
}
}
&__entry-label {
display: flex;
flex: 1;
align-items: center;
gap: var(--adf-theme-spacing);
}
&__entry-counter { &__entry-counter {
padding: 0 5px; padding: 0 5px;
border-radius: 15px; border-radius: var(--mat-sys-corner-large, 15px);
&.adf-active { &.adf-active {
background-color: var(--theme-accent-color); background-color: var(--mat-sys-secondary, var(--theme-accent-color));
color: var(--theme-accent-color-default-contrast);
font-size: smaller; @include mat.list-overrides(
(
list-item-trailing-supporting-text-color: var(--mat-sys-on-secondary, var(--theme-accent-color-default-contrast))
)
);
} }
} }
.adf-active .adf-task-filters__entry-label {
color: var(--theme-primary-color);
}
} }
@@ -16,7 +16,7 @@
*/ */
import { AppConfigService, NoopAuthModule } from '@alfresco/adf-core'; import { AppConfigService, NoopAuthModule } from '@alfresco/adf-core';
import { SimpleChange } from '@angular/core'; import { Component, SimpleChange } from '@angular/core';
import { ComponentFixture, TestBed, fakeAsync, flush } from '@angular/core/testing'; import { ComponentFixture, TestBed, fakeAsync, flush } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { first, of, throwError } from 'rxjs'; import { first, of, throwError } from 'rxjs';
@@ -27,12 +27,17 @@ import { TaskFilterCloudService } from '../../services/task-filter-cloud.service
import { TaskFiltersCloudComponent } from './task-filters-cloud.component'; import { TaskFiltersCloudComponent } from './task-filters-cloud.component';
import { TaskListCloudService } from '../../../task-list/services/task-list-cloud.service'; import { TaskListCloudService } from '../../../task-list/services/task-list-cloud.service';
import { HarnessLoader } from '@angular/cdk/testing'; import { HarnessLoader } from '@angular/cdk/testing';
import { MatActionListItemHarness } from '@angular/material/list/testing'; import { MatNavListItemHarness } from '@angular/material/list/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { TaskFilterCloudAdapter } from '../../../../models/filter-cloud-model'; import { TaskFilterCloudAdapter } from '../../../../models/filter-cloud-model';
import { ApolloTestingModule } from 'apollo-angular/testing'; import { ApolloTestingModule } from 'apollo-angular/testing';
import { TaskFilterCloudModel } from '../../models/filter-cloud.model'; import { TaskFilterCloudModel } from '../../models/filter-cloud.model';
import { MatIconHarness } from '@angular/material/icon/testing'; import { MatIconHarness } from '@angular/material/icon/testing';
import { ActivatedRoute, provideRouter, Router } from '@angular/router';
import { RouterTestingHarness } from '@angular/router/testing';
@Component({ selector: 'adf-cloud-dummy', template: '' })
class DummyComponent {}
describe('TaskFiltersCloudComponent', () => { describe('TaskFiltersCloudComponent', () => {
let loader: HarnessLoader; let loader: HarnessLoader;
@@ -45,11 +50,28 @@ describe('TaskFiltersCloudComponent', () => {
let getTaskFilterCounterSpy: jasmine.Spy; let getTaskFilterCounterSpy: jasmine.Spy;
let getTaskListFiltersSpy: jasmine.Spy; let getTaskListFiltersSpy: jasmine.Spy;
let getTaskListCountSpy: jasmine.Spy; let getTaskListCountSpy: jasmine.Spy;
let router: Router;
const configureTestingModule = (searchApiMethod: 'GET' | 'POST') => { const configureTestingModule = async (searchApiMethod: 'GET' | 'POST') => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [NoopAuthModule, TaskFiltersCloudComponent, ApolloTestingModule], imports: [NoopAuthModule, TaskFiltersCloudComponent, ApolloTestingModule],
providers: [{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }] providers: [
{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService },
provideRouter([{ path: 'task-list-cloud', component: DummyComponent }]),
{
provide: ActivatedRoute,
useValue: {
queryParamMap: of({
get: (param: string) => {
if (param === 'filter') {
return 'fake-task-filter-id';
}
return null;
}
})
}
}
]
}); });
taskFilterService = TestBed.inject(TaskFilterCloudService); taskFilterService = TestBed.inject(TaskFilterCloudService);
taskListService = TestBed.inject(TaskListCloudService); taskListService = TestBed.inject(TaskListCloudService);
@@ -65,6 +87,9 @@ describe('TaskFiltersCloudComponent', () => {
loader = TestbedHarnessEnvironment.loader(fixture); loader = TestbedHarnessEnvironment.loader(fixture);
component.searchApiMethod = searchApiMethod; component.searchApiMethod = searchApiMethod;
TestBed.inject(ActivatedRoute);
router = TestBed.inject(Router);
await RouterTestingHarness.create();
}; };
afterEach(() => { afterEach(() => {
@@ -72,8 +97,8 @@ describe('TaskFiltersCloudComponent', () => {
}); });
describe('searchApiMethod set to GET', () => { describe('searchApiMethod set to GET', () => {
beforeEach(() => { beforeEach(async () => {
configureTestingModule('GET'); await configureTestingModule('GET');
}); });
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 () => {
@@ -295,7 +320,7 @@ describe('TaskFiltersCloudComponent', () => {
const spy = spyOn(component.filterClicked, 'emit'); const spy = spyOn(component.filterClicked, 'emit');
const filterButton = await loader.getHarness( const filterButton = await loader.getHarness(
MatActionListItemHarness.with({ selector: `[data-automation-id="${fakeGlobalFilter[0].key}_filter"]` }) MatNavListItemHarness.with({ selector: `[data-automation-id="${fakeGlobalFilter[0].key}_filter"]` })
); );
await filterButton.click(); await filterButton.click();
@@ -353,7 +378,7 @@ describe('TaskFiltersCloudComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
const filterButton = await loader.getHarness( const filterButton = await loader.getHarness(
MatActionListItemHarness.with({ selector: `[data-automation-id="${fakeGlobalFilter[0].key}_filter"]` }) MatNavListItemHarness.with({ selector: `[data-automation-id="${fakeGlobalFilter[0].key}_filter"]` })
); );
await filterButton.click(); await filterButton.click();
@@ -577,19 +602,6 @@ describe('TaskFiltersCloudComponent', () => {
describe('Highlight Selected Filter', () => { describe('Highlight Selected Filter', () => {
const assignedTasksFilterKey = defaultTaskFiltersMock[1].key; const assignedTasksFilterKey = defaultTaskFiltersMock[1].key;
const queuedTasksFilterKey = defaultTaskFiltersMock[0].key;
const completedTasksFilterKey = defaultTaskFiltersMock[2].key;
const getActiveFilterElement = (filterKey: string): Element => {
const activeFilter = fixture.debugElement.query(By.css(`.adf-active`));
return activeFilter.nativeElement.querySelector(`[data-automation-id="${filterKey}_filter"]`);
};
const clickOnFilter = async (filterKey: string) => {
fixture.debugElement.nativeElement.querySelector(`[data-automation-id="${filterKey}_filter"]`).click();
fixture.detectChanges();
await fixture.whenStable();
};
it('Should highlight task filter on filter click', async () => { it('Should highlight task filter on filter click', async () => {
getTaskListFiltersSpy.and.returnValue(of(defaultTaskFiltersMock)); getTaskListFiltersSpy.and.returnValue(of(defaultTaskFiltersMock));
@@ -599,52 +611,12 @@ describe('TaskFiltersCloudComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
await clickOnFilter(assignedTasksFilterKey); const filterLink = fixture.debugElement.query(By.css(`[data-automation-id="${assignedTasksFilterKey}_filter"]`));
filterLink.nativeElement.click();
expect(getActiveFilterElement(assignedTasksFilterKey)).toBeDefined();
expect(getActiveFilterElement(queuedTasksFilterKey)).toBeNull();
expect(getActiveFilterElement(completedTasksFilterKey)).toBeNull();
await clickOnFilter(queuedTasksFilterKey);
expect(getActiveFilterElement(assignedTasksFilterKey)).toBeNull();
expect(getActiveFilterElement(queuedTasksFilterKey)).toBeDefined();
expect(getActiveFilterElement(completedTasksFilterKey)).toBeNull();
await clickOnFilter(completedTasksFilterKey);
expect(getActiveFilterElement(assignedTasksFilterKey)).toBeNull();
expect(getActiveFilterElement(queuedTasksFilterKey)).toBeNull();
expect(getActiveFilterElement(completedTasksFilterKey)).toBeDefined();
});
it('Should highlight task filter when filterParam input changed', async () => {
getTaskListFiltersSpy.and.returnValue(of(defaultTaskFiltersMock));
fixture.detectChanges();
component.ngOnChanges({ filterParam: new SimpleChange(null, { key: assignedTasksFilterKey }, true) });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
expect(getActiveFilterElement(assignedTasksFilterKey)).toBeDefined(); expect(router.url).toBe('/task-list-cloud?filter=2');
expect(getActiveFilterElement(queuedTasksFilterKey)).toBeNull();
expect(getActiveFilterElement(completedTasksFilterKey)).toBeNull();
component.ngOnChanges({ filterParam: new SimpleChange(null, { key: queuedTasksFilterKey }, true) });
fixture.detectChanges();
await fixture.whenStable();
expect(getActiveFilterElement(assignedTasksFilterKey)).toBeNull();
expect(getActiveFilterElement(queuedTasksFilterKey)).toBeDefined();
expect(getActiveFilterElement(completedTasksFilterKey)).toBeNull();
component.ngOnChanges({ filterParam: new SimpleChange(null, { key: completedTasksFilterKey }, true) });
fixture.detectChanges();
await fixture.whenStable();
expect(getActiveFilterElement(assignedTasksFilterKey)).toBeNull();
expect(getActiveFilterElement(queuedTasksFilterKey)).toBeNull();
expect(getActiveFilterElement(completedTasksFilterKey)).toBeDefined();
}); });
}); });
}); });
@@ -15,31 +15,33 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges, ViewEncapsulation } from '@angular/core'; import { Component, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { TaskFilterCloudService } from '../../services/task-filter-cloud.service'; import { TaskFilterCloudService } from '../../services/task-filter-cloud.service';
import { FilterParamsModel, TaskFilterCloudModel } from '../../models/filter-cloud.model'; import { FilterParamsModel, TaskFilterCloudModel } from '../../models/filter-cloud.model';
import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core'; import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core';
import { debounceTime, tap } from 'rxjs/operators'; import { debounceTime, map, tap } from 'rxjs/operators';
import { BaseTaskFiltersCloudComponent } from '../base-task-filters-cloud.component'; import { BaseTaskFiltersCloudComponent } from '../base-task-filters-cloud.component';
import { TaskDetailsCloudModel } from '../../../models/task-details-cloud.model'; import { TaskDetailsCloudModel } from '../../../models/task-details-cloud.model';
import { TaskCloudEngineEvent } from '../../../../models/engine-event-cloud.model'; import { TaskCloudEngineEvent } from '../../../../models/engine-event-cloud.model';
import { TaskListCloudService } from '../../../task-list/services/task-list-cloud.service'; import { TaskListCloudService } from '../../../task-list/services/task-list-cloud.service';
import { TaskFilterCloudAdapter } from '../../../../models/filter-cloud-model'; import { TaskFilterCloudAdapter } from '../../../../models/filter-cloud-model';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { TranslatePipe } from '@ngx-translate/core'; import { TranslatePipe } from '@ngx-translate/core';
import { CommonModule } from '@angular/common';
import { MatListModule } from '@angular/material/list'; import { MatListModule } from '@angular/material/list';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { AsyncPipe } from '@angular/common';
@Component({ @Component({
selector: 'adf-cloud-task-filters', selector: 'adf-cloud-task-filters',
imports: [CommonModule, MatProgressSpinnerModule, TranslatePipe, IconModule, MatListModule], imports: [AsyncPipe, MatProgressSpinnerModule, TranslatePipe, MatListModule, RouterLink, IconModule],
templateUrl: './task-filters-cloud.component.html', templateUrl: './task-filters-cloud.component.html',
styleUrls: ['./task-filters-cloud.component.scss'], styleUrls: ['./task-filters-cloud.component.scss']
encapsulation: ViewEncapsulation.None
}) })
export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent implements OnInit, OnChanges { export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent implements OnInit, OnChanges {
protected readonly TASKS_ROUTE = '/task-list-cloud';
/** (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the task count. */ /** (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the task count. */
@Input() @Input()
searchApiMethod: 'GET' | 'POST' = 'GET'; searchApiMethod: 'GET' | 'POST' = 'GET';
@@ -70,6 +72,8 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
private readonly taskListCloudService = inject(TaskListCloudService); private readonly taskListCloudService = inject(TaskListCloudService);
private readonly translationService = inject(TranslationService); private readonly translationService = inject(TranslationService);
private readonly appConfigService = inject(AppConfigService); private readonly appConfigService = inject(AppConfigService);
private readonly activatedRoute = inject(ActivatedRoute);
readonly currentRouteFilterId = toSignal(this.activatedRoute.queryParamMap.pipe(map((params) => params.get('filter'))));
ngOnInit() { ngOnInit() {
this.enableNotifications = this.appConfigService.get('notifications', true); this.enableNotifications = this.appConfigService.get('notifications', true);