diff --git a/docs/process-services-cloud/components/people-cloud.component.md b/docs/process-services-cloud/components/people-cloud.component.md index 82b94b0e10..b4e63b4339 100644 --- a/docs/process-services-cloud/components/people-cloud.component.md +++ b/docs/process-services-cloud/components/people-cloud.component.md @@ -34,7 +34,8 @@ Allows one or more users to be selected (with auto-suggestion) based on the inpu | required | `boolean` | false | Mark this field as required | | roles | `string[]` | | Role names of the users to be listed. | | searchUserCtrl | `FormControl` | | FormControl to search the user | -| title | `string` | | Placeholder translation key | +| title | `string` | | Label translation key | +| placeholder | `string` | | Placeholder for the input field | | hideInputOnSingleSelection | `boolean` | false | Hide the input field when a user is selected in single selection mode. The input will be shown again when the user is removed using the icon on the chip. | | formFieldAppearance | [`MatFormFieldAppearance`](https://material.angular.io/components/form-field/api#MatFormFieldAppearance) | "fill" | Material form field appearance (fill / outline). | | formFieldSubscriptSizing | [`SubscriptSizing`](https://material.angular.io/components/form-field/api#SubscriptSizing) | "fixed" | Material form field subscript sizing (fixed / dynamic). | diff --git a/docs/process-services-cloud/components/task-list-cloud.component.md b/docs/process-services-cloud/components/task-list-cloud.component.md index 20a937af84..99a11c7350 100644 --- a/docs/process-services-cloud/components/task-list-cloud.component.md +++ b/docs/process-services-cloud/components/task-list-cloud.component.md @@ -87,6 +87,12 @@ when the task list is empty: | standalone | `boolean` | false | Filter the tasks. Display only the tasks that belong to a process in case is false or tasks that doesn't belong to a process in case of true. | | status | `string` | "" | Filter the tasks. Display only tasks with status equal to the supplied value. | | stickyHeader | `boolean` | false | Toggles the sticky header mode. | +| names | `string[]` | [] | Filter the tasks. Display only tasks with names matching any of the supplied strings. This input will be used only if `TASK_SEARCH_API_METHOD_TOKEN` is provided with `POST` value. | +| processDefinitionNames | `string[]` | [] | Filter the tasks. Display only tasks under provided processes. This input will be used only if `TASK_SEARCH_API_METHOD_TOKEN` is provided with `POST` value. | +| statuses | `string[]` | [] | Filter the tasks. Display only tasks with provided statuses. This input will be used only if `TASK_SEARCH_API_METHOD_TOKEN` is provided with `POST` value. | +| assignees | `string[]` | [] | Filter the tasks. Display only tasks with assignees whose usernames are present in the array. This input will be used only if `TASK_SEARCH_API_METHOD_TOKEN` is provided with `POST` value. | +| priorities | `string[]` | [] | Filter the tasks. Display only tasks with provided priorities. This input will be used only if `TASK_SEARCH_API_METHOD_TOKEN` is provided with `POST` value. | +| completedByUsers | `string[]` | [] | Filter the tasks. Display only tasks completed by users whose usernames are present in the array. This input will be used only if `TASK_SEARCH_API_METHOD_TOKEN` is provided with `POST` value. | ### Events diff --git a/lib/process-services-cloud/src/lib/models/filter-cloud-model.ts b/lib/process-services-cloud/src/lib/models/filter-cloud-model.ts index 258aa0a525..7d78f685f4 100644 --- a/lib/process-services-cloud/src/lib/models/filter-cloud-model.ts +++ b/lib/process-services-cloud/src/lib/models/filter-cloud-model.ts @@ -15,7 +15,9 @@ * limitations under the License. */ +import { Pagination } from '@alfresco/js-api'; import { TaskListCloudSortingModel } from './task-list-sorting.model'; +import { TaskFilterCloudModel } from '../task/task-filters/models/filter-cloud.model'; export class TaskQueryCloudRequestModel { appName: string; @@ -90,3 +92,102 @@ export class TaskQueryCloudRequestModel { } } } + +export interface TaskListRequestTaskVariableFilter { + name?: string; + type?: string; + value?: string; + operator?: string; +} + +export class TaskListRequestModel { + appName: string; + pagination?: Pagination; + sorting?: TaskListCloudSortingModel[]; + + onlyStandalone?: boolean; + onlyRoot?: boolean; + name?: string[]; + description?: string[]; + processDefinitionName?: string[]; + priority?: string[]; + status?: string[]; + completedBy?: string[]; + assignee?: string[]; + createdFrom?: string; + createdTo?: string; + lastModifiedFrom?: string; + lastModifiedTo?: string; + lastClaimedFrom?: string; + lastClaimedTo?: string; + dueDateFrom?: string; + dueDateTo?: string; + completedFrom?: string; + completedTo?: string; + candidateUserId?: string[]; + candidateGroupId?: string[]; + + taskVariableFilters?: TaskListRequestTaskVariableFilter[]; + variableKeys?: string[]; + + constructor(obj: Partial) { + if (!obj.appName) { + throw new Error('appName not configured'); + } + + this.appName = obj.appName; + this.pagination = obj.pagination; + this.sorting = obj.sorting; + + this.onlyStandalone = obj.onlyStandalone; + this.onlyRoot = obj.onlyRoot; + this.name = obj.name; + this.description = obj.description; + this.processDefinitionName = obj.processDefinitionName; + this.priority = obj.priority; + this.status = obj.status; + this.completedBy = obj.completedBy; + this.assignee = obj.assignee; + this.createdFrom = obj.createdFrom; + this.createdTo = obj.createdTo; + this.lastModifiedFrom = obj.lastModifiedFrom; + this.lastModifiedTo = obj.lastModifiedTo; + this.lastClaimedFrom = obj.lastClaimedFrom; + this.lastClaimedTo = obj.lastClaimedTo; + this.dueDateFrom = obj.dueDateFrom; + this.dueDateTo = obj.dueDateTo; + this.completedFrom = obj.completedFrom; + this.completedTo = obj.completedTo; + this.candidateUserId = obj.candidateUserId; + this.candidateGroupId = obj.candidateGroupId; + this.taskVariableFilters = obj.taskVariableFilters; + this.variableKeys = obj.variableKeys; + } +} + +export class TaskFilterCloudAdapter extends TaskListRequestModel { + constructor(filter: TaskFilterCloudModel) { + super({ + appName: filter.appName, + pagination: { maxItems: 25, skipCount: 0 }, + sorting: [{ orderBy: filter.sort, direction: filter.order }], + + onlyStandalone: filter.standalone, + name: filter.taskNames, + processDefinitionName: filter.processDefinitionNames, + priority: filter.priorities?.map((priority) => priority.toString()), + status: filter.statuses, + completedBy: filter.completedByUsers, + assignee: filter.assignees, + createdFrom: filter.createdFrom, + createdTo: filter.createdTo, + lastModifiedFrom: filter.lastModifiedFrom, + lastModifiedTo: filter.lastModifiedTo, + dueDateFrom: filter.dueDateFrom, + dueDateTo: filter.dueDateTo, + completedFrom: filter.completedFrom, + completedTo: filter.completedTo, + candidateGroupId: filter.candidateGroups?.map((group) => group.id) + }); + } +} diff --git a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.html b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.html index fed2536b68..bc1b56bc44 100644 --- a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.html +++ b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.html @@ -29,10 +29,13 @@ [matAutocomplete]="auto" [matChipInputFor]="userMultipleChipList" [required]="required" + [placeholder]="placeholder" (focus)="setFocus(true)" (blur)="setFocus(false); markAsTouched()" class="adf-cloud-input" - data-automation-id="adf-people-cloud-search-input" #userInput> + data-automation-id="adf-people-cloud-search-input" + #userInput + > diff --git a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.ts b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.ts index 674e25b3fd..c02496be0d 100644 --- a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.ts @@ -119,11 +119,17 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy, After searchUserCtrl = new UntypedFormControl({ value: '', disabled: false }); /** - * Placeholder translation key + * Label translation key */ @Input() title: string; + /** + * Placeholder for the input field + */ + @Input() + placeholder: string; + /** * Hide the matInput associated with the chip grid when a single user is selected in single selection mode. * The input will be shown again when the user is removed using the icon on the chip. diff --git a/lib/process-services-cloud/src/lib/process/process-list/services/process-task-list-cloud.service.ts b/lib/process-services-cloud/src/lib/process/process-list/services/process-task-list-cloud.service.ts index d4078cfc80..9ee8554473 100644 --- a/lib/process-services-cloud/src/lib/process/process-list/services/process-task-list-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/process/process-list/services/process-task-list-cloud.service.ts @@ -19,13 +19,12 @@ import { Injectable } from '@angular/core'; import { Observable, throwError } from 'rxjs'; import { BaseCloudService } from '../../../services/base-cloud.service'; import { map } from 'rxjs/operators'; -import { TaskListCloudServiceInterface } from '../../../services/task-list-cloud.service.interface'; import { TaskQueryCloudRequestModel } from '../../../models/filter-cloud-model'; import { TaskCloudNodePaging } from '../../../models/task-cloud.model'; import { TaskListCloudSortingModel } from '../../../models/task-list-sorting.model'; @Injectable({ providedIn: 'root' }) -export class ProcessTaskListCloudService extends BaseCloudService implements TaskListCloudServiceInterface { +export class ProcessTaskListCloudService extends BaseCloudService { /** * Finds a task using an object with optional query properties. * diff --git a/lib/process-services-cloud/src/lib/services/cloud-token.service.ts b/lib/process-services-cloud/src/lib/services/cloud-token.service.ts index d9ca89cd0c..d3ad1f7999 100644 --- a/lib/process-services-cloud/src/lib/services/cloud-token.service.ts +++ b/lib/process-services-cloud/src/lib/services/cloud-token.service.ts @@ -28,3 +28,9 @@ export const PROCESS_FILTERS_SERVICE_TOKEN = new InjectionToken('task-filters-cloud'); export const TASK_LIST_CLOUD_TOKEN = new InjectionToken('task-list-cloud'); + +/** + * Token used to indicate the API used to search for tasks. + * 'POST' value should be provided only if the used Activiti version is 8.7.0 or higher. + */ +export const TASK_SEARCH_API_METHOD_TOKEN = new InjectionToken<'GET' | 'POST'>('task-search-method'); diff --git a/lib/process-services-cloud/src/lib/services/task-list-cloud.service.interface.ts b/lib/process-services-cloud/src/lib/services/task-list-cloud.service.interface.ts index 5015a8a341..5d6312447b 100644 --- a/lib/process-services-cloud/src/lib/services/task-list-cloud.service.interface.ts +++ b/lib/process-services-cloud/src/lib/services/task-list-cloud.service.interface.ts @@ -16,8 +16,26 @@ */ import { Observable } from 'rxjs'; -import { TaskQueryCloudRequestModel } from '../models/filter-cloud-model'; +import { TaskListRequestModel, TaskQueryCloudRequestModel } from '../models/filter-cloud-model'; export interface TaskListCloudServiceInterface { + /** + * Finds a task using an object with optional query properties. + * + * @deprecated From Activiti 8.7.0 forward, use TaskListCloudService.fetchTaskList instead. + * @param requestNode Query object + * @param queryUrl Query url + * @returns Task information + */ getTaskByRequest(requestNode: TaskQueryCloudRequestModel, queryUrl?: string): Observable; + + /** + * Available from Activiti version 8.7.0 onwards. + * Retrieves a list of tasks using an object with optional query properties. + * + * @param requestNode Query object + * @param queryUrl Query url + * @returns List of tasks + */ + fetchTaskList(requestNode: TaskListRequestModel, queryUrl?: string): Observable; } diff --git a/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters-cloud.component.spec.ts index 322f67eb1a..9a158551f7 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters-cloud.component.spec.ts @@ -20,30 +20,40 @@ import { SimpleChange } from '@angular/core'; import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { first, of, throwError } from 'rxjs'; -import { TASK_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.service'; +import { TASK_FILTERS_SERVICE_TOKEN, TASK_SEARCH_API_METHOD_TOKEN } from '../../../services/cloud-token.service'; import { LocalPreferenceCloudService } from '../../../services/local-preference-cloud.service'; import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module'; import { defaultTaskFiltersMock, fakeGlobalFilter, taskNotifications } from '../mock/task-filters-cloud.mock'; import { TaskFilterCloudService } from '../services/task-filter-cloud.service'; import { TaskFiltersCloudModule } from '../task-filters-cloud.module'; import { TaskFiltersCloudComponent } from './task-filters-cloud.component'; +import { TaskListCloudService } from '../../task-list/services/task-list-cloud.service'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { MatActionListItemHarness } from '@angular/material/list/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { TaskFilterCloudAdapter } from '../../../models/filter-cloud-model'; describe('TaskFiltersCloudComponent', () => { + let loader: HarnessLoader; let taskFilterService: TaskFilterCloudService; + let taskListService: TaskListCloudService; let appConfigService: AppConfigService; let component: TaskFiltersCloudComponent; let fixture: ComponentFixture; let getTaskFilterCounterSpy: jasmine.Spy; let getTaskListFiltersSpy: jasmine.Spy; + let getTaskListCounterSpy: jasmine.Spy; - beforeEach(() => { + const configureTestingModule = (providers: any[]) => { TestBed.configureTestingModule({ imports: [ProcessServiceCloudTestingModule, TaskFiltersCloudModule], - providers: [{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }] + providers: [{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, ...providers] }); taskFilterService = TestBed.inject(TaskFilterCloudService); + taskListService = TestBed.inject(TaskListCloudService); getTaskFilterCounterSpy = spyOn(taskFilterService, 'getTaskFilterCounter').and.returnValue(of(11)); + getTaskListCounterSpy = spyOn(taskListService, 'getTaskListCounter').and.returnValue(of(11)); spyOn(taskFilterService, 'getTaskNotificationSubscription').and.returnValue(of(taskNotifications)); getTaskListFiltersSpy = spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(of(fakeGlobalFilter)); @@ -51,447 +61,576 @@ describe('TaskFiltersCloudComponent', () => { fixture = TestBed.createComponent(TaskFiltersCloudComponent); component = fixture.componentInstance; - }); + loader = TestbedHarnessEnvironment.loader(fixture); + }; afterEach(() => { fixture.destroy(); }); - it('should attach specific icon for each filter if hasIcon is true', async () => { - const change = new SimpleChange(undefined, 'my-app-1', true); - component.ngOnChanges({ appName: change }); - - fixture.detectChanges(); - await fixture.whenStable(); - - component.showIcons = true; - - fixture.detectChanges(); - await fixture.whenStable(); - - expect(component.filters.length).toBe(3); - - const filters = fixture.nativeElement.querySelectorAll('.adf-icon'); - expect(filters.length).toBe(3); - expect(filters[0].innerText).toContain('adjust'); - expect(filters[1].innerText).toContain('done'); - expect(filters[2].innerText).toContain('inbox'); - }); - - it('should not attach icons for each filter if hasIcon is false', async () => { - component.showIcons = false; - const change = new SimpleChange(undefined, 'my-app-1', true); - component.ngOnChanges({ appName: change }); - - fixture.detectChanges(); - await fixture.whenStable(); - - const filters: any = fixture.debugElement.queryAll(By.css('.adf-icon')); - expect(filters.length).toBe(0); - }); - - it('should display the filters', async () => { - const change = new SimpleChange(undefined, 'my-app-1', true); - component.ngOnChanges({ appName: change }); - - fixture.detectChanges(); - await fixture.whenStable(); - - component.showIcons = true; - - fixture.detectChanges(); - await fixture.whenStable(); - - const filters = fixture.debugElement.queryAll(By.css('.adf-task-filters__entry')); - - expect(component.filters.length).toBe(3); - expect(filters.length).toBe(3); - expect(filters[0].nativeElement.innerText).toContain('FakeInvolvedTasks'); - expect(filters[1].nativeElement.innerText).toContain('FakeMyTasks1'); - expect(filters[2].nativeElement.innerText).toContain('FakeMyTasks2'); - }); - - it('should emit an error with a bad response', (done) => { - const mockErrorFilterList = { - error: 'wrong request' - }; - getTaskListFiltersSpy.and.returnValue(throwError(mockErrorFilterList)); - - const appName = 'my-app-1'; - const change = new SimpleChange(null, appName, true); - - component.error.subscribe((err) => { - expect(err).toBeDefined(); - done(); + describe('TASK_SEARCH_API_METHOD_TOKEN injected with GET value', () => { + beforeEach(() => { + configureTestingModule([{ provide: TASK_SEARCH_API_METHOD_TOKEN, useValue: 'GET' }]); }); - component.ngOnChanges({ appName: change }); - }); + it('should attach specific icon for each filter if hasIcon is true', async () => { + const change = new SimpleChange(undefined, 'my-app-1', true); + component.ngOnChanges({ appName: change }); - it('should display the task filters', async () => { - const appName = 'my-app-1'; - const change = new SimpleChange(null, appName, true); + fixture.detectChanges(); + await fixture.whenStable(); - component.ngOnChanges({ appName: change }); + component.showIcons = true; - fixture.detectChanges(); - await fixture.whenStable(); + fixture.detectChanges(); + await fixture.whenStable(); - const filters = fixture.debugElement.queryAll(By.css('.adf-task-filters__entry')); - expect(component.filters).toEqual(fakeGlobalFilter); - expect(filters.length).toBe(3); - expect(filters[0].nativeElement.innerText).toContain('FakeInvolvedTasks'); - expect(filters[1].nativeElement.innerText).toContain('FakeMyTasks1'); - expect(filters[2].nativeElement.innerText).toContain('FakeMyTasks2'); - }); + expect(component.filters.length).toBe(3); - it('should not select any filter as default', async () => { - const appName = 'my-app-1'; - const change = new SimpleChange(null, appName, true); - - component.ngOnChanges({ appName: change }); - fixture.detectChanges(); - await fixture.whenStable(); - - expect(component.currentFilter).toBeUndefined(); - }); - - it('should select the task filter based on the input by name param', async () => { - const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); - const change = new SimpleChange(null, { name: 'FakeMyTasks2' }, true); - - fixture.detectChanges(); - await fixture.whenStable(); - component.ngOnChanges({ filterParam: change }); - - expect(component.currentFilter).toEqual(fakeGlobalFilter[2]); - expect(filterSelectedSpy).toHaveBeenCalledWith(fakeGlobalFilter[2]); - }); - - it('should not select any task filter if filter input does not exist', async () => { - const change = new SimpleChange(null, { name: 'nonexistentFilter' }, true); - fixture.detectChanges(); - await fixture.whenStable(); - component.ngOnChanges({ filterParam: change }); - - expect(component.currentFilter).toBeUndefined(); - }); - - it('should select the task filter based on the input by index param', async () => { - const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); - const change = new SimpleChange(null, { index: 2 }, true); - - fixture.detectChanges(); - await fixture.whenStable(); - component.ngOnChanges({ filterParam: change }); - - expect(component.currentFilter).toEqual(fakeGlobalFilter[2]); - expect(filterSelectedSpy).toHaveBeenCalledWith(fakeGlobalFilter[2]); - }); - - it('should select the task filter based on the input by id param', async () => { - const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); - const change = new SimpleChange(null, { id: '12' }, true); - - fixture.detectChanges(); - await fixture.whenStable(); - component.ngOnChanges({ filterParam: change }); - - expect(component.currentFilter).toEqual(fakeGlobalFilter[2]); - expect(filterSelectedSpy).toHaveBeenCalledWith(fakeGlobalFilter[2]); - }); - - it('should select the task filter based on the input by key param', async () => { - const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); - const change = new SimpleChange(null, { key: 'fake-my-task2' }, true); - - fixture.detectChanges(); - await fixture.whenStable(); - component.ngOnChanges({ filterParam: change }); - - expect(component.currentFilter).toEqual(fakeGlobalFilter[2]); - expect(filterSelectedSpy).toHaveBeenCalledWith(fakeGlobalFilter[2]); - }); - - it('should filterClicked emit when a filter is clicked from the UI', async () => { - spyOn(component.filterClicked, 'emit'); - - fixture.detectChanges(); - await fixture.whenStable(); - - const filterButton = fixture.debugElement.nativeElement.querySelector(`[data-automation-id="${fakeGlobalFilter[0].key}_filter"]`); - filterButton.click(); - - fixture.detectChanges(); - await fixture.whenStable(); - - expect(component.filterClicked.emit).toHaveBeenCalledWith(fakeGlobalFilter[0]); - }); - - it('should not emit a filter clicked event when a filter is selected through the filterParam input (filterClicked emits only through a UI click action)', async () => { - const filterClickedSpy = spyOn(component.filterClicked, 'emit'); - const change = new SimpleChange(null, { id: '10' }, true); - - fixture.detectChanges(); - await fixture.whenStable(); - component.ngOnChanges({ filterParam: change }); - - expect(component.currentFilter).toBe(fakeGlobalFilter[0]); - expect(filterClickedSpy).not.toHaveBeenCalled(); - }); - - it('should reset the filter when the param is undefined', () => { - const change = new SimpleChange(fakeGlobalFilter[0], undefined, false); - component.currentFilter = fakeGlobalFilter[0]; - component.ngOnChanges({ filterParam: change }); - - expect(component.currentFilter).toEqual(undefined); - }); - - it('should reload filters by appName on binding changes', () => { - spyOn(component, 'getFilters').and.stub(); - const appName = 'my-app-1'; - - const change = new SimpleChange(null, appName, true); - component.ngOnChanges({ appName: change }); - - expect(component.getFilters).toHaveBeenCalledWith(appName); - }); - - it('should display filter counter if property set to true', async () => { - const change = new SimpleChange(undefined, 'my-app-1', true); - component.ngOnChanges({ appName: change }); - - fixture.detectChanges(); - await fixture.whenStable(); - - component.showIcons = true; - - fixture.detectChanges(); - await fixture.whenStable(); - - const filterCounters = fixture.debugElement.queryAll(By.css('.adf-task-filters__entry-counter')); - expect(component.filters.length).toBe(3); - expect(filterCounters.length).toBe(1); - expect(filterCounters[0].nativeElement.innerText).toContain('11'); - }); - - it('should update filter counter when notification received', () => { - component.appName = 'my-app-1'; - component.showIcons = true; - fixture.detectChanges(); - - const updatedFilterCounters = fixture.debugElement.queryAll(By.css('span.adf-active')); - expect(updatedFilterCounters.length).toBe(1); - expect(Object.keys(component.counters).length).toBe(3); - expect(component.counters['fake-involved-tasks']).toBeDefined(); - }); - - it('should not update filter counter when notifications are disabled from app.config.json', fakeAsync(() => { - spyOn(appConfigService, 'get').and.returnValue(false); - component.appName = 'my-app-1'; - component.showIcons = true; - fixture.detectChanges(); - - const updatedFilterCounters = fixture.debugElement.queryAll(By.css('span.adf-active')); - expect(updatedFilterCounters.length).toBe(0); - })); - - it('should reset filter counter notification when filter is selected', () => { - spyOn(appConfigService, 'get').and.returnValue(true); - const change = new SimpleChange(null, { key: fakeGlobalFilter[0].key }, true); - component.appName = 'my-app-1'; - component.showIcons = true; - fixture.detectChanges(); - - let updatedFilterCounters = fixture.debugElement.queryAll(By.css('span.adf-active')); - expect(updatedFilterCounters.length).toBe(1); - - component.filters = fakeGlobalFilter; - component.currentFilter = null; - - component.ngOnChanges({ filterParam: change }); - fixture.detectChanges(); - - updatedFilterCounters = fixture.debugElement.queryAll(By.css('span.adf-active')); - expect(updatedFilterCounters.length).toBe(0); - }); - - it('should update filter counter when filter is selected', () => { - component.appName = 'my-app-1'; - component.showIcons = true; - fixture.detectChanges(); - - const filterButton = fixture.debugElement.nativeElement.querySelector(`[data-automation-id="${fakeGlobalFilter[0].key}_filter"]`); - filterButton.click(); - - fixture.detectChanges(); - expect(getTaskFilterCounterSpy).toHaveBeenCalledWith(fakeGlobalFilter[0]); - }); - - it('should not emit filter key when filter counter is set for first time', () => { - component.currentFiltersValues = {}; - const fakeFilterKey = 'testKey'; - const fakeFilterValue = 10; - const updatedFilterSpy = spyOn(component.updatedFilter, 'emit'); - component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, fakeFilterValue); - fixture.detectChanges(); - - expect(component.currentFiltersValues).not.toEqual({}); - expect(component.currentFiltersValues[fakeFilterKey]).toBe(fakeFilterValue); - expect(updatedFilterSpy).not.toHaveBeenCalled(); - }); - - it('should not emit filter key when filter counter has not changd', () => { - component.currentFiltersValues = {}; - const fakeFilterKey = 'testKey'; - const fakeFilterValue = 10; - const updatedFilterSpy = spyOn(component.updatedFilter, 'emit'); - component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, fakeFilterValue); - fixture.detectChanges(); - - expect(component.currentFiltersValues).not.toEqual({}); - expect(component.currentFiltersValues[fakeFilterKey]).toBe(fakeFilterValue); - - component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, fakeFilterValue); - expect(component.currentFiltersValues[fakeFilterKey]).toBe(fakeFilterValue); - expect(updatedFilterSpy).not.toHaveBeenCalled(); - }); - - it('should emit filter key when filter counter is increased', (done) => { - component.currentFiltersValues = {}; - const fakeFilterKey = 'testKey'; - component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 10); - - component.updatedFilter.pipe(first()).subscribe((updatedFilter: string) => { - expect(updatedFilter).toBe(fakeFilterKey); - expect(component.currentFiltersValues[fakeFilterKey]).toBe(20); - done(); - }); - component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 20); - fixture.detectChanges(); - }); - - it('should emit filter key when filter counter is decreased', (done) => { - component.currentFiltersValues = {}; - const fakeFilterKey = 'testKey'; - component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 10); - - component.updatedFilter.pipe(first()).subscribe((updatedFilter: string) => { - expect(updatedFilter).toBe(fakeFilterKey); - done(); + const filters = fixture.nativeElement.querySelectorAll('.adf-icon'); + expect(filters.length).toBe(3); + expect(filters[0].innerText).toContain('adjust'); + expect(filters[1].innerText).toContain('done'); + expect(filters[2].innerText).toContain('inbox'); }); - component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 5); - fixture.detectChanges(); - }); + it('should not attach icons for each filter if hasIcon is false', async () => { + component.showIcons = false; + const change = new SimpleChange(undefined, 'my-app-1', true); + component.ngOnChanges({ appName: change }); - it('should remove key from set of updated filters when received refreshed filter key', async () => { - const filterKeyTest = 'filter-key-test'; - component.updatedCountersSet.add(filterKeyTest); - - expect(component.updatedCountersSet.size).toBe(1); - - taskFilterService.filterKeyToBeRefreshed$ = of(filterKeyTest); - fixture.detectChanges(); - - expect(component.updatedCountersSet.size).toBe(0); - }); - - it('should remove key from set of updated filters when clicked on filter', async () => { - const filter = defaultTaskFiltersMock[1]; - component.updatedCountersSet.add(filter.key); - fixture.detectChanges(); - - expect(component.updatedCountersSet.size).toBe(1); - - component.onFilterClick(filter); - await fixture.whenStable(); - fixture.detectChanges(); - - expect(component.updatedCountersSet.size).toBe(0); - }); - - it('should add key to set of updated filters when value has changed', () => { - component.updatedCountersSet = new Set(); - const fakeFilterKey = 'testKey'; - component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 10); - component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 20); - - expect(component.updatedCountersSet.size).toBe(1); - expect(component.updatedCountersSet.has(fakeFilterKey)).toBe(true); - }); - - describe('Highlight Selected Filter', () => { - 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 () => { - getTaskListFiltersSpy.and.returnValue(of(defaultTaskFiltersMock)); - 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(assignedTasksFilterKey); - - 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(); + const filters: any = fixture.debugElement.queryAll(By.css('.adf-icon')); + expect(filters.length).toBe(0); }); - it('Should highlight task filter when filterParam input changed', async () => { - getTaskListFiltersSpy.and.returnValue(of(defaultTaskFiltersMock)); - fixture.detectChanges(); + it('should display the filters', async () => { + const change = new SimpleChange(undefined, 'my-app-1', true); + component.ngOnChanges({ appName: change }); - component.ngOnChanges({ filterParam: new SimpleChange(null, { key: assignedTasksFilterKey }, true) }); fixture.detectChanges(); await fixture.whenStable(); - expect(getActiveFilterElement(assignedTasksFilterKey)).toBeDefined(); - expect(getActiveFilterElement(queuedTasksFilterKey)).toBeNull(); - expect(getActiveFilterElement(completedTasksFilterKey)).toBeNull(); + component.showIcons = true; - 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(); + const filters = fixture.debugElement.queryAll(By.css('.adf-task-filters__entry')); + + expect(component.filters.length).toBe(3); + expect(filters.length).toBe(3); + expect(filters[0].nativeElement.innerText).toContain('FakeInvolvedTasks'); + expect(filters[1].nativeElement.innerText).toContain('FakeMyTasks1'); + expect(filters[2].nativeElement.innerText).toContain('FakeMyTasks2'); + }); + + it('should display the task filters', async () => { + const appName = 'my-app-1'; + const change = new SimpleChange(null, appName, true); + + component.ngOnChanges({ appName: change }); - 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(); + const filters = fixture.debugElement.queryAll(By.css('.adf-task-filters__entry')); + expect(component.filters).toEqual(fakeGlobalFilter); + expect(filters.length).toBe(3); + expect(filters[0].nativeElement.innerText).toContain('FakeInvolvedTasks'); + expect(filters[1].nativeElement.innerText).toContain('FakeMyTasks1'); + expect(filters[2].nativeElement.innerText).toContain('FakeMyTasks2'); + }); + + it('should not select any filter as default', async () => { + const appName = 'my-app-1'; + const change = new SimpleChange(null, appName, true); + + component.ngOnChanges({ appName: change }); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.currentFilter).toBeUndefined(); + }); + + it('should filterClicked emit when a filter is clicked from the UI', async () => { + spyOn(component.filterClicked, 'emit'); + + fixture.detectChanges(); + await fixture.whenStable(); + + const filterButton = fixture.debugElement.nativeElement.querySelector(`[data-automation-id="${fakeGlobalFilter[0].key}_filter"]`); + filterButton.click(); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.filterClicked.emit).toHaveBeenCalledWith(fakeGlobalFilter[0]); + }); + + it('should display filter counter if property set to true', async () => { + const change = new SimpleChange(undefined, 'my-app-1', true); + component.ngOnChanges({ appName: change }); + + fixture.detectChanges(); + await fixture.whenStable(); + + component.showIcons = true; + + fixture.detectChanges(); + await fixture.whenStable(); + + const filterCounters = fixture.debugElement.queryAll(By.css('.adf-task-filters__entry-counter')); + expect(component.filters.length).toBe(3); + expect(filterCounters.length).toBe(1); + expect(filterCounters[0].nativeElement.innerText).toContain('11'); + }); + + it('should update filter counter when notification received', () => { + component.appName = 'my-app-1'; + component.showIcons = true; + fixture.detectChanges(); + + const updatedFilterCounters = fixture.debugElement.queryAll(By.css('span.adf-active')); + expect(updatedFilterCounters.length).toBe(1); + expect(Object.keys(component.counters).length).toBe(3); + expect(component.counters['fake-involved-tasks']).toBeDefined(); + }); + + it('should not update filter counter when notifications are disabled from app.config.json', fakeAsync(() => { + spyOn(appConfigService, 'get').and.returnValue(false); + component.appName = 'my-app-1'; + component.showIcons = true; + fixture.detectChanges(); + + const updatedFilterCounters = fixture.debugElement.queryAll(By.css('span.adf-active')); + expect(updatedFilterCounters.length).toBe(0); + })); + + it('should reset filter counter notification when filter is selected', () => { + spyOn(appConfigService, 'get').and.returnValue(true); + const change = new SimpleChange(null, { key: fakeGlobalFilter[0].key }, true); + component.appName = 'my-app-1'; + component.showIcons = true; + fixture.detectChanges(); + + let updatedFilterCounters = fixture.debugElement.queryAll(By.css('span.adf-active')); + expect(updatedFilterCounters.length).toBe(1); + + component.filters = fakeGlobalFilter; + component.currentFilter = null; + + component.ngOnChanges({ filterParam: change }); + fixture.detectChanges(); + + updatedFilterCounters = fixture.debugElement.queryAll(By.css('span.adf-active')); + expect(updatedFilterCounters.length).toBe(0); + }); + + it('should update filter counter when filter is selected', () => { + component.appName = 'my-app-1'; + component.showIcons = true; + fixture.detectChanges(); + + const filterButton = fixture.debugElement.nativeElement.querySelector(`[data-automation-id="${fakeGlobalFilter[0].key}_filter"]`); + filterButton.click(); + + fixture.detectChanges(); + expect(getTaskFilterCounterSpy).toHaveBeenCalledWith(fakeGlobalFilter[0]); + }); + }); + + describe('TASK_SEARCH_API_METHOD_TOKEN injected with POST value', () => { + beforeEach(() => { + configureTestingModule([{ provide: TASK_SEARCH_API_METHOD_TOKEN, useValue: 'POST' }]); + component.showIcons = true; + component.appName = 'my-app-1'; + }); + + it('should attach specific icon for each filter if hasIcon is true', () => { + fixture.detectChanges(); + + const filters = fixture.nativeElement.querySelectorAll('.adf-icon'); + + expect(component.filters.length).toBe(3); + expect(filters.length).toBe(3); + expect(filters[0].innerText).toContain('adjust'); + expect(filters[1].innerText).toContain('done'); + expect(filters[2].innerText).toContain('inbox'); + }); + + it('should not attach icons for each filter if showIcons is false', () => { + component.showIcons = false; + fixture.detectChanges(); + + const filters: any = fixture.debugElement.queryAll(By.css('.adf-icon')); + + expect(filters.length).toBe(0); + }); + + it('should display the filters', () => { + fixture.detectChanges(); + + const filters = fixture.debugElement.queryAll(By.css('.adf-task-filters__entry')); + + expect(component.filters.length).toBe(3); + expect(filters.length).toBe(3); + expect(filters[0].nativeElement.innerText).toContain('FakeInvolvedTasks'); + expect(filters[1].nativeElement.innerText).toContain('FakeMyTasks1'); + expect(filters[2].nativeElement.innerText).toContain('FakeMyTasks2'); + }); + + it('should not select any filter as default', () => { + fixture.detectChanges(); + + expect(component.currentFilter).toBeUndefined(); + }); + + it('should emit filterClicked when a filter is clicked from the UI', async () => { + fixture.detectChanges(); + const spy = spyOn(component.filterClicked, 'emit'); + + const filterButton = await loader.getHarness( + MatActionListItemHarness.with({ selector: `[data-automation-id="${fakeGlobalFilter[0].key}_filter"]` }) + ); + await filterButton.click(); + + expect(spy).toHaveBeenCalledWith(fakeGlobalFilter[0]); + }); + + it('should display filter counter if property set to true', () => { + fixture.detectChanges(); + + const filterCounters = fixture.debugElement.queryAll(By.css('.adf-task-filters__entry-counter')); + + expect(component.filters.length).toBe(3); + expect(filterCounters.length).toBe(1); + expect(filterCounters[0].nativeElement.innerText).toContain('11'); + }); + + it('should update filter counter when notification received', () => { + fixture.detectChanges(); + + const updatedFilterCounters = fixture.debugElement.queryAll(By.css('span.adf-active')); + + expect(updatedFilterCounters.length).toBe(1); + expect(Object.keys(component.counters).length).toBe(3); + expect(component.counters['fake-involved-tasks']).toBeDefined(); + }); + + it('should not update filter counter when notifications are disabled from app.config.json', () => { + spyOn(appConfigService, 'get').and.returnValue(false); + fixture.detectChanges(); + + const updatedFilterCounters = fixture.debugElement.queryAll(By.css('span.adf-active')); + expect(updatedFilterCounters.length).toBe(0); + }); + + it('should reset filter counter notification when filter is selected', () => { + fixture.detectChanges(); + spyOn(appConfigService, 'get').and.returnValue(true); + const change = new SimpleChange(null, { key: fakeGlobalFilter[0].key }, true); + + let updatedFilterCounters = fixture.debugElement.queryAll(By.css('span.adf-active')); + expect(updatedFilterCounters.length).toBe(1); + + component.filters = fakeGlobalFilter; + component.currentFilter = null; + + component.ngOnChanges({ filterParam: change }); + fixture.detectChanges(); + + updatedFilterCounters = fixture.debugElement.queryAll(By.css('span.adf-active')); + expect(updatedFilterCounters.length).toBe(0); + }); + + it('should update filter counter when filter is selected', async () => { + fixture.detectChanges(); + + const filterButton = await loader.getHarness( + MatActionListItemHarness.with({ selector: `[data-automation-id="${fakeGlobalFilter[0].key}_filter"]` }) + ); + await filterButton.click(); + + expect(getTaskListCounterSpy).toHaveBeenCalledWith(new TaskFilterCloudAdapter(fakeGlobalFilter[0])); + }); + }); + + describe('API agnostic', () => { + beforeEach(() => { + configureTestingModule([]); + }); + + it('should emit an error with a bad response', (done) => { + const mockErrorFilterList = { + error: 'wrong request' + }; + getTaskListFiltersSpy.and.returnValue(throwError(mockErrorFilterList)); + + const appName = 'my-app-1'; + const change = new SimpleChange(null, appName, true); + + component.error.subscribe((err) => { + expect(err).toBeDefined(); + done(); + }); + + component.ngOnChanges({ appName: change }); + }); + + it('should select the task filter based on the input by name param', async () => { + const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); + const change = new SimpleChange(null, { name: 'FakeMyTasks2' }, true); + + fixture.detectChanges(); + await fixture.whenStable(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toEqual(fakeGlobalFilter[2]); + expect(filterSelectedSpy).toHaveBeenCalledWith(fakeGlobalFilter[2]); + }); + + it('should not select any task filter if filter input does not exist', async () => { + const change = new SimpleChange(null, { name: 'nonexistentFilter' }, true); + fixture.detectChanges(); + await fixture.whenStable(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toBeUndefined(); + }); + + it('should select the task filter based on the input by index param', async () => { + const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); + const change = new SimpleChange(null, { index: 2 }, true); + + fixture.detectChanges(); + await fixture.whenStable(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toEqual(fakeGlobalFilter[2]); + expect(filterSelectedSpy).toHaveBeenCalledWith(fakeGlobalFilter[2]); + }); + + it('should select the task filter based on the input by id param', async () => { + const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); + const change = new SimpleChange(null, { id: '12' }, true); + + fixture.detectChanges(); + await fixture.whenStable(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toEqual(fakeGlobalFilter[2]); + expect(filterSelectedSpy).toHaveBeenCalledWith(fakeGlobalFilter[2]); + }); + + it('should select the task filter based on the input by key param', async () => { + const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); + const change = new SimpleChange(null, { key: 'fake-my-task2' }, true); + + fixture.detectChanges(); + await fixture.whenStable(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toEqual(fakeGlobalFilter[2]); + expect(filterSelectedSpy).toHaveBeenCalledWith(fakeGlobalFilter[2]); + }); + + it('should not emit a filter clicked event when a filter is selected through the filterParam input (filterClicked emits only through a UI click action)', async () => { + const filterClickedSpy = spyOn(component.filterClicked, 'emit'); + const change = new SimpleChange(null, { id: '10' }, true); + + fixture.detectChanges(); + await fixture.whenStable(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toBe(fakeGlobalFilter[0]); + expect(filterClickedSpy).not.toHaveBeenCalled(); + }); + + it('should reset the filter when the param is undefined', () => { + const change = new SimpleChange(fakeGlobalFilter[0], undefined, false); + component.currentFilter = fakeGlobalFilter[0]; + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toEqual(undefined); + }); + + it('should reload filters by appName on binding changes', () => { + spyOn(component, 'getFilters').and.stub(); + const appName = 'my-app-1'; + + const change = new SimpleChange(null, appName, true); + component.ngOnChanges({ appName: change }); + + expect(component.getFilters).toHaveBeenCalledWith(appName); + }); + + it('should not emit filter key when filter counter is set for first time', () => { + component.currentFiltersValues = {}; + const fakeFilterKey = 'testKey'; + const fakeFilterValue = 10; + const updatedFilterSpy = spyOn(component.updatedFilter, 'emit'); + component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, fakeFilterValue); + fixture.detectChanges(); + + expect(component.currentFiltersValues).not.toEqual({}); + expect(component.currentFiltersValues[fakeFilterKey]).toBe(fakeFilterValue); + expect(updatedFilterSpy).not.toHaveBeenCalled(); + }); + + it('should not emit filter key when filter counter has not changd', () => { + component.currentFiltersValues = {}; + const fakeFilterKey = 'testKey'; + const fakeFilterValue = 10; + const updatedFilterSpy = spyOn(component.updatedFilter, 'emit'); + component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, fakeFilterValue); + fixture.detectChanges(); + + expect(component.currentFiltersValues).not.toEqual({}); + expect(component.currentFiltersValues[fakeFilterKey]).toBe(fakeFilterValue); + + component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, fakeFilterValue); + expect(component.currentFiltersValues[fakeFilterKey]).toBe(fakeFilterValue); + expect(updatedFilterSpy).not.toHaveBeenCalled(); + }); + + it('should emit filter key when filter counter is increased', (done) => { + component.currentFiltersValues = {}; + const fakeFilterKey = 'testKey'; + component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 10); + + component.updatedFilter.pipe(first()).subscribe((updatedFilter: string) => { + expect(updatedFilter).toBe(fakeFilterKey); + expect(component.currentFiltersValues[fakeFilterKey]).toBe(20); + done(); + }); + component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 20); + fixture.detectChanges(); + }); + + it('should emit filter key when filter counter is decreased', (done) => { + component.currentFiltersValues = {}; + const fakeFilterKey = 'testKey'; + component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 10); + + component.updatedFilter.pipe(first()).subscribe((updatedFilter: string) => { + expect(updatedFilter).toBe(fakeFilterKey); + done(); + }); + + component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 5); + fixture.detectChanges(); + }); + + it('should remove key from set of updated filters when received refreshed filter key', async () => { + const filterKeyTest = 'filter-key-test'; + component.updatedCountersSet.add(filterKeyTest); + + expect(component.updatedCountersSet.size).toBe(1); + + taskFilterService.filterKeyToBeRefreshed$ = of(filterKeyTest); + fixture.detectChanges(); + + expect(component.updatedCountersSet.size).toBe(0); + }); + + it('should remove key from set of updated filters when clicked on filter', async () => { + const filter = defaultTaskFiltersMock[1]; + component.updatedCountersSet.add(filter.key); + fixture.detectChanges(); + + expect(component.updatedCountersSet.size).toBe(1); + + component.onFilterClick(filter); + await fixture.whenStable(); + fixture.detectChanges(); + + expect(component.updatedCountersSet.size).toBe(0); + }); + + it('should add key to set of updated filters when value has changed', () => { + component.updatedCountersSet = new Set(); + const fakeFilterKey = 'testKey'; + component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 10); + component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 20); + + expect(component.updatedCountersSet.size).toBe(1); + expect(component.updatedCountersSet.has(fakeFilterKey)).toBe(true); + }); + + describe('Highlight Selected Filter', () => { + 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 () => { + getTaskListFiltersSpy.and.returnValue(of(defaultTaskFiltersMock)); + 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(assignedTasksFilterKey); + + 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(); + await fixture.whenStable(); + + expect(getActiveFilterElement(assignedTasksFilterKey)).toBeDefined(); + 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(); + }); }); }); }); diff --git a/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters-cloud.component.ts b/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters-cloud.component.ts index b8062d510f..5ef4d3c3cb 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters-cloud.component.ts @@ -24,6 +24,9 @@ import { debounceTime, takeUntil, tap } from 'rxjs/operators'; import { BaseTaskFiltersCloudComponent } from './base-task-filters-cloud.component'; import { TaskDetailsCloudModel } from '../../start-task/models/task-details-cloud.model'; import { TaskCloudEngineEvent } from '../../../models/engine-event-cloud.model'; +import { TaskListCloudService } from '../../task-list/services/task-list-cloud.service'; +import { TaskFilterCloudAdapter } from '../../../models/filter-cloud-model'; +import { TASK_SEARCH_API_METHOD_TOKEN } from '../../../services/cloud-token.service'; @Component({ selector: 'adf-cloud-task-filters', @@ -55,8 +58,10 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp currentFiltersValues: { [key: string]: number } = {}; private readonly taskFilterCloudService = inject(TaskFilterCloudService); + private readonly taskListCloudService = inject(TaskListCloudService); private readonly translationService = inject(TranslationService); private readonly appConfigService = inject(AppConfigService); + private readonly searchMethod = inject<'GET' | 'POST'>(TASK_SEARCH_API_METHOD_TOKEN, { optional: true }); ngOnInit() { this.enableNotifications = this.appConfigService.get('notifications', true); @@ -114,24 +119,31 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp /** * Get current value for filter and check if value has changed + * * @param filter filter */ updateFilterCounter(filter: TaskFilterCloudModel): void { - if (filter?.showCounter) { - this.taskFilterCloudService - .getTaskFilterCounter(filter) - .pipe( - tap((filterCounter) => { - this.checkIfFilterValuesHasBeenUpdated(filter.key, filterCounter); - }) - ) - .subscribe((data) => { - this.counters = { - ...this.counters, - [filter.key]: data - }; - }); + if (!filter?.showCounter) { + return; } + this.fetchTaskFilterCounter(filter) + .pipe( + tap((filterCounter) => { + this.checkIfFilterValuesHasBeenUpdated(filter.key, filterCounter); + }) + ) + .subscribe((data) => { + this.counters = { + ...this.counters, + [filter.key]: data + }; + }); + } + + private fetchTaskFilterCounter(filter: TaskFilterCloudModel): Observable { + return this.searchMethod === 'POST' + ? this.taskListCloudService.getTaskListCounter(new TaskFilterCloudAdapter(filter)) + : this.taskFilterCloudService.getTaskFilterCounter(filter); } initFilterCounterNotifications() { diff --git a/lib/process-services-cloud/src/lib/task/task-filters/mock/task-filters-cloud.mock.ts b/lib/process-services-cloud/src/lib/task/task-filters/mock/task-filters-cloud.mock.ts index a8f183b451..cc2d7d092e 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/mock/task-filters-cloud.mock.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/mock/task-filters-cloud.mock.ts @@ -21,6 +21,7 @@ import { TaskFilterCloudModel, ServiceTaskFilterCloudModel, AssignmentType, Task export const fakeGlobalFilter: any[] = [ { + appName: 'fake-app-name', name: 'FakeInvolvedTasks', key: 'fake-involved-tasks', icon: 'adjust', @@ -244,37 +245,37 @@ export const fakeTaskFilter = new TaskFilterCloudModel({ status: 'ALL' }); -export const fakeTaskCloudFilters = [ - { +export const fakeTaskCloudFilters: TaskFilterCloudModel[] = [ + new TaskFilterCloudModel({ name: 'FAKE_TASK_1', id: '1', - key: 'all-fake-task', + key: 'completed-fake-task', icon: 'adjust', appName: 'fakeAppName', sort: 'startDate', - status: 'ALL', + status: TaskStatusFilter.COMPLETED, order: 'DESC' - }, - { + }), + new TaskFilterCloudModel({ name: 'FAKE_TASK_2', id: '2', key: 'run-fake-task', icon: 'adjust', appName: 'fakeAppName', sort: 'startDate', - status: 'RUNNING', + status: TaskStatusFilter.ASSIGNED, order: 'DESC' - }, - { + }), + new TaskFilterCloudModel({ name: 'FAKE_TASK_3', id: '3', key: 'complete-fake-task', icon: 'adjust', appName: 'fakeAppName', sort: 'startDate', - status: 'COMPLETED', + status: TaskStatusFilter.COMPLETED, order: 'DESC' - } + }) ]; export const taskNotifications = [ @@ -290,8 +291,8 @@ export const taskCloudEngineEventsMock = { } }; -export const defaultTaskFiltersMock = [ - { +export const defaultTaskFiltersMock: TaskFilterCloudModel[] = [ + new TaskFilterCloudModel({ name: 'CREATED_TASK_FILTER', id: '1', key: 'created', @@ -300,8 +301,8 @@ export const defaultTaskFiltersMock = [ sort: 'startDate', status: TaskStatusFilter.CREATED, order: 'DESC' - }, - { + }), + new TaskFilterCloudModel({ name: 'ASSIGNED_TASK_FILTER', id: '2', key: 'assigned', @@ -310,8 +311,8 @@ export const defaultTaskFiltersMock = [ sort: 'startDate', status: TaskStatusFilter.ASSIGNED, order: 'DESC' - }, - { + }), + new TaskFilterCloudModel({ name: 'COMPLETED_TASK_FILTER', id: '3', key: 'complete-fake-task', @@ -320,7 +321,7 @@ export const defaultTaskFiltersMock = [ sort: 'startDate', status: TaskStatusFilter.COMPLETED, order: 'DESC' - } + }) ]; export const fakeFilterNotification: TaskDetailsCloudModel = { diff --git a/lib/process-services-cloud/src/lib/task/task-filters/models/filter-cloud.model.ts b/lib/process-services-cloud/src/lib/task/task-filters/models/filter-cloud.model.ts index 0a6ed24829..8ae648ab0b 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/models/filter-cloud.model.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/models/filter-cloud.model.ts @@ -25,7 +25,7 @@ import { ComponentSelectionMode } from '../../../types'; import { IdentityGroupModel } from '../../../group/models/identity-group.model'; import { IdentityUserModel } from '../../../people/models/identity-user.model'; -export class TaskFilterCloudModel { +export class TaskFilterCloudModel { id: string; name: string; key: string; @@ -60,6 +60,13 @@ export class TaskFilterCloudModel { completedBy: IdentityUserModel; showCounter: boolean; + taskNames: string[] | null; + statuses: TaskStatusFilter[] | null; + assignees: string[] | null; + processDefinitionNames: string[] | null; + priorities: string[] | null; + completedByUsers: string[] | null; + private _completedFrom: string; private _completedTo: string; private _dueDateFrom: string; @@ -108,6 +115,13 @@ export class TaskFilterCloudModel { this.createdTo = obj._createdTo || null; this.candidateGroups = obj.candidateGroups || null; this.showCounter = obj.showCounter || false; + + this.taskNames = obj.taskNames || null; + this.statuses = obj.statuses || null; + this.assignees = obj.assignees || null; + this.processDefinitionNames = obj.processDefinitionNames || null; + this.priorities = obj.priorities || null; + this.completedByUsers = obj.completedByUsers || null; } } diff --git a/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.spec.ts b/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.spec.ts index a6085a3a91..09e48f0e08 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.spec.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.spec.ts @@ -36,6 +36,7 @@ import { ProcessServiceCloudTestingModule } from '../../../testing/process-servi import { IdentityUserService } from '../../../people/services/identity-user.service'; import { ApolloModule } from 'apollo-angular'; import { StorageService } from '@alfresco/adf-core'; +import { TaskStatusFilter } from '../public-api'; describe('TaskFilterCloudService', () => { let service: TaskFilterCloudService; @@ -46,18 +47,17 @@ describe('TaskFilterCloudService', () => { let createPreferenceSpy: jasmine.Spy; let getCurrentUserInfoSpy: jasmine.Spy; - const identityUserMock = { username: 'fakeusername', firstName: 'fake-identity-first-name', lastName: 'fake-identity-last-name', email: 'fakeIdentity@email.com' }; + const identityUserMock = { + username: 'fakeusername', + firstName: 'fake-identity-first-name', + lastName: 'fake-identity-last-name', + email: 'fakeIdentity@email.com' + }; beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ProcessServiceCloudTestingModule, - ApolloModule - ], - providers: [ - { provide: TASK_FILTERS_SERVICE_TOKEN, useClass: UserPreferenceCloudService } - ] + imports: [HttpClientTestingModule, ProcessServiceCloudTestingModule, ApolloModule], + providers: [{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: UserPreferenceCloudService }] }); service = TestBed.inject(TaskFilterCloudService); notificationCloudService = TestBed.inject(NotificationCloudService); @@ -90,17 +90,20 @@ describe('TaskFilterCloudService', () => { expect(res[0].appName).toBe('fakeAppName'); expect(res[0].id).toBe('1'); expect(res[0].name).toBe('FAKE_TASK_1'); - expect(res[0].status).toBe('ALL'); + expect(res[0].status).toBe(TaskStatusFilter.COMPLETED); + expect(res[0].statuses).toContain(TaskStatusFilter.COMPLETED); expect(res[1].appName).toBe('fakeAppName'); expect(res[1].id).toBe('2'); expect(res[1].name).toBe('FAKE_TASK_2'); - expect(res[1].status).toBe('RUNNING'); + expect(res[1].status).toBe(TaskStatusFilter.ASSIGNED); + expect(res[1].statuses).toContain(TaskStatusFilter.ASSIGNED); expect(res[2].appName).toBe('fakeAppName'); expect(res[2].id).toBe('3'); expect(res[2].name).toBe('FAKE_TASK_3'); - expect(res[2].status).toBe('COMPLETED'); + expect(res[2].status).toBe(TaskStatusFilter.COMPLETED); + expect(res[2].statuses).toContain(TaskStatusFilter.COMPLETED); expect(createPreferenceSpy).toHaveBeenCalled(); done(); @@ -116,17 +119,20 @@ describe('TaskFilterCloudService', () => { expect(res[0].appName).toBe('fakeAppName'); expect(res[0].id).toBe('1'); expect(res[0].name).toBe('FAKE_TASK_1'); - expect(res[0].status).toBe('ALL'); + expect(res[0].status).toBe(TaskStatusFilter.COMPLETED); + expect(res[0].statuses).toContain(TaskStatusFilter.COMPLETED); expect(res[1].appName).toBe('fakeAppName'); expect(res[1].id).toBe('2'); expect(res[1].name).toBe('FAKE_TASK_2'); - expect(res[1].status).toBe('RUNNING'); + expect(res[1].status).toBe(TaskStatusFilter.ASSIGNED); + expect(res[1].statuses).toContain(TaskStatusFilter.ASSIGNED); expect(res[2].appName).toBe('fakeAppName'); expect(res[2].id).toBe('3'); expect(res[2].name).toBe('FAKE_TASK_3'); - expect(res[2].status).toBe('COMPLETED'); + expect(res[2].status).toBe(TaskStatusFilter.COMPLETED); + expect(res[2].statuses).toContain(TaskStatusFilter.COMPLETED); expect(getPreferencesSpy).toHaveBeenCalled(); done(); @@ -144,17 +150,20 @@ describe('TaskFilterCloudService', () => { expect(res[0].appName).toBe('fakeAppName'); expect(res[0].id).toBe('1'); expect(res[0].name).toBe('FAKE_TASK_1'); - expect(res[0].status).toBe('ALL'); + expect(res[0].status).toBe(TaskStatusFilter.COMPLETED); + expect(res[0].statuses).toContain(TaskStatusFilter.COMPLETED); expect(res[1].appName).toBe('fakeAppName'); expect(res[1].id).toBe('2'); expect(res[1].name).toBe('FAKE_TASK_2'); - expect(res[1].status).toBe('RUNNING'); + expect(res[1].status).toBe(TaskStatusFilter.ASSIGNED); + expect(res[1].statuses).toContain(TaskStatusFilter.ASSIGNED); expect(res[2].appName).toBe('fakeAppName'); expect(res[2].id).toBe('3'); expect(res[2].name).toBe('FAKE_TASK_3'); - expect(res[2].status).toBe('COMPLETED'); + expect(res[2].status).toBe(TaskStatusFilter.COMPLETED); + expect(res[2].statuses).toContain(TaskStatusFilter.COMPLETED); done(); }); @@ -167,7 +176,8 @@ describe('TaskFilterCloudService', () => { expect(res.appName).toBe('fakeAppName'); expect(res.id).toBe('2'); expect(res.name).toBe('FAKE_TASK_2'); - expect(res.status).toBe('RUNNING'); + expect(res.status).toBe(TaskStatusFilter.ASSIGNED); + expect(res.statuses).toContain(TaskStatusFilter.ASSIGNED); expect(getPreferenceByKeySpy).toHaveBeenCalled(); done(); @@ -183,7 +193,8 @@ describe('TaskFilterCloudService', () => { expect(res.appName).toBe('fakeAppName'); expect(res.id).toBe('2'); expect(res.name).toBe('FAKE_TASK_2'); - expect(res.status).toBe('RUNNING'); + expect(res.status).toBe(TaskStatusFilter.ASSIGNED); + expect(res.statuses).toContain(TaskStatusFilter.ASSIGNED); done(); }); }); @@ -245,14 +256,17 @@ describe('Inject [LocalPreferenceCloudService] into the TaskFilterCloudService', let getPreferencesSpy: jasmine.Spy; let storageService: StorageService; - const identityUserMock = { username: 'fakeusername', firstName: 'fake-identity-first-name', lastName: 'fake-identity-last-name', email: 'fakeIdentity@email.com' }; + const identityUserMock = { + username: 'fakeusername', + firstName: 'fake-identity-first-name', + lastName: 'fake-identity-last-name', + email: 'fakeIdentity@email.com' + }; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule, ProcessServiceCloudTestingModule, ApolloModule], - providers: [ - { provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService } - ] + providers: [{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }] }); service = TestBed.inject(TaskFilterCloudService); preferenceCloudService = service.preferenceService; @@ -272,20 +286,24 @@ describe('Inject [LocalPreferenceCloudService] into the TaskFilterCloudService', expect(res[0].key).toEqual('my-tasks'); expect(res[0].appName).toEqual(appName); expect(res[0].icon).toEqual('inbox'); - expect(res[0].status).toEqual('ASSIGNED'); + expect(res[0].status).toEqual(TaskStatusFilter.ASSIGNED); + expect(res[0].statuses).toContain(TaskStatusFilter.ASSIGNED); expect(res[0].assignee).toEqual(identityUserMock.username); + expect(res[0].assignees).toContain(identityUserMock.username); expect(res[1].name).toEqual('ADF_CLOUD_TASK_FILTERS.QUEUED_TASKS'); expect(res[1].key).toEqual('queued-tasks'); expect(res[1].appName).toEqual(appName); expect(res[1].icon).toEqual('queue'); - expect(res[1].status).toEqual('CREATED'); + expect(res[1].status).toEqual(TaskStatusFilter.CREATED); + expect(res[1].statuses).toContain(TaskStatusFilter.CREATED); expect(res[2].name).toEqual('ADF_CLOUD_TASK_FILTERS.COMPLETED_TASKS'); expect(res[2].key).toEqual('completed-tasks'); expect(res[2].appName).toEqual(appName); expect(res[2].icon).toEqual('done'); - expect(res[2].status).toEqual('COMPLETED'); + expect(res[2].status).toEqual(TaskStatusFilter.COMPLETED); + expect(res[2].statuses).toContain(TaskStatusFilter.COMPLETED); expect(getPreferencesSpy).toHaveBeenCalled(); const localData = JSON.parse(storageService.getItem(`task-filters-${appName}-${identityUserMock.username}`)); @@ -295,20 +313,24 @@ describe('Inject [LocalPreferenceCloudService] into the TaskFilterCloudService', expect(localData[0].key).toEqual('my-tasks'); expect(localData[0].appName).toEqual(appName); expect(localData[0].icon).toEqual('inbox'); - expect(localData[0].status).toEqual('ASSIGNED'); + expect(localData[0].status).toEqual(TaskStatusFilter.ASSIGNED); + expect(localData[0].statuses).toContain(TaskStatusFilter.ASSIGNED); expect(localData[0].assignee).toEqual(identityUserMock.username); + expect(localData[0].assignees).toContain(identityUserMock.username); expect(localData[1].name).toEqual('ADF_CLOUD_TASK_FILTERS.QUEUED_TASKS'); expect(localData[1].key).toEqual('queued-tasks'); expect(localData[1].appName).toEqual(appName); expect(localData[1].icon).toEqual('queue'); - expect(localData[1].status).toEqual('CREATED'); + expect(localData[1].status).toEqual(TaskStatusFilter.CREATED); + expect(localData[1].statuses).toContain(TaskStatusFilter.CREATED); expect(localData[2].name).toEqual('ADF_CLOUD_TASK_FILTERS.COMPLETED_TASKS'); expect(localData[2].key).toEqual('completed-tasks'); expect(localData[2].appName).toEqual(appName); expect(localData[2].icon).toEqual('done'); - expect(localData[2].status).toEqual('COMPLETED'); + expect(localData[2].status).toEqual(TaskStatusFilter.COMPLETED); + expect(localData[2].statuses).toContain(TaskStatusFilter.COMPLETED); done(); }); diff --git a/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.ts b/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.ts index 65a3fb113a..05796cefd3 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.ts @@ -80,7 +80,8 @@ export class TaskFilterCloudService extends BaseCloudService { } else { return of(this.findFiltersByKeyInPreferences(preferences, key)); } - }) + }), + switchMap((filters) => this.handleCreateFilterBackwardsCompatibility(appName, key, filters)) ) .subscribe((filters) => { this.addFiltersToStream(filters); @@ -379,4 +380,45 @@ export class TaskFilterCloudService extends BaseCloudService { refreshFilter(filterKey: string): void { this.filterKeyToBeRefreshedSource.next(filterKey); } + + /** + * This method is run after retrieving the filter array from preferences. + * It handles the backwards compatibility with the new API by looking for the new properties and their counterparts in each passed filter. + * If the new property is not found, it is created and assigned the value constructed from the old property. + * The filters are then updated in the preferences and returned. + * Old properties are left untouched for purposes like feature toggling. + * + * @param appName Name of the target app. + * @param key Key of the task filters. + * @param filters Array of task filters to be checked for backward compatibility. + * @returns Observable of task filters with updated properties. + */ + private handleCreateFilterBackwardsCompatibility( + appName: string, + key: string, + filters: TaskFilterCloudModel[] + ): Observable { + filters.forEach((filter) => { + if (filter.taskName && !filter.taskNames) { + filter.taskNames = [filter.taskName]; + } + if (filter.status && !filter.statuses) { + filter.statuses = [filter.status]; + } + if (filter.assignee && !filter.assignees) { + filter.assignees = [filter.assignee]; + } + if (filter.processDefinitionName && !filter.processDefinitionNames) { + filter.processDefinitionNames = [filter.processDefinitionName]; + } + if (filter.completedBy?.username && !filter.completedByUsers) { + filter.completedByUsers = [filter.completedBy.username]; + } + if (filter.priority && !filter.priorities) { + filter.priorities = [`${filter.priority}`]; + } + }); + + return this.updateTaskFilters(appName, key, filters); + } } diff --git a/lib/process-services-cloud/src/lib/task/task-list/components/task-list-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/task/task-list/components/task-list-cloud.component.spec.ts index a28dd37f7f..2bd66f4387 100644 --- a/lib/process-services-cloud/src/lib/task/task-list/components/task-list-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/task/task-list/components/task-list-cloud.component.spec.ts @@ -27,7 +27,7 @@ import { ProcessServiceCloudTestingModule } from '../../../testing/process-servi import { TaskListCloudSortingModel } from '../../../models/task-list-sorting.model'; import { shareReplay, skip } from 'rxjs/operators'; import { TaskListCloudServiceInterface } from '../../../services/task-list-cloud.service.interface'; -import { TASK_LIST_CLOUD_TOKEN, TASK_LIST_PREFERENCES_SERVICE_TOKEN } from '../../../services/cloud-token.service'; +import { TASK_LIST_CLOUD_TOKEN, TASK_LIST_PREFERENCES_SERVICE_TOKEN, TASK_SEARCH_API_METHOD_TOKEN } from '../../../services/cloud-token.service'; import { TaskListCloudModule } from '../task-list-cloud.module'; import { PreferenceCloudServiceInterface } from '../../../services/preference-cloud.interface'; import { HarnessLoader } from '@angular/cdk/testing'; @@ -100,7 +100,7 @@ describe('TaskListCloudComponent', () => { updatePreference: of({}) }); - beforeEach(() => { + const configureTestingModule = (providers: any[]) => { TestBed.configureTestingModule({ imports: [ProcessServiceCloudTestingModule], providers: [ @@ -111,7 +111,8 @@ describe('TaskListCloudComponent', () => { { provide: TASK_LIST_PREFERENCES_SERVICE_TOKEN, useValue: preferencesService - } + }, + ...providers ] }); appConfig = TestBed.inject(AppConfigService); @@ -141,352 +142,512 @@ describe('TaskListCloudComponent', () => { component.isColumnSchemaCreated$ = of(true).pipe(shareReplay(1)); loader = TestbedHarnessEnvironment.loader(fixture); - }); + }; afterEach(() => { fixture.destroy(); }); - it('should be able to inject TaskListCloudService instance', () => { - fixture.detectChanges(); - - expect(component.taskListCloudService instanceof TaskListCloudService).toBeTruthy(); - }); - - it('should use the default schemaColumn as default', () => { - component.ngAfterContentInit(); - expect(component.columns).toBeDefined(); - expect(component.columns.length).toEqual(3); - }); - - it('should display empty content when process list is empty', async () => { - const emptyList = { list: { entries: [] } }; - spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(emptyList)); - fixture.detectChanges(); - - const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); - component.ngOnChanges({ appName }); - fixture.detectChanges(); - - expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(false); - - const emptyContent = fixture.debugElement.query(By.css('.adf-empty-content')); - expect(emptyContent.nativeElement).toBeDefined(); - }); - - it('should load spinner and show the content', async () => { - spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); - const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); - - fixture.detectChanges(); - component.ngOnChanges({ appName }); - fixture.detectChanges(); - - expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(false); - - const emptyContent = fixture.debugElement.query(By.css('.adf-empty-content')); - expect(emptyContent).toBeFalsy(); - - expect(component.rows.length).toEqual(1); - }); - - it('should use the custom schemaColumn from app.config.json', () => { - component.presetColumn = 'fakeCustomSchema'; - component.ngAfterContentInit(); - fixture.detectChanges(); - expect(component.columns).toEqual(fakeCustomSchema); - }); - - it('should hide columns on applying new columns visibility through columns selector', () => { - component.showMainDatatableActions = true; - spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); - component.ngAfterContentInit(); - - const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); - component.ngOnChanges({ appName }); - - fixture.detectChanges(); - - const mainMenuButton = fixture.debugElement.query(By.css('[data-automation-id="adf-datatable-main-menu-button"]')); - mainMenuButton.triggerEventHandler('click', {}); - fixture.detectChanges(); - - const columnSelectorMenu = fixture.debugElement.query(By.css('adf-datatable-column-selector')); - expect(columnSelectorMenu).toBeTruthy(); - - const columnsSelectorInstance = columnSelectorMenu.componentInstance as ColumnsSelectorComponent; - expect(columnsSelectorInstance.columns).toBe(component.columns, 'should pass columns as input'); - - const newColumns = (component.columns as DataColumn[]).map((column, index) => ({ - ...column, - isHidden: index !== 0 // only first one is shown - })); - - columnSelectorMenu.triggerEventHandler('submitColumnsVisibility', newColumns); - fixture.detectChanges(); - - const displayedColumns = fixture.debugElement.queryAll(By.css('.adf-datatable-cell-header')); - expect(displayedColumns.length).toBe(2, 'only column with isHidden set to false and action column should be shown'); - }); - - it('should fetch custom schemaColumn when the input presetColumn is defined', () => { - component.presetColumn = 'fakeCustomSchema'; - fixture.detectChanges(); - expect(component.columns).toBeDefined(); - expect(component.columns.length).toEqual(2); - }); - - it('should return an empty task list when no input parameters are passed', () => { - component.ngAfterContentInit(); - expect(component.rows).toBeDefined(); - expect(component.isListEmpty()).toBeTruthy(); - }); - - it('should return the results if an application name is given', (done) => { - spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); - - component.success.subscribe((res) => { - expect(res).toBeDefined(); - expect(component.rows).toBeDefined(); - expect(component.isListEmpty()).not.toBeTruthy(); - expect(component.rows.length).toEqual(1); - - const expectedTask = { - ...fakeGlobalTask, - variables: fakeGlobalTask.processVariables - }; - - expect(component.rows[0]).toEqual(expectedTask); - done(); - }); - - component.reload(); - fixture.detectChanges(); - }); - - it('should emit row click event', (done) => { - const row = new ObjectDataRow({ id: '999' }); - const rowEvent = new DataRowEvent(row, null); - component.rowClick.subscribe((taskId) => { - expect(taskId).toEqual('999'); - expect(component.currentInstanceId).toEqual('999'); - done(); - }); - component.onRowClick(rowEvent); - }); - - it('should re-create columns when a column width gets changed', () => { - component.reload(); - fixture.detectChanges(); - - const newColumns = [...component.columns]; - newColumns[0].width = 120; - component.onColumnsWidthChanged(newColumns); - - expect(component.columns[0].width).toBe(120); - }); - - it('should update columns widths when a column width gets changed', () => { - component.appName = 'fake-app-name'; - component.reload(); - fixture.detectChanges(); - - const newColumns = [...component.columns]; - newColumns[0].width = 120; - component.onColumnsWidthChanged(newColumns); - - expect(component.columns[0].width).toBe(120); - expect(preferencesService.updatePreference).toHaveBeenCalledWith('fake-app-name', 'tasks-list-cloud-columns-widths', { - name: 120 - }); - }); - - it('should update columns widths while preserving previously saved widths when a column width gets changed', () => { - component.appName = 'fake-app-name'; - component.reload(); - fixture.detectChanges(); - - const newColumns = [...component.columns]; - newColumns[0].width = 120; - component.onColumnsWidthChanged(newColumns); - - expect(component.columns[0].width).toBe(120); - expect(preferencesService.updatePreference).toHaveBeenCalledWith('fake-app-name', 'tasks-list-cloud-columns-widths', { - name: 120 - }); - - newColumns[1].width = 150; - component.onColumnsWidthChanged(newColumns); - - expect(component.columns[0].width).toBe(120); - expect(component.columns[1].width).toBe(150); - expect(preferencesService.updatePreference).toHaveBeenCalledWith('fake-app-name', 'tasks-list-cloud-columns-widths', { - name: 120, - created: 150 - }); - }); - - it('should re-create columns when a column order gets changed', () => { - component.reload(); - fixture.detectChanges(); - - expect(component.columns[0].title).toBe('ADF_CLOUD_TASK_LIST.PROPERTIES.NAME'); - expect(component.columns[1].title).toBe('ADF_CLOUD_TASK_LIST.PROPERTIES.CREATED'); - expect(component.columns[2].title).toBe('ADF_CLOUD_TASK_LIST.PROPERTIES.ASSIGNEE'); - - component.onColumnOrderChanged([component.columns[1], ...component.columns]); - fixture.detectChanges(); - - expect(component.columns[0].title).toBe('ADF_CLOUD_TASK_LIST.PROPERTIES.CREATED'); - expect(component.columns[1].title).toBe('ADF_CLOUD_TASK_LIST.PROPERTIES.NAME'); - expect(component.columns[2].title).toBe('ADF_CLOUD_TASK_LIST.PROPERTIES.ASSIGNEE'); - }); - - it('should create datatable schema when a column visibility gets changed', () => { - component.ngAfterContentInit(); - spyOn(component, 'createDatatableSchema'); - - component.onColumnsVisibilityChange(component.columns); - - fixture.detectChanges(); - - expect(component.createDatatableSchema).toHaveBeenCalled(); - }); - - it('should call endpoint when a column visibility gets changed', () => { - spyOn(taskListCloudService, 'getTaskByRequest'); - component.ngAfterContentInit(); - spyOn(component, 'createDatatableSchema'); - component.appName = 'fake-app-name'; - component.reload(); - fixture.detectChanges(); - - component.onColumnsVisibilityChange(component.columns); - - fixture.detectChanges(); - - expect(taskListCloudService.getTaskByRequest).toHaveBeenCalledTimes(1); - }); - - describe('component changes', () => { + describe('TASK_SEARCH_API_METHOD_TOKEN injected with GET value', () => { beforeEach(() => { - component.rows = fakeGlobalTasks.list.entries; + configureTestingModule([{ provide: TASK_SEARCH_API_METHOD_TOKEN, useValue: 'GET' }]); + }); + + it('should load spinner and show the content', async () => { + spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); + const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); + + fixture.detectChanges(); + component.ngOnChanges({ appName }); + fixture.detectChanges(); + + expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(false); + + const emptyContent = fixture.debugElement.query(By.css('.adf-empty-content')); + expect(emptyContent).toBeFalsy(); + + expect(component.rows.length).toEqual(1); + }); + + it('should hide columns on applying new columns visibility through columns selector', () => { + component.showMainDatatableActions = true; + spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); + component.ngAfterContentInit(); + + const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); + component.ngOnChanges({ appName }); + + fixture.detectChanges(); + + const mainMenuButton = fixture.debugElement.query(By.css('[data-automation-id="adf-datatable-main-menu-button"]')); + mainMenuButton.triggerEventHandler('click', {}); + fixture.detectChanges(); + + const columnSelectorMenu = fixture.debugElement.query(By.css('adf-datatable-column-selector')); + expect(columnSelectorMenu).toBeTruthy(); + + const columnsSelectorInstance = columnSelectorMenu.componentInstance as ColumnsSelectorComponent; + expect(columnsSelectorInstance.columns).toBe(component.columns, 'should pass columns as input'); + + const newColumns = (component.columns as DataColumn[]).map((column, index) => ({ + ...column, + isHidden: index !== 0 // only first one is shown + })); + + columnSelectorMenu.triggerEventHandler('submitColumnsVisibility', newColumns); + fixture.detectChanges(); + + const displayedColumns = fixture.debugElement.queryAll(By.css('.adf-datatable-cell-header')); + expect(displayedColumns.length).toBe(2, 'only column with isHidden set to false and action column should be shown'); + }); + + it('should return the results if an application name is given', (done) => { + spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); + + component.success.subscribe((res) => { + expect(res).toBeDefined(); + expect(component.rows).toBeDefined(); + expect(component.isListEmpty()).not.toBeTruthy(); + expect(component.rows.length).toEqual(1); + + const expectedTask = { + ...fakeGlobalTask, + variables: fakeGlobalTask.processVariables + }; + + expect(component.rows[0]).toEqual(expectedTask); + done(); + }); + + component.reload(); fixture.detectChanges(); }); - it('should NOT reload the task list when no parameters changed', () => { + it('should call endpoint when a column visibility gets changed', () => { spyOn(taskListCloudService, 'getTaskByRequest'); - component.rows = null; + component.ngAfterContentInit(); + spyOn(component, 'createDatatableSchema'); + component.appName = 'fake-app-name'; + component.reload(); fixture.detectChanges(); + + component.onColumnsVisibilityChange(component.columns); + + fixture.detectChanges(); + + expect(taskListCloudService.getTaskByRequest).toHaveBeenCalledTimes(1); + }); + describe('component changes', () => { + beforeEach(() => { + component.rows = fakeGlobalTasks.list.entries; + fixture.detectChanges(); + }); + + it('should reload the task list when input parameters changed', () => { + const getTaskByRequestSpy = spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); + component.appName = 'mock-app-name'; + component.priority = 1; + component.status = 'mock-status'; + component.lastModifiedFrom = 'mock-lastmodified-date'; + component.owner = 'mock-owner-name'; + const priorityChange = new SimpleChange(undefined, 1, true); + const statusChange = new SimpleChange(undefined, 'mock-status', true); + const lastModifiedFromChange = new SimpleChange(undefined, 'mock-lastmodified-date', true); + const ownerChange = new SimpleChange(undefined, 'mock-owner-name', true); + component.ngOnChanges({ + priority: priorityChange, + status: statusChange, + lastModifiedFrom: lastModifiedFromChange, + owner: ownerChange + }); + fixture.detectChanges(); + expect(component.isListEmpty()).toBeFalsy(); + expect(getTaskByRequestSpy).toHaveBeenCalled(); + }); + + it('should reload task list when sorting on a column changes', () => { + const getTaskByRequestSpy = spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); + component.onSortingChanged( + new CustomEvent('sorting-changed', { + detail: { + key: 'fakeName', + direction: 'asc' + }, + bubbles: true + }) + ); + fixture.detectChanges(); + expect(component.sorting).toEqual([ + new TaskListCloudSortingModel({ + orderBy: 'fakeName', + direction: 'ASC' + }) + ]); + expect(component.formattedSorting).toEqual(['fakeName', 'asc']); + expect(component.isListEmpty()).toBeFalsy(); + expect(getTaskByRequestSpy).toHaveBeenCalled(); + }); + }); + }); + + describe('TASK_SEARCH_API_METHOD_TOKEN injected with POST value', () => { + beforeEach(() => { + configureTestingModule([{ provide: TASK_SEARCH_API_METHOD_TOKEN, useValue: 'POST' }]); + component.appName = 'mock-app-name'; + }); + + it('should load spinner and show the content', async () => { + spyOn(taskListCloudService, 'fetchTaskList').and.returnValue(of(fakeGlobalTasks)); + const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); + + fixture.detectChanges(); + component.ngOnChanges({ appName }); + fixture.detectChanges(); + + expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(false); + + const emptyContent = fixture.debugElement.query(By.css('.adf-empty-content')); + expect(emptyContent).toBeFalsy(); + + expect(component.rows.length).toEqual(1); + }); + + it('should hide columns on applying new columns visibility through columns selector', () => { + component.showMainDatatableActions = true; + spyOn(taskListCloudService, 'fetchTaskList').and.returnValue(of(fakeGlobalTasks)); + component.ngAfterContentInit(); + + const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); + component.ngOnChanges({ appName }); + + fixture.detectChanges(); + + const mainMenuButton = fixture.debugElement.query(By.css('[data-automation-id="adf-datatable-main-menu-button"]')); + mainMenuButton.triggerEventHandler('click', {}); + fixture.detectChanges(); + + const columnSelectorMenu = fixture.debugElement.query(By.css('adf-datatable-column-selector')); + expect(columnSelectorMenu).toBeTruthy(); + + const columnsSelectorInstance = columnSelectorMenu.componentInstance as ColumnsSelectorComponent; + expect(columnsSelectorInstance.columns).toBe(component.columns, 'should pass columns as input'); + + const newColumns = (component.columns as DataColumn[]).map((column, index) => ({ + ...column, + isHidden: index !== 0 // only first one is shown + })); + + columnSelectorMenu.triggerEventHandler('submitColumnsVisibility', newColumns); + fixture.detectChanges(); + + const displayedColumns = fixture.debugElement.queryAll(By.css('.adf-datatable-cell-header')); + expect(displayedColumns.length).toBe(2, 'only column with isHidden set to false and action column should be shown'); + }); + + it('should return the results if an application name is given', (done) => { + spyOn(taskListCloudService, 'fetchTaskList').and.returnValue(of(fakeGlobalTasks)); + + component.success.subscribe((res) => { + expect(res).toBeDefined(); + expect(component.rows).toBeDefined(); + expect(component.isListEmpty()).not.toBeTruthy(); + expect(component.rows.length).toEqual(1); + + const expectedTask = { + ...fakeGlobalTask, + variables: fakeGlobalTask.processVariables + }; + + expect(component.rows[0]).toEqual(expectedTask); + done(); + }); + + component.reload(); + fixture.detectChanges(); + }); + + it('should call endpoint when a column visibility gets changed', () => { + const fetchTaskListSpy = spyOn(taskListCloudService, 'fetchTaskList').and.returnValue(of(fakeGlobalTasks)); + component.ngAfterContentInit(); + spyOn(component, 'createDatatableSchema'); + component.appName = 'fake-app-name'; + component.reload(); + fixture.detectChanges(); + + component.onColumnsVisibilityChange(component.columns); + + fixture.detectChanges(); + + expect(fetchTaskListSpy).toHaveBeenCalledTimes(1); + }); + describe('component changes', () => { + beforeEach(() => { + component.rows = fakeGlobalTasks.list.entries; + component.appName = 'mock-app-name'; + fixture.detectChanges(); + }); + + it('should reload the task list when input parameters changed', () => { + const fetchTaskListSpy = spyOn(taskListCloudService, 'fetchTaskList').and.returnValue(of(fakeGlobalTasks)); + component.appName = 'mock-app-name'; + component.priorities = ['1', '2']; + component.statuses = ['mock-status-1', 'mock-status-2']; + component.lastModifiedFrom = 'mock-lastmodified-date'; + const prioritiesChange = new SimpleChange(undefined, ['1'], true); + const statusesChange = new SimpleChange(undefined, ['mock-status'], true); + const lastModifiedFromChange = new SimpleChange(undefined, 'mock-lastmodified-date', true); + component.ngOnChanges({ + priorities: prioritiesChange, + statuses: statusesChange, + lastModifiedFrom: lastModifiedFromChange + }); + fixture.detectChanges(); + expect(component.isListEmpty()).toBeFalsy(); + expect(fetchTaskListSpy).toHaveBeenCalled(); + }); + + it('should reload task list when sorting on a column changes', () => { + const fetchTaskListSpy = spyOn(taskListCloudService, 'fetchTaskList').and.returnValue(of(fakeGlobalTasks)); + fixture.detectChanges(); + component.onSortingChanged( + new CustomEvent('sorting-changed', { + detail: { + key: 'fakeName', + direction: 'asc' + }, + bubbles: true + }) + ); + fixture.detectChanges(); + expect(component.sorting).toEqual([ + new TaskListCloudSortingModel({ + orderBy: 'fakeName', + direction: 'ASC' + }) + ]); + expect(component.formattedSorting).toEqual(['fakeName', 'asc']); + expect(component.isListEmpty()).toBeFalsy(); + expect(fetchTaskListSpy).toHaveBeenCalled(); + }); + }); + }); + + describe('API agnostic', () => { + beforeEach(() => { + configureTestingModule([]); + }); + + it('should be able to inject TaskListCloudService instance', () => { + fixture.detectChanges(); + + expect(component.taskListCloudService instanceof TaskListCloudService).toBeTruthy(); + }); + + it('should use the default schemaColumn as default', () => { + component.ngAfterContentInit(); + expect(component.columns).toBeDefined(); + expect(component.columns.length).toEqual(3); + }); + + it('should display empty content when process list is empty', async () => { + const emptyList = { list: { entries: [] } }; + spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(emptyList)); + fixture.detectChanges(); + + const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); + component.ngOnChanges({ appName }); + fixture.detectChanges(); + + expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(false); + + const emptyContent = fixture.debugElement.query(By.css('.adf-empty-content')); + expect(emptyContent.nativeElement).toBeDefined(); + }); + + it('should use the custom schemaColumn from app.config.json', () => { + component.presetColumn = 'fakeCustomSchema'; + component.ngAfterContentInit(); + fixture.detectChanges(); + expect(component.columns).toEqual(fakeCustomSchema); + }); + + it('should fetch custom schemaColumn when the input presetColumn is defined', () => { + component.presetColumn = 'fakeCustomSchema'; + fixture.detectChanges(); + expect(component.columns).toBeDefined(); + expect(component.columns.length).toEqual(2); + }); + + it('should return an empty task list when no input parameters are passed', () => { + component.ngAfterContentInit(); + expect(component.rows).toBeDefined(); expect(component.isListEmpty()).toBeTruthy(); }); - it('should reload the task list when input parameters changed', () => { - const getTaskByRequestSpy = spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); - component.appName = 'mock-app-name'; - component.priority = 1; - component.status = 'mock-status'; - component.lastModifiedFrom = 'mock-lastmodified-date'; - component.owner = 'mock-owner-name'; - const priorityChange = new SimpleChange(undefined, 1, true); - const statusChange = new SimpleChange(undefined, 'mock-status', true); - const lastModifiedFromChange = new SimpleChange(undefined, 'mock-lastmodified-date', true); - const ownerChange = new SimpleChange(undefined, 'mock-owner-name', true); - component.ngOnChanges({ - priority: priorityChange, - status: statusChange, - lastModifiedFrom: lastModifiedFromChange, - owner: ownerChange - }); - fixture.detectChanges(); - expect(component.isListEmpty()).toBeFalsy(); - expect(getTaskByRequestSpy).toHaveBeenCalled(); - }); - - it('should set formattedSorting if sorting input changes', () => { - spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); - spyOn(component, 'formatSorting').and.callThrough(); - - component.appName = 'mock-app-name'; - const mockSort = [ - new TaskListCloudSortingModel({ - orderBy: 'startDate', - direction: 'DESC' - }) - ]; - const sortChange = new SimpleChange(undefined, mockSort, true); - component.ngOnChanges({ - sorting: sortChange - }); - fixture.detectChanges(); - expect(component.formatSorting).toHaveBeenCalledWith(mockSort); - expect(component.formattedSorting).toEqual(['startDate', 'desc']); - }); - - it('should reload task list when sorting on a column changes', () => { - const getTaskByRequestSpy = spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); - component.onSortingChanged( - new CustomEvent('sorting-changed', { - detail: { - key: 'fakeName', - direction: 'asc' - }, - bubbles: true - }) - ); - fixture.detectChanges(); - expect(component.sorting).toEqual([ - new TaskListCloudSortingModel({ - orderBy: 'fakeName', - direction: 'ASC' - }) - ]); - expect(component.formattedSorting).toEqual(['fakeName', 'asc']); - expect(component.isListEmpty()).toBeFalsy(); - expect(getTaskByRequestSpy).toHaveBeenCalled(); - }); - - it('should reset pagination when resetPaginationValues is called', (done) => { - spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); - - const size = component.size; - const skipCount = component.skipCount; - component.pagination.pipe(skip(3)).subscribe((updatedPagination) => { - fixture.detectChanges(); - expect(component.size).toBe(size); - expect(component.skipCount).toBe(skipCount); - expect(updatedPagination.maxItems).toEqual(size); - expect(updatedPagination.skipCount).toEqual(skipCount); + it('should emit row click event', (done) => { + const row = new ObjectDataRow({ id: '999' }); + const rowEvent = new DataRowEvent(row, null); + component.rowClick.subscribe((taskId) => { + expect(taskId).toEqual('999'); + expect(component.currentInstanceId).toEqual('999'); done(); }); - - const pagination = { - maxItems: 250, - skipCount: 200 - }; - component.updatePagination(pagination); - - component.resetPagination(); + component.onRowClick(rowEvent); }); - it('should set pagination and reload when updatePagination is called', (done) => { - spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); - spyOn(component, 'reload').and.stub(); + it('should re-create columns when a column width gets changed', () => { + component.reload(); + fixture.detectChanges(); - const pagination = { - maxItems: 250, - skipCount: 200 - }; - component.pagination.pipe(skip(1)).subscribe((updatedPagination) => { - fixture.detectChanges(); - expect(component.size).toBe(pagination.maxItems); - expect(component.skipCount).toBe(pagination.skipCount); - expect(updatedPagination.maxItems).toEqual(pagination.maxItems); - expect(updatedPagination.skipCount).toEqual(pagination.skipCount); - done(); + const newColumns = [...component.columns]; + newColumns[0].width = 120; + component.onColumnsWidthChanged(newColumns); + + expect(component.columns[0].width).toBe(120); + }); + + it('should update columns widths when a column width gets changed', () => { + component.appName = 'fake-app-name'; + component.reload(); + fixture.detectChanges(); + + const newColumns = [...component.columns]; + newColumns[0].width = 120; + component.onColumnsWidthChanged(newColumns); + + expect(component.columns[0].width).toBe(120); + expect(preferencesService.updatePreference).toHaveBeenCalledWith('fake-app-name', 'tasks-list-cloud-columns-widths', { + name: 120 + }); + }); + + it('should update columns widths while preserving previously saved widths when a column width gets changed', () => { + component.appName = 'fake-app-name'; + component.reload(); + fixture.detectChanges(); + + const newColumns = [...component.columns]; + newColumns[0].width = 120; + component.onColumnsWidthChanged(newColumns); + + expect(component.columns[0].width).toBe(120); + expect(preferencesService.updatePreference).toHaveBeenCalledWith('fake-app-name', 'tasks-list-cloud-columns-widths', { + name: 120 }); - component.updatePagination(pagination); + newColumns[1].width = 150; + component.onColumnsWidthChanged(newColumns); + + expect(component.columns[0].width).toBe(120); + expect(component.columns[1].width).toBe(150); + expect(preferencesService.updatePreference).toHaveBeenCalledWith('fake-app-name', 'tasks-list-cloud-columns-widths', { + name: 120, + created: 150 + }); + }); + + it('should re-create columns when a column order gets changed', () => { + component.reload(); + fixture.detectChanges(); + + expect(component.columns[0].title).toBe('ADF_CLOUD_TASK_LIST.PROPERTIES.NAME'); + expect(component.columns[1].title).toBe('ADF_CLOUD_TASK_LIST.PROPERTIES.CREATED'); + expect(component.columns[2].title).toBe('ADF_CLOUD_TASK_LIST.PROPERTIES.ASSIGNEE'); + + component.onColumnOrderChanged([component.columns[1], ...component.columns]); + fixture.detectChanges(); + + expect(component.columns[0].title).toBe('ADF_CLOUD_TASK_LIST.PROPERTIES.CREATED'); + expect(component.columns[1].title).toBe('ADF_CLOUD_TASK_LIST.PROPERTIES.NAME'); + expect(component.columns[2].title).toBe('ADF_CLOUD_TASK_LIST.PROPERTIES.ASSIGNEE'); + }); + + it('should create datatable schema when a column visibility gets changed', () => { + component.ngAfterContentInit(); + spyOn(component, 'createDatatableSchema'); + + component.onColumnsVisibilityChange(component.columns); + + fixture.detectChanges(); + + expect(component.createDatatableSchema).toHaveBeenCalled(); + }); + + describe('component changes', () => { + beforeEach(() => { + component.rows = fakeGlobalTasks.list.entries; + fixture.detectChanges(); + }); + + it('should NOT reload the task list when no parameters changed', () => { + spyOn(taskListCloudService, 'getTaskByRequest'); + component.rows = null; + fixture.detectChanges(); + expect(component.isListEmpty()).toBeTruthy(); + }); + + it('should set formattedSorting if sorting input changes', () => { + spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); + spyOn(component, 'formatSorting').and.callThrough(); + + component.appName = 'mock-app-name'; + const mockSort = [ + new TaskListCloudSortingModel({ + orderBy: 'startDate', + direction: 'DESC' + }) + ]; + const sortChange = new SimpleChange(undefined, mockSort, true); + component.ngOnChanges({ + sorting: sortChange + }); + fixture.detectChanges(); + expect(component.formatSorting).toHaveBeenCalledWith(mockSort); + expect(component.formattedSorting).toEqual(['startDate', 'desc']); + }); + + it('should reset pagination when resetPaginationValues is called', (done) => { + spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); + + const size = component.size; + const skipCount = component.skipCount; + component.pagination.pipe(skip(3)).subscribe((updatedPagination) => { + fixture.detectChanges(); + expect(component.size).toBe(size); + expect(component.skipCount).toBe(skipCount); + expect(updatedPagination.maxItems).toEqual(size); + expect(updatedPagination.skipCount).toEqual(skipCount); + done(); + }); + + const pagination = { + maxItems: 250, + skipCount: 200 + }; + component.updatePagination(pagination); + + component.resetPagination(); + }); + + it('should set pagination and reload when updatePagination is called', (done) => { + spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); + spyOn(component, 'reload').and.stub(); + + const pagination = { + maxItems: 250, + skipCount: 200 + }; + component.pagination.pipe(skip(1)).subscribe((updatedPagination) => { + fixture.detectChanges(); + expect(component.size).toBe(pagination.maxItems); + expect(component.skipCount).toBe(pagination.skipCount); + expect(updatedPagination.maxItems).toEqual(pagination.maxItems); + expect(updatedPagination.skipCount).toEqual(pagination.skipCount); + done(); + }); + + component.updatePagination(pagination); + }); }); }); }); @@ -505,6 +666,7 @@ describe('TaskListCloudComponent: Injecting custom colums for tasklist - CustomT }); taskListCloudService = TestBed.inject(TASK_LIST_CLOUD_TOKEN); spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); + spyOn(taskListCloudService, 'fetchTaskList').and.returnValue(of(fakeGlobalTasks)); fixtureCustom = TestBed.createComponent(CustomTaskListComponent); copyFixture = TestBed.createComponent(CustomCopyContentTaskListComponent); fixtureCustom.detectChanges(); @@ -559,6 +721,7 @@ describe('TaskListCloudComponent: Creating an empty custom template - EmptyTempl taskListCloudService = TestBed.inject(TASK_LIST_CLOUD_TOKEN); const emptyList = { list: { entries: [] } }; spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(emptyList)); + spyOn(taskListCloudService, 'fetchTaskList').and.returnValue(of(fakeGlobalTasks)); fixtureEmpty = TestBed.createComponent(EmptyTemplateComponent); fixtureEmpty.detectChanges(); @@ -578,7 +741,8 @@ describe('TaskListCloudComponent: Creating an empty custom template - EmptyTempl }); describe('TaskListCloudComponent: Copy cell content directive from app.config specifications', () => { - let taskSpy: jasmine.Spy; + let getTaskByRequestSpy: jasmine.Spy; + let fetchTaskListSpy: jasmine.Spy; let appConfig: AppConfigService; let taskListCloudService: TaskListCloudServiceInterface; let component: TaskListCloudComponent; @@ -619,7 +783,8 @@ describe('TaskListCloudComponent: Copy cell content directive from app.config sp }); fixture = TestBed.createComponent(TaskListCloudComponent); component = fixture.componentInstance; - taskSpy = spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); + getTaskByRequestSpy = spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTasks)); + fetchTaskListSpy = spyOn(taskListCloudService, 'fetchTaskList').and.returnValue(of(fakeGlobalTasks)); component.isColumnSchemaCreated$ = of(true); }); @@ -628,7 +793,6 @@ describe('TaskListCloudComponent: Copy cell content directive from app.config sp }); it('should show tooltip if config copyContent flag is true', () => { - taskSpy.and.returnValue(of(fakeGlobalTasks)); component.presetColumn = 'fakeCustomSchema'; component.reload(); @@ -643,19 +807,18 @@ describe('TaskListCloudComponent: Copy cell content directive from app.config sp }); it('should replace priority values', () => { - taskSpy.and.returnValue(of(fakeGlobalTasks)); component.presetColumn = 'fakeCustomSchema'; component.reload(); fixture.detectChanges(); - const cell = fixture.debugElement.query(By.css('[data-automation-id="text_ADF_CLOUD_TASK_LIST.PROPERTIES.PRIORITY_VALUES.NONE"]')); expect(cell.nativeElement.textContent).toEqual('ADF_CLOUD_TASK_LIST.PROPERTIES.PRIORITY_VALUES.NONE'); }); it('replacePriorityValues should return undefined when no rows defined', () => { const emptyList = { list: { entries: [] } }; - taskSpy.and.returnValue(of(emptyList)); + getTaskByRequestSpy.and.returnValue(of(emptyList)); + fetchTaskListSpy.and.returnValue(of(emptyList)); fixture.detectChanges(); const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); @@ -681,7 +844,6 @@ describe('TaskListCloudComponent: Copy cell content directive from app.config sp }); it('replacePriorityValues should return replaced value when rows are defined', () => { - taskSpy.and.returnValue(of(fakeGlobalTasks)); fixture.detectChanges(); const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); diff --git a/lib/process-services-cloud/src/lib/task/task-list/components/task-list-cloud.component.ts b/lib/process-services-cloud/src/lib/task/task-list/components/task-list-cloud.component.ts index d8e10d445a..a74fe8371e 100644 --- a/lib/process-services-cloud/src/lib/task/task-list/components/task-list-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/task/task-list/components/task-list-cloud.component.ts @@ -15,16 +15,16 @@ * limitations under the License. */ -import { Component, ViewEncapsulation, Input, Inject, OnDestroy } from '@angular/core'; +import { Component, ViewEncapsulation, Input, Inject, OnDestroy, Optional } from '@angular/core'; import { AppConfigService, UserPreferencesService } from '@alfresco/adf-core'; -import { TaskQueryCloudRequestModel } from '../../../models/filter-cloud-model'; +import { TaskListRequestModel, TaskQueryCloudRequestModel } from '../../../models/filter-cloud-model'; import { BaseTaskListCloudComponent } from './base-task-list-cloud.component'; import { TaskCloudService } from '../../services/task-cloud.service'; -import { TASK_LIST_CLOUD_TOKEN, TASK_LIST_PREFERENCES_SERVICE_TOKEN } from '../../../services/cloud-token.service'; +import { TASK_LIST_CLOUD_TOKEN, TASK_LIST_PREFERENCES_SERVICE_TOKEN, TASK_SEARCH_API_METHOD_TOKEN } from '../../../services/cloud-token.service'; import { PreferenceCloudServiceInterface } from '../../../services/preference-cloud.interface'; import { TaskListCloudServiceInterface } from '../../../services/task-list-cloud.service.interface'; -import { Subject, of, BehaviorSubject, combineLatest } from 'rxjs'; -import { map, switchMap, takeUntil, tap } from 'rxjs/operators'; +import { Subject, BehaviorSubject, combineLatest } from 'rxjs'; +import { filter, map, switchMap, take, takeUntil } from 'rxjs/operators'; import { VariableMapperService } from '../../../services/variable-mapper.sevice'; import { ProcessListDataColumnCustomData } from '../../../models/data-column-custom-data'; import { TaskCloudModel } from '../../../models/task-cloud.model'; @@ -145,6 +145,48 @@ export class TaskListCloudComponent extends BaseTaskListCloudComponent(); rows: TaskInstanceCloudListViewModel[] = []; @@ -156,6 +198,7 @@ export class TaskListCloudComponent extends BaseTaskListCloudComponent of(this.createRequestNode())), - tap((requestNode) => (this.requestNode = requestNode)), - switchMap((requestNode) => this.taskListCloudService.getTaskByRequest(requestNode)), + filter((isColumnSchemaCreated) => !!isColumnSchemaCreated), + take(1), + switchMap(() => { + if (this.searchMethod === 'POST') { + const requestNode = this.createTaskListRequestNode(); + return this.taskListCloudService.fetchTaskList(requestNode).pipe(take(1)); + } else { + const requestNode = this.createRequestNode(); + this.requestNode = requestNode; + return this.taskListCloudService.getTaskByRequest(requestNode); + } + }), takeUntil(this.onDestroyTaskList$) ) - .subscribe( - (tasks: { list: PaginatedEntries }) => { + .subscribe({ + next: (tasks: { list: PaginatedEntries }) => { const tasksWithVariables = tasks.list.entries.map((task) => ({ ...task, variables: task.processVariables @@ -196,14 +248,43 @@ export class TaskListCloudComponent extends BaseTaskListCloudComponent { + error: (error) => { this.error.emit(error); this.isReloadingSubject$.next(false); } - ); + }); } - createRequestNode(): TaskQueryCloudRequestModel { + private createTaskListRequestNode(): TaskListRequestModel { + const requestNode: TaskListRequestModel = { + appName: this.appName, + pagination: { + maxItems: this.size, + skipCount: this.skipCount + }, + sorting: this.sorting, + onlyStandalone: this.standalone, + name: this.names, + processDefinitionName: this.processDefinitionNames, + priority: this.priorities, + status: this.statuses, + completedBy: this.completedByUsers, + assignee: this.assignees, + createdFrom: this.createdFrom, + createdTo: this.createdTo, + lastModifiedFrom: this.lastModifiedFrom, + lastModifiedTo: this.lastModifiedTo, + dueDateFrom: this.dueDateFrom, + dueDateTo: this.dueDateTo, + completedFrom: this.completedFrom, + completedTo: this.completedTo, + variableKeys: this.getRequestNodeVariables() + }; + + return new TaskListRequestModel(requestNode); + } + + private createRequestNode(): TaskQueryCloudRequestModel { const requestNode = { appName: this.appName, assignee: this.assignee, diff --git a/lib/process-services-cloud/src/lib/task/task-list/services/task-list-cloud.service.spec.ts b/lib/process-services-cloud/src/lib/task/task-list/services/task-list-cloud.service.spec.ts index 364a6b026c..bd208b6d99 100644 --- a/lib/process-services-cloud/src/lib/task/task-list/services/task-list-cloud.service.spec.ts +++ b/lib/process-services-cloud/src/lib/task/task-list/services/task-list-cloud.service.spec.ts @@ -17,9 +17,10 @@ import { TestBed } from '@angular/core/testing'; import { TaskListCloudService } from './task-list-cloud.service'; -import { TaskQueryCloudRequestModel } from '../../../models/filter-cloud-model'; +import { TaskListRequestModel, TaskQueryCloudRequestModel } from '../../../models/filter-cloud-model'; import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module'; import { AdfHttpClient } from '@alfresco/adf-core/api'; +import { catchError, firstValueFrom, of } from 'rxjs'; describe('TaskListCloudService', () => { let service: TaskListCloudService; @@ -39,59 +40,126 @@ describe('TaskListCloudService', () => { requestSpy = spyOn(adfHttpClient, 'request'); }); - it('should append to the call all the parameters', (done) => { - const taskRequest = { appName: 'fakeName', skipCount: 0, maxItems: 20, service: 'fake-service' } as TaskQueryCloudRequestModel; - requestSpy.and.callFake(returnCallQueryParameters); - service.getTaskByRequest(taskRequest).subscribe((res) => { + describe('getTaskByRequest', () => { + it('should append to the call all the parameters', async () => { + const taskRequest = { + appName: 'fakeName', + skipCount: 0, + maxItems: 20, + service: 'fake-service' + } as TaskQueryCloudRequestModel; + requestSpy.and.callFake(returnCallQueryParameters); + + const res = await firstValueFrom(service.getTaskByRequest(taskRequest)); + expect(res).toBeDefined(); expect(res).not.toBeNull(); expect(res.skipCount).toBe(0); expect(res.maxItems).toBe(20); expect(res.service).toBe('fake-service'); - done(); }); - }); - it('should concat the app name to the request url', (done) => { - const taskRequest = { appName: 'fakeName', skipCount: 0, maxItems: 20, service: 'fake-service' } as TaskQueryCloudRequestModel; - requestSpy.and.callFake(returnCallUrl); - service.getTaskByRequest(taskRequest).subscribe((requestUrl) => { + it('should concat the app name to the request url', async () => { + const taskRequest = { + appName: 'fakeName', + skipCount: 0, + maxItems: 20, + service: 'fake-service' + } as TaskQueryCloudRequestModel; + requestSpy.and.callFake(returnCallUrl); + + const requestUrl = await firstValueFrom(service.getTaskByRequest(taskRequest)); + expect(requestUrl).toBeDefined(); expect(requestUrl).not.toBeNull(); expect(requestUrl).toContain('/fakeName/query/v1/tasks'); - done(); }); - }); - it('should concat the sorting to append as parameters', (done) => { - const taskRequest = { - appName: 'fakeName', - skipCount: 0, - maxItems: 20, - service: 'fake-service', - sorting: [ - { orderBy: 'NAME', direction: 'DESC' }, - { orderBy: 'TITLE', direction: 'ASC' } - ] - } as TaskQueryCloudRequestModel; - requestSpy.and.callFake(returnCallQueryParameters); - service.getTaskByRequest(taskRequest).subscribe((res) => { + it('should concat the sorting to append as parameters', async () => { + const taskRequest = { + appName: 'fakeName', + skipCount: 0, + maxItems: 20, + service: 'fake-service', + sorting: [ + { orderBy: 'NAME', direction: 'DESC' }, + { orderBy: 'TITLE', direction: 'ASC' } + ] + } as TaskQueryCloudRequestModel; + requestSpy.and.callFake(returnCallQueryParameters); + + const res = await firstValueFrom(service.getTaskByRequest(taskRequest)); + expect(res).toBeDefined(); expect(res).not.toBeNull(); expect(res.sort).toBe('NAME,DESC&TITLE,ASC'); - done(); + }); + + it('should return an error when app name is not specified', async () => { + const taskRequest = { appName: null } as TaskQueryCloudRequestModel; + requestSpy.and.callFake(returnCallUrl); + + const res = await firstValueFrom(service.getTaskByRequest(taskRequest).pipe(catchError((error) => of(error)))); + + expect(res).toBe('Appname not configured'); }); }); - it('should return an error when app name is not specified', (done) => { - const taskRequest = { appName: null } as TaskQueryCloudRequestModel; - requestSpy.and.callFake(returnCallUrl); - service.getTaskByRequest(taskRequest).subscribe( - () => {}, - (error) => { - expect(error).toBe('Appname not configured'); - done(); - } - ); + describe('fetchTaskList', () => { + it('should append to the call all the parameters', async () => { + const taskRequest = { + appName: 'fakeName', + pagination: { skipCount: 0, maxItems: 20 } + } as TaskListRequestModel; + requestSpy.and.callFake(returnCallQueryParameters); + + const res = await firstValueFrom(service.fetchTaskList(taskRequest)); + + expect(res).toBeDefined(); + expect(res).not.toBeNull(); + expect(res.skipCount).toBe(0); + expect(res.maxItems).toBe(20); + }); + + it('should concat the app name to the request url', async () => { + const taskRequest = { + appName: 'fakeName', + pagination: { skipCount: 0, maxItems: 20 } + } as TaskListRequestModel; + requestSpy.and.callFake(returnCallUrl); + + const res = await firstValueFrom(service.fetchTaskList(taskRequest)); + + expect(res).toBeDefined(); + expect(res).not.toBeNull(); + expect(res).toContain('/fakeName/query/v1/tasks/search'); + }); + + it('should concat the sorting to append as parameters', async () => { + const taskRequest = { + appName: 'fakeName', + pagination: { skipCount: 0, maxItems: 20 }, + sorting: [ + { orderBy: 'NAME', direction: 'DESC' }, + { orderBy: 'TITLE', direction: 'ASC' } + ] + } as TaskListRequestModel; + requestSpy.and.callFake(returnCallQueryParameters); + + const res = await firstValueFrom(service.fetchTaskList(taskRequest)); + + expect(res).toBeDefined(); + expect(res).not.toBeNull(); + expect(res.sort).toBe('NAME,DESC&TITLE,ASC'); + }); + + it('should return an error when app name is not specified', async () => { + const taskRequest = { appName: null } as TaskListRequestModel; + requestSpy.and.callFake(returnCallUrl); + + const res = await firstValueFrom(service.fetchTaskList(taskRequest).pipe(catchError((error) => of(error.message)))); + + expect(res).toBe('Appname not configured'); + }); }); }); diff --git a/lib/process-services-cloud/src/lib/task/task-list/services/task-list-cloud.service.ts b/lib/process-services-cloud/src/lib/task/task-list/services/task-list-cloud.service.ts index d0c8d7be1b..ad169cff5d 100644 --- a/lib/process-services-cloud/src/lib/task/task-list/services/task-list-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/task/task-list/services/task-list-cloud.service.ts @@ -16,7 +16,7 @@ */ import { Injectable } from '@angular/core'; -import { TaskQueryCloudRequestModel } from '../../../models/filter-cloud-model'; +import { TaskQueryCloudRequestModel, TaskListRequestModel } from '../../../models/filter-cloud-model'; import { Observable, throwError } from 'rxjs'; import { TaskListCloudSortingModel } from '../../../models/task-list-sorting.model'; import { BaseCloudService } from '../../../services/base-cloud.service'; @@ -29,6 +29,7 @@ export class TaskListCloudService extends BaseCloudService implements TaskListCl /** * Finds a task using an object with optional query properties. * + * @deprecated From Activiti 8.7.0 forward, use TaskListCloudService.fetchTaskList instead. * @param requestNode Query object * @param queryUrl Query url * @returns Task information @@ -56,6 +57,78 @@ export class TaskListCloudService extends BaseCloudService implements TaskListCl } } + /** + * Available from Activiti version 8.7.0 onwards. + * Retrieves a list of tasks using an object with optional query properties. + * + * @param requestNode Query object + * @param queryUrl Query url + * @returns List of tasks + */ + fetchTaskList(requestNode: TaskListRequestModel, queryUrl?: string): Observable { + if (!requestNode?.appName) { + return throwError(() => new Error('Appname not configured')); + } + + queryUrl = queryUrl || `${this.getBasePath(requestNode.appName)}/query/v1/tasks/search`; + + const queryParams = { + maxItems: requestNode.pagination?.maxItems || 25, + skipCount: requestNode.pagination?.skipCount || 0, + sort: this.buildSortingParam(requestNode.sorting || []) + }; + + const queryData = this.buildQueryData(requestNode); + + return this.post(queryUrl, queryData, queryParams).pipe( + map((response) => { + const entries = response.list?.entries; + if (entries) { + response.list.entries = entries.map((entryData) => entryData.entry) as any; + } + return response; + }) + ); + } + + getTaskListCounter(requestNode: TaskListRequestModel): Observable { + if (!requestNode.appName) { + return throwError(() => new Error('Appname not configured')); + } + return this.fetchTaskList(requestNode).pipe(map((tasks) => tasks.list.pagination.totalItems)); + } + + protected buildQueryData(requestNode: TaskListRequestModel) { + const variableKeys = requestNode.variableKeys?.length > 0 ? requestNode.variableKeys.join(',') : undefined; + + const queryData: any = { + status: requestNode.status, + processDefinitionName: requestNode.processDefinitionName, + assignee: requestNode.assignee, + priority: requestNode.priority, + name: requestNode.name, + completedBy: requestNode.completedBy, + completedFrom: requestNode.completedFrom, + completedTo: requestNode.completedTo, + createdFrom: requestNode.createdFrom, + createdTo: requestNode.createdTo, + dueDateFrom: requestNode.dueDateFrom, + dueDateTo: requestNode.dueDateTo, + variableKeys + }; + + Object.keys(queryData).forEach((key) => { + const value = queryData[key]; + const isValueEmpty = !value; + const isValueArrayWithEmptyValue = Array.isArray(value) && (value.length === 0 || value[0] === null); + if (isValueEmpty || isValueArrayWithEmptyValue) { + delete queryData[key]; + } + }); + + return queryData; + } + protected buildQueryParams(requestNode: TaskQueryCloudRequestModel): any { const queryParam: any = {}; for (const propertyKey in requestNode) {