AAE-26192 New task search API (#10319)

* AAE-26192 New task search API

* small changes

* use DI instead of inputs

* cr

* fix units
This commit is contained in:
Robert Duda 2024-10-23 15:01:51 +02:00 committed by GitHub
parent 2303cb2d59
commit 0a89d9be97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1622 additions and 868 deletions

View File

@ -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<any>` | | 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). |

View File

@ -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

View File

@ -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<TaskListRequestModel>) {
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)
});
}
}

View File

@ -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
>
</mat-chip-grid>

View File

@ -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.

View File

@ -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.
*

View File

@ -28,3 +28,9 @@ export const PROCESS_FILTERS_SERVICE_TOKEN = new InjectionToken<PreferenceCloudS
export const TASK_FILTERS_SERVICE_TOKEN = new InjectionToken<PreferenceCloudServiceInterface>('task-filters-cloud');
export const TASK_LIST_CLOUD_TOKEN = new InjectionToken<TaskListCloudServiceInterface>('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');

View File

@ -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<any>;
/**
* 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<any>;
}

View File

@ -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<number> {
return this.searchMethod === 'POST'
? this.taskListCloudService.getTaskListCounter(new TaskFilterCloudAdapter(filter))
: this.taskFilterCloudService.getTaskFilterCounter(filter);
}
initFilterCounterNotifications() {

View File

@ -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 = {

View File

@ -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;
}
}

View File

@ -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();
});

View File

@ -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<TaskFilterCloudModel[]> {
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);
}
}

View File

@ -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);

View File

@ -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<ProcessLi
@Input()
candidateGroupId: 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.
*/
@Input()
names: 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.
*/
@Input()
assignees: 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.
*/
@Input()
statuses: 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.
*/
@Input()
processDefinitionNames: 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.
*/
@Input()
priorities: 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.
*/
@Input()
completedByUsers: string[] = [];
private onDestroyTaskList$ = new Subject<boolean>();
rows: TaskInstanceCloudListViewModel[] = [];
@ -156,6 +198,7 @@ export class TaskListCloudComponent extends BaseTaskListCloudComponent<ProcessLi
);
constructor(
@Inject(TASK_SEARCH_API_METHOD_TOKEN) @Optional() private searchMethod: 'GET' | 'POST',
@Inject(TASK_LIST_CLOUD_TOKEN) public taskListCloudService: TaskListCloudServiceInterface,
appConfigService: AppConfigService,
taskCloudService: TaskCloudService,
@ -176,13 +219,22 @@ export class TaskListCloudComponent extends BaseTaskListCloudComponent<ProcessLi
this.isColumnSchemaCreated$
.pipe(
switchMap(() => 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<TaskCloudModel> }) => {
.subscribe({
next: (tasks: { list: PaginatedEntries<TaskCloudModel> }) => {
const tasksWithVariables = tasks.list.entries.map((task) => ({
...task,
variables: task.processVariables
@ -196,14 +248,43 @@ export class TaskListCloudComponent extends BaseTaskListCloudComponent<ProcessLi
this.isReloadingSubject$.next(false);
this.pagination.next(tasks.list.pagination);
},
(error) => {
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,

View File

@ -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');
});
});
});

View File

@ -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<any> {
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<any, TaskCloudNodePaging>(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<number> {
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) {