mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
AAE-49653 Migrating to batch count endpoint
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TaskCloudEngineEvent } from './engine-event-cloud.model';
|
||||
|
||||
/**
|
||||
* Entity types accepted by the `POST /query/v1/count` endpoint.
|
||||
*/
|
||||
export const FilterCounterEntityType = {
|
||||
TASK: 'TASK',
|
||||
PROCESS_INSTANCE: 'PROCESS_INSTANCE'
|
||||
} as const;
|
||||
|
||||
export type FilterCounterEntityType = (typeof FilterCounterEntityType)[keyof typeof FilterCounterEntityType];
|
||||
|
||||
export interface FilterCountersQuerySort {
|
||||
field: string;
|
||||
direction: string;
|
||||
isProcessVariable: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single query of the batched count request, holding the criteria of one filter.
|
||||
*/
|
||||
export interface FilterCountersQuery {
|
||||
status?: string[];
|
||||
assignee?: string[];
|
||||
sort?: FilterCountersQuerySort;
|
||||
[criteria: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload of the batched count request, one entry per counter to be resolved.
|
||||
*/
|
||||
export type FilterCountersRequest = {
|
||||
[entityType in FilterCounterEntityType]?: FilterCountersQuery[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Shape of a task or process filter the counters are resolved for.
|
||||
*/
|
||||
export interface FilterCounterCandidate {
|
||||
key: string;
|
||||
status?: string | null;
|
||||
statuses?: string[] | null;
|
||||
showCounter?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters of every entity type the counters are resolved for.
|
||||
*/
|
||||
export type FilterCountersFilters = {
|
||||
[entityType in FilterCounterEntityType]: any[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Counts returned by the batched count request, keyed by entity type and then by status.
|
||||
* e.g. `{ TASK: { ASSIGNED: 5, CREATED: 0 }, PROCESS_INSTANCE: { RUNNING: 5 } }`
|
||||
*/
|
||||
export type FilterCounters = {
|
||||
[entityType in FilterCounterEntityType]?: { [status: string]: number };
|
||||
};
|
||||
|
||||
export interface FilterCountersNotification {
|
||||
/** Engine events of the debounced batch that triggered the count request. */
|
||||
events: TaskCloudEngineEvent[];
|
||||
/** Counts resolved by a single call to the batched count endpoint. */
|
||||
counters: FilterCounters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the statuses of a filter, which the counters of the count response are keyed by.
|
||||
*
|
||||
* @param filter task or process filter
|
||||
* @param filter.status Status of the filter
|
||||
* @param filter.statuses Statuses of the filter
|
||||
* @returns the statuses of the filter, empty when the filter targets every status
|
||||
*/
|
||||
export function resolveFilterCounterStatuses(filter: { status?: string | null; statuses?: string[] | null }): string[] {
|
||||
const statuses = filter?.statuses?.length ? filter.statuses : filter?.status ? [filter.status] : [];
|
||||
|
||||
return statuses.filter((status) => !!status);
|
||||
}
|
||||
+141
-26
@@ -16,15 +16,15 @@
|
||||
*/
|
||||
|
||||
import { Component, SimpleChange } from '@angular/core';
|
||||
import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing';
|
||||
import { first, of, Subject, throwError } from 'rxjs';
|
||||
import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing';
|
||||
import { EMPTY, first, of, Subject, throwError } from 'rxjs';
|
||||
import { ProcessFilterCloudService } from '../../services/process-filter-cloud.service';
|
||||
import { ProcessFiltersCloudComponent } from './process-filters-cloud.component';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { PROCESS_FILTERS_SERVICE_TOKEN } from '../../../../services/cloud-token.service';
|
||||
import { LocalPreferenceCloudService } from '../../../../services/local-preference-cloud.service';
|
||||
import { mockProcessFilters } from '../../mock/process-filters-cloud.mock';
|
||||
import { AppConfigService, AppConfigServiceMock } from '@alfresco/adf-core';
|
||||
import { AppConfigService, AppConfigServiceMock, NoopAuthModule } from '@alfresco/adf-core';
|
||||
import { ProcessListCloudService } from '../../../process-list/services/process-list-cloud.service';
|
||||
import { ApolloTestingModule } from 'apollo-angular/testing';
|
||||
import { HarnessLoader } from '@angular/cdk/testing';
|
||||
@@ -32,39 +32,36 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
import { MatIconHarness } from '@angular/material/icon/testing';
|
||||
import { ActivatedRoute, provideRouter, Router } from '@angular/router';
|
||||
import { RouterTestingHarness } from '@angular/router/testing';
|
||||
import { TaskCloudEngineEvent } from '../../../../models/engine-event-cloud.model';
|
||||
import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service';
|
||||
import { FilterCountersNotification } from '../../../../models/filter-counters-cloud.model';
|
||||
import { ProcessFilterCloudModel } from '../../models/process-filter-cloud.model';
|
||||
|
||||
@Component({ selector: 'adf-cloud-dummy', template: '' })
|
||||
class DummyComponent {}
|
||||
|
||||
const ProcessFilterCloudServiceMock = {
|
||||
getProcessFilters: () => of(mockProcessFilters),
|
||||
getProcessNotificationSubscription: () => of([]),
|
||||
filterKeyToBeRefreshed$: of(mockProcessFilters[0].key)
|
||||
};
|
||||
|
||||
describe('ProcessFiltersCloudComponent', () => {
|
||||
let processFilterService: ProcessFilterCloudService;
|
||||
let filterCountersService: FilterCountersCloudService;
|
||||
let processListService: ProcessListCloudService;
|
||||
let component: ProcessFiltersCloudComponent;
|
||||
let fixture: ComponentFixture<ProcessFiltersCloudComponent>;
|
||||
let getProcessFiltersSpy: jasmine.Spy;
|
||||
let getProcessNotificationSubscriptionSpy: jasmine.Spy;
|
||||
let getFilterCountersNotificationsSpy: jasmine.Spy;
|
||||
let loader: HarnessLoader;
|
||||
let router: Router;
|
||||
|
||||
const configureTestingModule = async (searchApiMethod: 'GET' | 'POST') => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ProcessFiltersCloudComponent, ApolloTestingModule],
|
||||
imports: [NoopAuthModule, ProcessFiltersCloudComponent, ApolloTestingModule],
|
||||
providers: [
|
||||
{ provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService },
|
||||
{ provide: AppConfigService, useClass: AppConfigServiceMock },
|
||||
{
|
||||
provide: ProcessListCloudService,
|
||||
useValue: {
|
||||
getProcessCounter: () => of(10),
|
||||
getProcessListCount: () => of(10)
|
||||
}
|
||||
},
|
||||
ProcessListCloudService,
|
||||
{ provide: ProcessFilterCloudService, useValue: ProcessFilterCloudServiceMock },
|
||||
provideRouter([{ path: 'process-list-cloud', component: DummyComponent }]),
|
||||
{
|
||||
@@ -88,11 +85,15 @@ describe('ProcessFiltersCloudComponent', () => {
|
||||
component.searchApiMethod = searchApiMethod;
|
||||
|
||||
processFilterService = TestBed.inject(ProcessFilterCloudService);
|
||||
filterCountersService = TestBed.inject(FilterCountersCloudService);
|
||||
processListService = TestBed.inject(ProcessListCloudService);
|
||||
TestBed.inject(ActivatedRoute);
|
||||
router = TestBed.inject(Router);
|
||||
await RouterTestingHarness.create();
|
||||
getProcessFiltersSpy = spyOn(processFilterService, 'getProcessFilters').and.returnValue(of(mockProcessFilters));
|
||||
getProcessNotificationSubscriptionSpy = spyOn(processFilterService, 'getProcessNotificationSubscription').and.returnValue(of([]));
|
||||
getFilterCountersNotificationsSpy = spyOn(filterCountersService, 'getFilterCountersNotifications').and.returnValue(EMPTY);
|
||||
spyOn(processListService, 'getProcessCounter').and.returnValue(of(10));
|
||||
spyOn(processListService, 'getProcessListCount').and.returnValue(of(10));
|
||||
};
|
||||
|
||||
const bindAppName = async (appName = 'my-app-1') => {
|
||||
@@ -476,6 +477,95 @@ describe('ProcessFiltersCloudComponent', () => {
|
||||
expect(fetchSpy).not.toHaveBeenCalledWith(filterWithoutCounter);
|
||||
});
|
||||
|
||||
describe('Batched counters registration', () => {
|
||||
let registerFiltersSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
registerFiltersSpy = spyOn(filterCountersService, 'registerFilters');
|
||||
});
|
||||
|
||||
const registeredQueries = () => registerFiltersSpy.calls.mostRecent().args[1];
|
||||
|
||||
it('should register every filter with a counter enabled', async () => {
|
||||
getProcessFiltersSpy.and.returnValue(
|
||||
of(
|
||||
mockProcessFilters.map(
|
||||
(filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true, sort: 'startDate', order: 'DESC' })
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
await bindAppName('mock-app-name');
|
||||
|
||||
expect(registerFiltersSpy).toHaveBeenCalledWith('PROCESS_INSTANCE', jasmine.any(Array));
|
||||
// the first mock filter targets every status, so it holds no status to be keyed by
|
||||
expect(registeredQueries().length).toBe(2);
|
||||
expect(registeredQueries().map((query: any) => query.status)).toEqual([['RUNNING'], ['COMPLETED']]);
|
||||
});
|
||||
|
||||
it('should not register a filter without a counter enabled', async () => {
|
||||
getProcessFiltersSpy.and.returnValue(
|
||||
of([
|
||||
new ProcessFilterCloudModel({ ...mockProcessFilters[1], showCounter: true, sort: 'startDate', order: 'DESC' }),
|
||||
new ProcessFilterCloudModel({ ...mockProcessFilters[2], showCounter: false, sort: 'startDate', order: 'DESC' })
|
||||
])
|
||||
);
|
||||
|
||||
await bindAppName('mock-app-name');
|
||||
|
||||
expect(registeredQueries().length).toBe(1);
|
||||
expect(registeredQueries()[0].status).toEqual(['RUNNING']);
|
||||
});
|
||||
|
||||
it('should register the full criteria of a filter', async () => {
|
||||
getProcessFiltersSpy.and.returnValue(
|
||||
of([
|
||||
new ProcessFilterCloudModel({
|
||||
...mockProcessFilters[1],
|
||||
showCounter: true,
|
||||
sort: 'startDate',
|
||||
order: 'DESC',
|
||||
initiator: 'mock-user',
|
||||
processDefinitionName: 'mock-process'
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
await bindAppName('mock-app-name');
|
||||
|
||||
const query = registeredQueries()[0];
|
||||
expect(query.status).toEqual(['RUNNING']);
|
||||
expect(query.initiator).toEqual(['mock-user']);
|
||||
expect(query.processDefinitionName).toEqual(['mock-process']);
|
||||
expect(query.sort).toEqual({ field: 'startDate', direction: 'desc', isProcessVariable: false });
|
||||
});
|
||||
|
||||
it('should not register a filter targeting every status', async () => {
|
||||
getProcessFiltersSpy.and.returnValue(
|
||||
of([new ProcessFilterCloudModel({ ...mockProcessFilters[0], showCounter: true, sort: 'startDate', order: 'DESC' })])
|
||||
);
|
||||
|
||||
await bindAppName('mock-app-name');
|
||||
|
||||
expect(registeredQueries().length).toBe(0);
|
||||
});
|
||||
|
||||
it('should not break the filter list when the query of a filter cannot be built', async () => {
|
||||
getProcessFiltersSpy.and.returnValue(
|
||||
of([
|
||||
new ProcessFilterCloudModel({ ...mockProcessFilters[1], showCounter: true, sort: undefined, order: undefined }),
|
||||
new ProcessFilterCloudModel({ ...mockProcessFilters[2], showCounter: true, sort: 'startDate', order: 'DESC' })
|
||||
])
|
||||
);
|
||||
|
||||
await bindAppName('mock-app-name');
|
||||
|
||||
expect(component.filters.length).toBe(2);
|
||||
expect(registeredQueries().length).toBe(1);
|
||||
expect(registeredQueries()[0].status).toEqual(['COMPLETED']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Notifications config', () => {
|
||||
it('should read enableNotifications and notificationDebounceTime from app config on init', () => {
|
||||
const appConfigService = TestBed.inject(AppConfigService);
|
||||
@@ -507,22 +597,47 @@ describe('ProcessFiltersCloudComponent', () => {
|
||||
expect(component.notificationDebounceTime).toBe(5000);
|
||||
});
|
||||
|
||||
it('should debounce notification subscription using the configured debounce time', fakeAsync(() => {
|
||||
const notifications$ = new Subject<TaskCloudEngineEvent[]>();
|
||||
getProcessNotificationSubscriptionSpy.and.returnValue(notifications$.asObservable());
|
||||
const initNotifications = (showCounter: boolean): Subject<FilterCountersNotification> => {
|
||||
const notifications$ = new Subject<FilterCountersNotification>();
|
||||
getFilterCountersNotificationsSpy.and.returnValue(notifications$.asObservable());
|
||||
component.appName = 'mock-app-name';
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const updateFilterCountersSpy = spyOn(component, 'updateFilterCounters');
|
||||
component.filters = mockProcessFilters.map((filter) => ({ ...filter, showCounter }));
|
||||
|
||||
notifications$.next([]);
|
||||
tick(1000);
|
||||
expect(updateFilterCountersSpy).not.toHaveBeenCalled();
|
||||
return notifications$;
|
||||
};
|
||||
|
||||
tick(2000);
|
||||
expect(updateFilterCountersSpy).toHaveBeenCalledTimes(1);
|
||||
it('should update the counters with the counts resolved by the batched count request', fakeAsync(() => {
|
||||
const notifications$ = initNotifications(true);
|
||||
|
||||
notifications$.next({ events: [], counters: { PROCESS_INSTANCE: { RUNNING: 7 } } });
|
||||
|
||||
expect(component.counters['FakeRunningProcesses']).toBe(7);
|
||||
flush();
|
||||
}));
|
||||
|
||||
it('should fetch the counters of the filters not resolved by the batched count request on their own', fakeAsync(() => {
|
||||
const notifications$ = initNotifications(true);
|
||||
const updateFilterCounterSpy = spyOn(component, 'updateFilterCounter');
|
||||
|
||||
notifications$.next({ events: [], counters: { PROCESS_INSTANCE: { RUNNING: 7 } } });
|
||||
|
||||
// the RUNNING filter is resolved by the batch, the other two are fetched on their own
|
||||
expect(component.counters['FakeRunningProcesses']).toBe(7);
|
||||
expect(updateFilterCounterSpy).toHaveBeenCalledTimes(2);
|
||||
expect(updateFilterCounterSpy.calls.allArgs().map(([filter]) => filter.key)).toEqual(['FakeAllProcesses', 'completed-processes']);
|
||||
flush();
|
||||
}));
|
||||
|
||||
it('should not update the counter of a filter without counter enabled', fakeAsync(() => {
|
||||
const notifications$ = initNotifications(false);
|
||||
component.counters = {};
|
||||
|
||||
notifications$.next({ events: [], counters: { PROCESS_INSTANCE: { RUNNING: 7 } } });
|
||||
|
||||
expect(component.counters['FakeRunningProcesses']).toBeUndefined();
|
||||
flush();
|
||||
}));
|
||||
});
|
||||
@@ -531,7 +646,7 @@ describe('ProcessFiltersCloudComponent', () => {
|
||||
it('should make subscription', async () => {
|
||||
component.enableNotifications = true;
|
||||
await bindAppName('mock-app-name');
|
||||
expect(getProcessNotificationSubscriptionSpy).toHaveBeenCalled();
|
||||
expect(getFilterCountersNotificationsSpy).toHaveBeenCalledWith('mock-app-name');
|
||||
});
|
||||
|
||||
it('should not make subscription when notifications are disabled', async () => {
|
||||
@@ -539,7 +654,7 @@ describe('ProcessFiltersCloudComponent', () => {
|
||||
spyOn(appConfigService, 'get').and.callFake((key: string, defaultValue: any) => (key === 'notifications' ? false : defaultValue));
|
||||
await bindAppName('mock-app-name');
|
||||
|
||||
expect(getProcessNotificationSubscriptionSpy).not.toHaveBeenCalled();
|
||||
expect(getFilterCountersNotificationsSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit filter key when filter counter is set for first time', () => {
|
||||
|
||||
+56
-63
@@ -21,9 +21,12 @@ import { ProcessFilterCloudService } from '../../services/process-filter-cloud.s
|
||||
import { ProcessFilterCloudModel } from '../../models/process-filter-cloud.model';
|
||||
import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core';
|
||||
import { FilterParamsModel } from '../../../../task/task-filters/models/filter-cloud.model';
|
||||
import { catchError, debounceTime, map, shareReplay, tap } from 'rxjs/operators';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { ProcessListCloudService } from '../../../process-list/services/process-list-cloud.service';
|
||||
import { ProcessFilterCloudAdapter } from '../../../process-list/models/process-cloud-query-request.model';
|
||||
import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service';
|
||||
import { FilterCounterEntityType } from '../../../../models/filter-counters-cloud.model';
|
||||
import { FilterCountersManager, FilterCounterAdapter } from '../../../../services/filter-counters-manager';
|
||||
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
@@ -78,35 +81,71 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
||||
filters$: Observable<ProcessFilterCloudModel[]>;
|
||||
currentFilter?: ProcessFilterCloudModel;
|
||||
filters: ProcessFilterCloudModel[] = [];
|
||||
counters: { [key: string]: number } = {};
|
||||
updatedFiltersSet = new Set<string>();
|
||||
enableNotifications = true;
|
||||
notificationDebounceTime = 3000;
|
||||
currentFiltersValues: { [key: string]: number } = {};
|
||||
updatedFiltersSet = new Set<string>();
|
||||
private filtersLoadedFor?: string;
|
||||
private countersManager: FilterCountersManager<ProcessFilterCloudModel>;
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly processFilterCloudService = inject(ProcessFilterCloudService);
|
||||
private readonly translationService = inject(TranslationService);
|
||||
private readonly appConfigService = inject(AppConfigService);
|
||||
private readonly processListCloudService = inject(ProcessListCloudService);
|
||||
private readonly filterCountersCloudService = inject(FilterCountersCloudService);
|
||||
private readonly activatedRoute = inject(ActivatedRoute);
|
||||
protected readonly currentRouteFilterId = toSignal(this.activatedRoute.queryParamMap.pipe(map((params) => params.get('filterId'))));
|
||||
|
||||
ngOnInit() {
|
||||
this.enableNotifications = this.appConfigService.get('notifications', true);
|
||||
this.notificationDebounceTime = this.appConfigService.get('notificationDebounceTime', 3000);
|
||||
|
||||
if (!this.countersManager) {
|
||||
this.initCountersManager();
|
||||
}
|
||||
|
||||
if (!this.filtersLoadedFor) {
|
||||
this.getFilters(this.appName);
|
||||
}
|
||||
this.initProcessNotification();
|
||||
this.getFilterKeysAfterExternalRefreshing();
|
||||
this.countersManager.subscribeToExternalRefresh(this.processFilterCloudService.filterKeyToBeRefreshed$);
|
||||
}
|
||||
|
||||
private initCountersManager(): void {
|
||||
const counterAdapter: FilterCounterAdapter<ProcessFilterCloudModel> = {
|
||||
getFilterCounter: (filter) =>
|
||||
this.searchApiMethod === 'POST'
|
||||
? this.processListCloudService.getProcessListCount(new ProcessFilterCloudAdapter(filter))
|
||||
: this.processListCloudService.getProcessCounter(filter.appName, filter.status)
|
||||
};
|
||||
|
||||
this.countersManager = new FilterCountersManager(
|
||||
FilterCounterEntityType.PROCESS_INSTANCE,
|
||||
this.filterCountersCloudService,
|
||||
counterAdapter,
|
||||
this.destroyRef,
|
||||
{
|
||||
onFilterUpdated: (filterKey) => {
|
||||
this.updatedFilter.emit(filterKey);
|
||||
this.updatedFiltersSet.add(filterKey);
|
||||
this.counters = this.countersManager.counters;
|
||||
this.currentFiltersValues = this.countersManager.currentFiltersValues;
|
||||
},
|
||||
onCountersUpdated: () => {
|
||||
this.counters = this.countersManager.counters;
|
||||
this.currentFiltersValues = this.countersManager.currentFiltersValues;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
const appName = changes['appName'];
|
||||
const filter = changes['filterParam'];
|
||||
if (appName?.currentValue) {
|
||||
if (!this.countersManager) {
|
||||
this.initCountersManager();
|
||||
}
|
||||
this.getFilters(appName.currentValue);
|
||||
} else if (filter && filter.currentValue !== filter.previousValue) {
|
||||
this.selectFilterAndEmit(filter.currentValue);
|
||||
@@ -120,17 +159,19 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
||||
*/
|
||||
getFilters(appName: string): void {
|
||||
this.filtersLoadedFor = appName;
|
||||
const filters$ = this.processFilterCloudService.getProcessFilters(appName).pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
||||
const filters$ = this.filterCountersCloudService
|
||||
.getFilters(appName)
|
||||
.pipe(map((filters) => filters[FilterCounterEntityType.PROCESS_INSTANCE] as ProcessFilterCloudModel[]));
|
||||
this.filters$ = filters$.pipe(catchError(() => EMPTY));
|
||||
|
||||
filters$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
||||
next: (res) => {
|
||||
this.resetFilter();
|
||||
this.filters = res || [];
|
||||
this.initFilterCounters();
|
||||
this.countersManager.initCounters(this.filters);
|
||||
this.selectFilterAndEmit(this.filterParam);
|
||||
this.success.emit(res);
|
||||
this.updateFilterCounters();
|
||||
this.countersManager.loadCounters(appName);
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.error.emit(err);
|
||||
@@ -138,12 +179,8 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize counter collection for filters
|
||||
*/
|
||||
initFilterCounters() {
|
||||
this.filters.forEach((filter) => (this.counters[filter.key] = 0));
|
||||
}
|
||||
counters: { [key: string]: number } = {};
|
||||
currentFiltersValues: { [key: string]: number } = {};
|
||||
|
||||
/**
|
||||
* Pass the selected filter as next
|
||||
@@ -214,6 +251,7 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
||||
this.selectFilter(filter);
|
||||
this.filterClicked.emit(this.currentFilter);
|
||||
this.updateFilterCounter(this.currentFilter);
|
||||
this.countersManager.resetFilterUpdate(filter.key);
|
||||
this.updatedFiltersSet.delete(filter.key);
|
||||
} else {
|
||||
this.currentFilter = undefined;
|
||||
@@ -261,12 +299,7 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
||||
|
||||
initProcessNotification(): void {
|
||||
if (this.appName && this.enableNotifications) {
|
||||
this.processFilterCloudService
|
||||
.getProcessNotificationSubscription(this.appName)
|
||||
.pipe(debounceTime(this.notificationDebounceTime), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(() => {
|
||||
this.updateFilterCounters();
|
||||
});
|
||||
this.countersManager.subscribeToNotifications(this.appName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,9 +307,7 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
||||
* Iterate over filters and update counters
|
||||
*/
|
||||
updateFilterCounters(): void {
|
||||
this.filters.forEach((filter: ProcessFilterCloudModel) => {
|
||||
this.updateFilterCounter(filter);
|
||||
});
|
||||
this.countersManager.updateAllCounters();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -285,49 +316,11 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
||||
* @param filter filter
|
||||
*/
|
||||
updateFilterCounter(filter: ProcessFilterCloudModel): void {
|
||||
if (!filter?.showCounter) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetchProcessFilterCounter(filter)
|
||||
.pipe(
|
||||
tap((filterCounter) => {
|
||||
this.checkIfFilterValuesHasBeenUpdated(filter.key, filterCounter);
|
||||
})
|
||||
)
|
||||
.subscribe((data) => {
|
||||
this.counters = {
|
||||
...this.counters,
|
||||
[filter.key]: data
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
checkIfFilterValuesHasBeenUpdated(filterKey: string, filterValue: number): void {
|
||||
if (this.currentFiltersValues[filterKey] === undefined || this.currentFiltersValues[filterKey] !== filterValue) {
|
||||
this.currentFiltersValues[filterKey] = filterValue;
|
||||
this.updatedFilter.emit(filterKey);
|
||||
this.updatedFiltersSet.add(filterKey);
|
||||
}
|
||||
this.countersManager.updateSingleCounter(filter);
|
||||
this.counters = this.countersManager.counters;
|
||||
}
|
||||
|
||||
isFilterUpdated(filterName: string): boolean {
|
||||
return this.updatedFiltersSet.has(filterName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filer key when filter was refreshed by external action
|
||||
*
|
||||
*/
|
||||
getFilterKeysAfterExternalRefreshing(): void {
|
||||
this.processFilterCloudService.filterKeyToBeRefreshed$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((filterKey: string) => {
|
||||
this.updatedFiltersSet.delete(filterKey);
|
||||
});
|
||||
}
|
||||
|
||||
private fetchProcessFilterCounter(filter: ProcessFilterCloudModel): Observable<number> {
|
||||
return this.searchApiMethod === 'POST'
|
||||
? this.processListCloudService.getProcessListCount(new ProcessFilterCloudAdapter(filter))
|
||||
: this.processListCloudService.getProcessCounter(filter.appName, filter.status);
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -404,6 +404,12 @@ export class ProcessFilterCloudService {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use FilterCountersCloudService.getFilterCountersNotifications instead, which shares a single
|
||||
* subscription with the task filters and resolves the counters with a single request.
|
||||
* @param appName Name of the target app
|
||||
* @returns Process engine events
|
||||
*/
|
||||
getProcessNotificationSubscription(appName: string): Observable<TaskCloudEngineEvent[]> {
|
||||
return this.notificationCloudService
|
||||
.makeGQLQuery(appName, PROCESS_EVENT_SUBSCRIPTION_QUERY)
|
||||
|
||||
+7
-1
@@ -100,7 +100,13 @@ export class ProcessListCloudService extends BaseCloudService {
|
||||
);
|
||||
}
|
||||
|
||||
protected buildQueryData(requestNode: ProcessListRequestModel): { [key: string]: any } {
|
||||
/**
|
||||
* Builds the body of a process query, with the empty properties of the request stripped out.
|
||||
*
|
||||
* @param requestNode Query object
|
||||
* @returns Body of the query
|
||||
*/
|
||||
buildQueryData(requestNode: ProcessListRequestModel): { [key: string]: any } {
|
||||
const queryData: { [key: string]: any } = {
|
||||
name: requestNode.name,
|
||||
id: requestNode.id,
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { AppConfigService, NoopAuthModule } from '@alfresco/adf-core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { ApolloTestingModule } from 'apollo-angular/testing';
|
||||
import { FilterCountersCloudService } from './filter-counters-cloud.service';
|
||||
import { NotificationCloudService } from './notification-cloud.service';
|
||||
import { FilterCounterEntityType, FilterCounters, FilterCountersNotification } from '../models/filter-counters-cloud.model';
|
||||
|
||||
describe('FilterCountersCloudService', () => {
|
||||
let service: FilterCountersCloudService;
|
||||
let notificationCloudService: NotificationCloudService;
|
||||
let appConfigService: AppConfigService;
|
||||
let engineEvents$: Subject<any>;
|
||||
let makeGQLQuerySpy: jasmine.Spy;
|
||||
let postSpy: jasmine.Spy;
|
||||
|
||||
const countersMock: FilterCounters = {
|
||||
TASK: { ASSIGNED: 5, CREATED: 0 },
|
||||
PROCESS_INSTANCE: { RUNNING: 5 }
|
||||
};
|
||||
|
||||
const emitEvent = (eventType = 'TASK_CREATED') => engineEvents$.next({ data: { engineEvents: [{ eventType, entity: {} }] } });
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAuthModule, ApolloTestingModule]
|
||||
});
|
||||
|
||||
service = TestBed.inject(FilterCountersCloudService);
|
||||
notificationCloudService = TestBed.inject(NotificationCloudService);
|
||||
appConfigService = TestBed.inject(AppConfigService);
|
||||
appConfigService.config.bpmHost = 'https://fake-bpm-host.com';
|
||||
|
||||
engineEvents$ = new Subject<any>();
|
||||
makeGQLQuerySpy = spyOn(notificationCloudService, 'makeGQLQuery').and.returnValue(engineEvents$.asObservable() as any);
|
||||
postSpy = spyOn<any>(service, 'post').and.returnValue(new Subject<FilterCounters>().asObservable());
|
||||
|
||||
service.registerFilters(FilterCounterEntityType.TASK, [{ status: ['ASSIGNED'], assignee: ['mock-user'] }, { status: ['CREATED'] }]);
|
||||
service.registerFilters(FilterCounterEntityType.PROCESS_INSTANCE, [{ status: ['RUNNING'] }]);
|
||||
});
|
||||
|
||||
it('should return EMPTY when appName is not set', () => {
|
||||
let completed = false;
|
||||
service.getFilterCountersNotifications('').subscribe({ complete: () => (completed = true) });
|
||||
|
||||
expect(completed).toBeTrue();
|
||||
expect(makeGQLQuerySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should open a single subscription for multiple subscribers of the same app', () => {
|
||||
service.getFilterCountersNotifications('mock-app').subscribe();
|
||||
service.getFilterCountersNotifications('mock-app').subscribe();
|
||||
|
||||
expect(makeGQLQuerySpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should subscribe to both the task and the process engine events', () => {
|
||||
service.getFilterCountersNotifications('mock-app').subscribe();
|
||||
|
||||
const [appName, query] = makeGQLQuerySpy.calls.mostRecent().args;
|
||||
expect(appName).toBe('mock-app');
|
||||
expect(query).toContain('TASK_CREATED');
|
||||
expect(query).toContain('PROCESS_STARTED');
|
||||
});
|
||||
|
||||
it('should open a separate subscription per app', () => {
|
||||
service.getFilterCountersNotifications('mock-app').subscribe();
|
||||
service.getFilterCountersNotifications('other-app').subscribe();
|
||||
|
||||
expect(makeGQLQuerySpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should make a single count request for a batch of events received by multiple subscribers', fakeAsync(() => {
|
||||
postSpy.and.returnValue(new Subject<FilterCounters>().asObservable());
|
||||
service.getFilterCountersNotifications('mock-app').subscribe();
|
||||
service.getFilterCountersNotifications('mock-app').subscribe();
|
||||
|
||||
emitEvent('TASK_CREATED');
|
||||
emitEvent('PROCESS_STARTED');
|
||||
tick(3000);
|
||||
|
||||
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||
}));
|
||||
|
||||
it('should call the batched count endpoint with the queries of the registered filters', fakeAsync(() => {
|
||||
service.getFilterCountersNotifications('mock-app').subscribe();
|
||||
|
||||
emitEvent();
|
||||
tick(3000);
|
||||
|
||||
const [url, body] = postSpy.calls.mostRecent().args;
|
||||
expect(url).toBe('https://fake-bpm-host.com/mock-app/query/v1/count');
|
||||
expect(body).toEqual({
|
||||
TASK: [{ status: ['ASSIGNED'], assignee: ['mock-user'] }, { status: ['CREATED'] }],
|
||||
PROCESS_INSTANCE: [{ status: ['RUNNING'] }]
|
||||
});
|
||||
}));
|
||||
|
||||
it('should send the full criteria of every registered filter', fakeAsync(() => {
|
||||
service.registerFilters(FilterCounterEntityType.TASK, [
|
||||
{ status: ['ASSIGNED'], assignee: ['mock-user'] },
|
||||
{ status: ['ASSIGNED'], priority: ['4'], dueDateFrom: '2026-01-01' },
|
||||
{ status: ['SUSPENDED', 'CREATED'], processVariableFilters: [{ name: 'amount', value: '10' }] }
|
||||
]);
|
||||
service.getFilterCountersNotifications('mock-app').subscribe();
|
||||
|
||||
emitEvent();
|
||||
tick(3000);
|
||||
|
||||
const [, body] = postSpy.calls.mostRecent().args;
|
||||
expect(body.TASK.length).toBe(3);
|
||||
expect(body.TASK[1]).toEqual({ status: ['ASSIGNED'], priority: ['4'], dueDateFrom: '2026-01-01' });
|
||||
expect(body.TASK[2]).toEqual({ status: ['SUSPENDED', 'CREATED'], processVariableFilters: [{ name: 'amount', value: '10' }] });
|
||||
}));
|
||||
|
||||
it('should replace the previously registered queries of an entity type', fakeAsync(() => {
|
||||
service.registerFilters(FilterCounterEntityType.TASK, [{ status: ['COMPLETED'] }]);
|
||||
service.getFilterCountersNotifications('mock-app').subscribe();
|
||||
|
||||
emitEvent();
|
||||
tick(3000);
|
||||
|
||||
const [, body] = postSpy.calls.mostRecent().args;
|
||||
expect(body.TASK).toEqual([{ status: ['COMPLETED'] }]);
|
||||
}));
|
||||
|
||||
it('should not call the batched count endpoint when no filter is registered', fakeAsync(() => {
|
||||
service.registerFilters(FilterCounterEntityType.TASK, []);
|
||||
service.registerFilters(FilterCounterEntityType.PROCESS_INSTANCE, []);
|
||||
let notification: FilterCountersNotification;
|
||||
service.getFilterCountersNotifications('mock-app').subscribe((result) => (notification = result));
|
||||
|
||||
emitEvent();
|
||||
tick(3000);
|
||||
|
||||
expect(postSpy).not.toHaveBeenCalled();
|
||||
expect(notification.counters).toEqual({});
|
||||
}));
|
||||
|
||||
it('should omit the entity type of an entity without registered filters', fakeAsync(() => {
|
||||
service.registerFilters(FilterCounterEntityType.PROCESS_INSTANCE, []);
|
||||
service.getFilterCountersNotifications('mock-app').subscribe();
|
||||
|
||||
emitEvent();
|
||||
tick(3000);
|
||||
|
||||
const [, body] = postSpy.calls.mostRecent().args;
|
||||
expect(body.PROCESS_INSTANCE).toBeUndefined();
|
||||
expect(body.TASK.length).toBe(2);
|
||||
}));
|
||||
|
||||
it('should debounce the events using the configured debounce time', fakeAsync(() => {
|
||||
spyOn(appConfigService, 'get').and.callFake((key: string, defaultValue: any) => (key === 'notificationDebounceTime' ? 5000 : defaultValue));
|
||||
service.getFilterCountersNotifications('mock-app').subscribe();
|
||||
|
||||
emitEvent();
|
||||
tick(3000);
|
||||
expect(postSpy).not.toHaveBeenCalled();
|
||||
|
||||
tick(2000);
|
||||
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||
}));
|
||||
|
||||
it('should emit the events along with the resolved counters', fakeAsync(() => {
|
||||
const counters$ = new Subject<FilterCounters>();
|
||||
postSpy.and.returnValue(counters$.asObservable());
|
||||
let notification: FilterCountersNotification;
|
||||
service.getFilterCountersNotifications('mock-app').subscribe((result) => (notification = result));
|
||||
|
||||
emitEvent('TASK_ASSIGNED');
|
||||
tick(3000);
|
||||
counters$.next(countersMock);
|
||||
|
||||
expect(notification.counters).toEqual(countersMock);
|
||||
expect(notification.events.length).toBe(1);
|
||||
expect(notification.events[0].eventType).toBe('TASK_ASSIGNED');
|
||||
}));
|
||||
|
||||
it('should emit the events with empty counters when the count request fails', fakeAsync(() => {
|
||||
const counters$ = new Subject<FilterCounters>();
|
||||
postSpy.and.returnValue(counters$.asObservable());
|
||||
let notification: FilterCountersNotification;
|
||||
service.getFilterCountersNotifications('mock-app').subscribe((result) => (notification = result));
|
||||
|
||||
emitEvent();
|
||||
tick(3000);
|
||||
counters$.error(new Error('count failed'));
|
||||
|
||||
expect(notification.counters).toEqual({});
|
||||
expect(notification.events.length).toBe(1);
|
||||
}));
|
||||
|
||||
it('should keep emitting after a failed count request', fakeAsync(() => {
|
||||
const notifications: FilterCountersNotification[] = [];
|
||||
service.getFilterCountersNotifications('mock-app').subscribe((result) => notifications.push(result));
|
||||
|
||||
postSpy.and.callFake(() => {
|
||||
const counters$ = new Subject<FilterCounters>();
|
||||
setTimeout(() => counters$.error(new Error('count failed')));
|
||||
return counters$.asObservable();
|
||||
});
|
||||
emitEvent();
|
||||
tick(3000);
|
||||
tick(0);
|
||||
|
||||
postSpy.and.callFake(() => {
|
||||
const counters$ = new Subject<FilterCounters>();
|
||||
setTimeout(() => counters$.next(countersMock));
|
||||
return counters$.asObservable();
|
||||
});
|
||||
emitEvent();
|
||||
tick(3000);
|
||||
tick(0);
|
||||
|
||||
expect(notifications.length).toBe(2);
|
||||
expect(notifications[1].counters).toEqual(countersMock);
|
||||
}));
|
||||
|
||||
describe('loadFilterCounters', () => {
|
||||
beforeEach(() => {
|
||||
postSpy.and.returnValue(of(countersMock));
|
||||
});
|
||||
|
||||
it('should resolve the counters of both entity types with a single request', fakeAsync(() => {
|
||||
service.expectFilters(FilterCounterEntityType.TASK);
|
||||
service.expectFilters(FilterCounterEntityType.PROCESS_INSTANCE);
|
||||
const taskCounters: FilterCounters[] = [];
|
||||
const processCounters: FilterCounters[] = [];
|
||||
|
||||
// both components load their filter lists independently and subscribe before registering
|
||||
service.loadFilterCounters('mock-app').subscribe((counters) => taskCounters.push(counters));
|
||||
service.loadFilterCounters('mock-app').subscribe((counters) => processCounters.push(counters));
|
||||
service.registerFilters(FilterCounterEntityType.TASK, [{ status: ['ASSIGNED'] }]);
|
||||
service.registerFilters(FilterCounterEntityType.PROCESS_INSTANCE, [{ status: ['RUNNING'] }]);
|
||||
tick(1000);
|
||||
|
||||
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||
expect(taskCounters).toEqual([countersMock]);
|
||||
expect(processCounters).toEqual([countersMock]);
|
||||
}));
|
||||
|
||||
it('should send the queries of every entity type in one request', fakeAsync(() => {
|
||||
service.expectFilters(FilterCounterEntityType.TASK);
|
||||
service.expectFilters(FilterCounterEntityType.PROCESS_INSTANCE);
|
||||
|
||||
service.loadFilterCounters('mock-app').subscribe();
|
||||
service.registerFilters(FilterCounterEntityType.TASK, [{ status: ['ASSIGNED'] }, { status: ['CREATED'] }]);
|
||||
service.registerFilters(FilterCounterEntityType.PROCESS_INSTANCE, [{ status: ['RUNNING'] }]);
|
||||
tick(1000);
|
||||
|
||||
const [url, body] = postSpy.calls.mostRecent().args;
|
||||
expect(url).toBe('https://fake-bpm-host.com/mock-app/query/v1/count');
|
||||
expect(body).toEqual({
|
||||
TASK: [{ status: ['ASSIGNED'] }, { status: ['CREATED'] }],
|
||||
PROCESS_INSTANCE: [{ status: ['RUNNING'] }]
|
||||
});
|
||||
}));
|
||||
|
||||
it('should wait for every expected entity type before sending the request', fakeAsync(() => {
|
||||
service.expectFilters(FilterCounterEntityType.TASK);
|
||||
service.expectFilters(FilterCounterEntityType.PROCESS_INSTANCE);
|
||||
|
||||
service.loadFilterCounters('mock-app').subscribe();
|
||||
service.registerFilters(FilterCounterEntityType.TASK, [{ status: ['ASSIGNED'] }]);
|
||||
|
||||
expect(postSpy).not.toHaveBeenCalled();
|
||||
|
||||
service.registerFilters(FilterCounterEntityType.PROCESS_INSTANCE, [{ status: ['RUNNING'] }]);
|
||||
|
||||
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||
tick(1000);
|
||||
}));
|
||||
|
||||
it('should send the request once the only expected entity type registers', fakeAsync(() => {
|
||||
service.expectFilters(FilterCounterEntityType.TASK);
|
||||
|
||||
service.loadFilterCounters('mock-app').subscribe();
|
||||
service.registerFilters(FilterCounterEntityType.TASK, [{ status: ['ASSIGNED'] }]);
|
||||
|
||||
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||
const [, body] = postSpy.calls.mostRecent().args;
|
||||
expect(body.PROCESS_INSTANCE).toBeUndefined();
|
||||
tick(1000);
|
||||
}));
|
||||
|
||||
it('should not wait longer than the max wait for a missing registration', fakeAsync(() => {
|
||||
service.expectFilters(FilterCounterEntityType.TASK);
|
||||
service.expectFilters(FilterCounterEntityType.PROCESS_INSTANCE);
|
||||
let counters: FilterCounters;
|
||||
|
||||
// the process filters fail to load, so they never register
|
||||
service.loadFilterCounters('mock-app').subscribe((result) => (counters = result));
|
||||
service.registerFilters(FilterCounterEntityType.TASK, [{ status: ['ASSIGNED'] }]);
|
||||
|
||||
expect(postSpy).not.toHaveBeenCalled();
|
||||
|
||||
tick(1000);
|
||||
|
||||
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||
expect(counters).toEqual(countersMock);
|
||||
}));
|
||||
|
||||
it('should use the max wait from the app config', fakeAsync(() => {
|
||||
spyOn(appConfigService, 'get').and.callFake((key: string, defaultValue: any) =>
|
||||
key === 'filterCounterBatchMaxWait' ? 5000 : defaultValue
|
||||
);
|
||||
service.expectFilters(FilterCounterEntityType.TASK);
|
||||
service.expectFilters(FilterCounterEntityType.PROCESS_INSTANCE);
|
||||
|
||||
service.loadFilterCounters('mock-app').subscribe();
|
||||
tick(1000);
|
||||
expect(postSpy).not.toHaveBeenCalled();
|
||||
|
||||
tick(4000);
|
||||
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||
}));
|
||||
|
||||
it('should emit empty counters when the request fails', fakeAsync(() => {
|
||||
postSpy.and.returnValue(throwError(() => new Error('count failed')));
|
||||
service.expectFilters(FilterCounterEntityType.TASK);
|
||||
let counters: FilterCounters;
|
||||
|
||||
service.loadFilterCounters('mock-app').subscribe((result) => (counters = result));
|
||||
service.registerFilters(FilterCounterEntityType.TASK, [{ status: ['ASSIGNED'] }]);
|
||||
tick(1000);
|
||||
|
||||
expect(counters).toEqual({});
|
||||
}));
|
||||
|
||||
it('should start a new batch for a subsequent load of another app', fakeAsync(() => {
|
||||
service.expectFilters(FilterCounterEntityType.TASK);
|
||||
|
||||
service.loadFilterCounters('mock-app').subscribe();
|
||||
service.registerFilters(FilterCounterEntityType.TASK, [{ status: ['ASSIGNED'] }]);
|
||||
tick(1000);
|
||||
|
||||
service.loadFilterCounters('other-app').subscribe();
|
||||
service.registerFilters(FilterCounterEntityType.TASK, [{ status: ['CREATED'] }]);
|
||||
tick(1000);
|
||||
|
||||
expect(postSpy).toHaveBeenCalledTimes(2);
|
||||
expect(postSpy.calls.allArgs().map(([url]) => url)).toEqual([
|
||||
'https://fake-bpm-host.com/mock-app/query/v1/count',
|
||||
'https://fake-bpm-host.com/other-app/query/v1/count'
|
||||
]);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('resolveFilterCounter', () => {
|
||||
it('should resolve the counter of a filter by its status', () => {
|
||||
expect(service.resolveFilterCounter(countersMock, FilterCounterEntityType.TASK, { key: 'my-tasks', status: 'ASSIGNED' })).toBe(5);
|
||||
expect(service.resolveFilterCounter(countersMock, FilterCounterEntityType.TASK, { key: 'queued', statuses: ['CREATED'] })).toBe(0);
|
||||
expect(service.resolveFilterCounter(countersMock, FilterCounterEntityType.PROCESS_INSTANCE, { key: 'running', status: 'RUNNING' })).toBe(
|
||||
5
|
||||
);
|
||||
});
|
||||
|
||||
it('should sum the counters of a filter targeting more than one status', () => {
|
||||
expect(service.resolveFilterCounter(countersMock, FilterCounterEntityType.TASK, { key: 'mine', statuses: ['ASSIGNED', 'CREATED'] })).toBe(
|
||||
5
|
||||
);
|
||||
expect(
|
||||
service.resolveFilterCounter({ TASK: { ASSIGNED: 5, CREATED: 2, SUSPENDED: 3 } }, FilterCounterEntityType.TASK, {
|
||||
key: 'mine',
|
||||
statuses: ['ASSIGNED', 'SUSPENDED']
|
||||
})
|
||||
).toBe(8);
|
||||
});
|
||||
|
||||
it('should resolve the counter from the statuses held by the response', () => {
|
||||
expect(
|
||||
service.resolveFilterCounter(countersMock, FilterCounterEntityType.TASK, { key: 'mine', statuses: ['ASSIGNED', 'COMPLETED'] })
|
||||
).toBe(5);
|
||||
});
|
||||
|
||||
it('should not resolve the counter of a filter not held by the response', () => {
|
||||
expect(service.resolveFilterCounter(countersMock, FilterCounterEntityType.TASK, { key: 'done', status: 'COMPLETED' })).toBeUndefined();
|
||||
expect(service.resolveFilterCounter({}, FilterCounterEntityType.TASK, { key: 'my-tasks', status: 'ASSIGNED' })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not resolve the counter of a filter targeting every status', () => {
|
||||
expect(service.resolveFilterCounter(countersMock, FilterCounterEntityType.TASK, { key: 'all', status: '' })).toBeUndefined();
|
||||
expect(service.resolveFilterCounter(countersMock, FilterCounterEntityType.TASK, { key: 'all', statuses: [] })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCounterBatched', () => {
|
||||
it('should batch the counter of a filter targeting one or more statuses', () => {
|
||||
expect(service.isCounterBatched({ key: 'my-tasks', status: 'ASSIGNED' })).toBeTrue();
|
||||
expect(service.isCounterBatched({ key: 'mine', statuses: ['ASSIGNED', 'SUSPENDED'] })).toBeTrue();
|
||||
});
|
||||
|
||||
it('should not batch the counter of a filter targeting every status', () => {
|
||||
expect(service.isCounterBatched({ key: 'all', status: '' })).toBeFalse();
|
||||
expect(service.isCounterBatched({ key: 'all', statuses: [] })).toBeFalse();
|
||||
expect(service.isCounterBatched({ key: 'all' })).toBeFalse();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,261 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { combineLatest, defer, EMPTY, Observable, of } from 'rxjs';
|
||||
import { catchError, debounceTime, map, shareReplay, switchMap, take } from 'rxjs/operators';
|
||||
import { BaseCloudService } from './base-cloud.service';
|
||||
import { NotificationCloudService } from './notification-cloud.service';
|
||||
import { TaskCloudEngineEvent } from '../models/engine-event-cloud.model';
|
||||
import { TaskFilterCloudService } from '../task/task-filters/services/task-filter-cloud.service';
|
||||
import { ProcessFilterCloudService } from '../process/process-filters/services/process-filter-cloud.service';
|
||||
import { TaskListCloudService } from '../task/task-list/services/task-list-cloud.service';
|
||||
import { ProcessListCloudService } from '../process/process-list/services/process-list-cloud.service';
|
||||
import { TaskFilterCloudAdapter } from '../models/filter-cloud-model';
|
||||
import { ProcessFilterCloudAdapter } from '../process/process-list/models/process-cloud-query-request.model';
|
||||
import {
|
||||
FilterCounterCandidate,
|
||||
FilterCounterEntityType,
|
||||
FilterCounters,
|
||||
FilterCountersFilters,
|
||||
FilterCountersNotification,
|
||||
FilterCountersQuery,
|
||||
FilterCountersRequest,
|
||||
resolveFilterCounterStatuses
|
||||
} from '../models/filter-counters-cloud.model';
|
||||
|
||||
/**
|
||||
* Single subscription covering both the task and the process engine events, so that a batch of
|
||||
* events results in one call to the batched count endpoint.
|
||||
*/
|
||||
const FILTER_COUNTERS_EVENT_SUBSCRIPTION_QUERY = `
|
||||
subscription {
|
||||
engineEvents(eventType: [
|
||||
TASK_COMPLETED
|
||||
TASK_ASSIGNED
|
||||
TASK_ACTIVATED
|
||||
TASK_SUSPENDED
|
||||
TASK_CANCELLED
|
||||
TASK_CREATED
|
||||
PROCESS_CANCELLED
|
||||
PROCESS_COMPLETED
|
||||
PROCESS_CREATED
|
||||
PROCESS_RESUMED
|
||||
PROCESS_SUSPENDED
|
||||
PROCESS_STARTED
|
||||
]) {
|
||||
eventType
|
||||
entity
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Central place handling the filter counters of the task and the process filter components:
|
||||
* a single engine event subscription, debounced into a single batched count request.
|
||||
*
|
||||
* Every filter with a counter enabled is registered by its component through `registerFilters`,
|
||||
* so that the counters of both the task and the process filters are resolved by one request.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FilterCountersCloudService extends BaseCloudService {
|
||||
private readonly notificationCloudService = inject(NotificationCloudService);
|
||||
|
||||
private readonly taskFilterCloudService = inject(TaskFilterCloudService);
|
||||
private readonly processFilterCloudService = inject(ProcessFilterCloudService);
|
||||
private readonly taskListCloudService = inject(TaskListCloudService);
|
||||
private readonly processListCloudService = inject(ProcessListCloudService);
|
||||
|
||||
private readonly notificationsPerApp = new Map<string, Observable<FilterCountersNotification>>();
|
||||
private readonly filtersPerApp = new Map<string, Observable<FilterCountersFilters>>();
|
||||
private readonly countersPerApp = new Map<string, Observable<FilterCounters>>();
|
||||
|
||||
get notificationDebounceTime(): number {
|
||||
return this.appConfigService.get('notificationDebounceTime', 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Task and process filters of the app, loaded once and shared between the filter components,
|
||||
* so that a single place owns the filters the counters are resolved for.
|
||||
*
|
||||
* @param appName Name of the target app
|
||||
* @returns Task and process filters of the app
|
||||
*/
|
||||
getFilters(appName: string): Observable<FilterCountersFilters> {
|
||||
let filters$ = this.filtersPerApp.get(appName);
|
||||
if (!filters$) {
|
||||
filters$ = combineLatest({
|
||||
[FilterCounterEntityType.TASK]: this.taskFilterCloudService.getTaskListFilters(appName).pipe(catchError(() => of([]))),
|
||||
[FilterCounterEntityType.PROCESS_INSTANCE]: this.processFilterCloudService.getProcessFilters(appName).pipe(catchError(() => of([])))
|
||||
}).pipe(shareReplay({ bufferSize: 1, refCount: false }));
|
||||
|
||||
this.filtersPerApp.set(appName, filters$);
|
||||
}
|
||||
|
||||
return filters$;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counters of every filter of the app with a counter enabled, resolved by a single request
|
||||
* shared between the filter components. Both the task and the process filters are loaded
|
||||
* before the request is built, so that one call resolves the counters of both components.
|
||||
*
|
||||
* @param appName Name of the target app
|
||||
* @returns Counters keyed by entity type and status
|
||||
*/
|
||||
loadFilterCounters(appName: string): Observable<FilterCounters> {
|
||||
let counters$ = this.countersPerApp.get(appName);
|
||||
if (!counters$) {
|
||||
counters$ = this.getFilters(appName).pipe(
|
||||
take(1),
|
||||
switchMap((filters) => this.fetchFilterCounters(appName, this.buildRequest(filters))),
|
||||
catchError(() => of({} as FilterCounters)),
|
||||
shareReplay({ bufferSize: 1, refCount: false })
|
||||
);
|
||||
|
||||
this.countersPerApp.set(appName, counters$);
|
||||
}
|
||||
|
||||
return counters$;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the payload of the batched count request from the filters with a counter enabled.
|
||||
*
|
||||
* @param filters Task and process filters of the app
|
||||
* @returns Payload of the count request
|
||||
*/
|
||||
buildRequest(filters: FilterCountersFilters): FilterCountersRequest {
|
||||
const request: FilterCountersRequest = {};
|
||||
|
||||
const taskQueries = this.buildQueries(filters[FilterCounterEntityType.TASK], (filter) =>
|
||||
this.taskListCloudService.buildQueryData(new TaskFilterCloudAdapter(filter))
|
||||
);
|
||||
if (taskQueries.length) {
|
||||
request[FilterCounterEntityType.TASK] = taskQueries;
|
||||
}
|
||||
|
||||
const processQueries = this.buildQueries(filters[FilterCounterEntityType.PROCESS_INSTANCE], (filter) =>
|
||||
this.processListCloudService.buildQueryData(new ProcessFilterCloudAdapter(filter))
|
||||
);
|
||||
if (processQueries.length) {
|
||||
request[FilterCounterEntityType.PROCESS_INSTANCE] = processQueries;
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private buildQueries<T extends FilterCounterCandidate>(filters: T[], buildQuery: (filter: T) => FilterCountersQuery): FilterCountersQuery[] {
|
||||
return (filters ?? [])
|
||||
.filter((filter) => filter?.showCounter && this.isCounterBatched(filter))
|
||||
.map((filter) => {
|
||||
try {
|
||||
return buildQuery(filter);
|
||||
} catch {
|
||||
/* A filter the query cannot be built for is left out of the batch and counted on its own. */
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
.filter((query) => !!query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Engine events of the app, debounced and enriched with the counters resolved by a single
|
||||
* call to the batched count endpoint. The underlying subscription and count request are
|
||||
* shared between all the subscribers of the same app.
|
||||
*
|
||||
* @param appName Name of the target app
|
||||
* @returns Debounced engine events along with the resolved counters
|
||||
*/
|
||||
getFilterCountersNotifications(appName: string): Observable<FilterCountersNotification> {
|
||||
if (!appName) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
let notifications$ = this.notificationsPerApp.get(appName);
|
||||
if (!notifications$) {
|
||||
notifications$ = defer(() => this.notificationCloudService.makeGQLQuery(appName, FILTER_COUNTERS_EVENT_SUBSCRIPTION_QUERY)).pipe(
|
||||
map((events: any) => (events?.data?.engineEvents ?? []) as TaskCloudEngineEvent[]),
|
||||
debounceTime(this.notificationDebounceTime),
|
||||
switchMap((events) =>
|
||||
this.getFilters(appName).pipe(
|
||||
take(1),
|
||||
switchMap((filters) => this.fetchFilterCounters(appName, this.buildRequest(filters))),
|
||||
map((counters) => ({ events, counters })),
|
||||
catchError(() => of({ events, counters: {} as FilterCounters }))
|
||||
)
|
||||
),
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
);
|
||||
this.notificationsPerApp.set(appName, notifications$);
|
||||
}
|
||||
|
||||
return notifications$;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the counters of the given queries with a single request.
|
||||
*
|
||||
* @param appName Name of the target app
|
||||
* @param request Payload of the count request
|
||||
* @returns Counters keyed by entity type and status
|
||||
*/
|
||||
fetchFilterCounters(appName: string, request: FilterCountersRequest): Observable<FilterCounters> {
|
||||
if (!Object.keys(request).length) {
|
||||
return of({});
|
||||
}
|
||||
|
||||
const queryUrl = `${this.getBasePath(appName)}/query/v1/count`;
|
||||
|
||||
return this.post<FilterCountersRequest, FilterCounters>(queryUrl, request).pipe(map((counters) => counters || {}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the counter of a filter from a count response. The counters are keyed by status, so
|
||||
* the counter of a filter targeting more than one status is the sum of the counters of its statuses.
|
||||
* A filter targeting every status holds no status to be keyed by, so its counter is not resolved
|
||||
* by the batched request and is left to be fetched on its own.
|
||||
*
|
||||
* @param counters Counters resolved by the batched count endpoint
|
||||
* @param entityType Entity type of the filter
|
||||
* @param filter Filter the counter is read for
|
||||
* @returns The counter of the filter, or `undefined` when the response holds no counter for it
|
||||
*/
|
||||
resolveFilterCounter(counters: FilterCounters, entityType: FilterCounterEntityType, filter: FilterCounterCandidate): number | undefined {
|
||||
const entityCounters = counters?.[entityType];
|
||||
const statuses = resolveFilterCounterStatuses(filter);
|
||||
|
||||
if (!entityCounters || !statuses.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const countedStatuses = statuses.filter((status) => entityCounters[status] !== undefined);
|
||||
|
||||
return countedStatuses.length ? countedStatuses.reduce((total, status) => total + entityCounters[status], 0) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the counter of a filter is resolved by the batched count request. A filter targeting
|
||||
* every status is not, since the counters of the response are keyed by status.
|
||||
*
|
||||
* @param filter Filter with a counter enabled
|
||||
* @returns `true` when the counter of the filter is resolved by the batched request, otherwise `false`
|
||||
*/
|
||||
isCounterBatched(filter: FilterCounterCandidate): boolean {
|
||||
return resolveFilterCounterStatuses(filter).length > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DestroyRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { FilterCountersCloudService } from './filter-counters-cloud.service';
|
||||
import { FilterCounterCandidate, FilterCounterEntityType, FilterCounters } from '../models/filter-counters-cloud.model';
|
||||
|
||||
export interface FilterCounterAdapter<TFilter> {
|
||||
getFilterCounter(filter: TFilter): Observable<number>;
|
||||
}
|
||||
|
||||
export interface FilterCountersManagerCallbacks {
|
||||
onCountersUpdated?: () => void;
|
||||
onFilterUpdated?: (filterKey: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages filter counters for a specific entity type using composition.
|
||||
* Handles loading, updating, and notification subscriptions for filter counters.
|
||||
*/
|
||||
export class FilterCountersManager<TFilter extends FilterCounterCandidate> {
|
||||
counters: { [key: string]: number } = {};
|
||||
currentFiltersValues: { [key: string]: number } = {};
|
||||
updatedFiltersSet = new Set<string>();
|
||||
|
||||
private filters: TFilter[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly entityType: FilterCounterEntityType,
|
||||
private readonly filterCountersService: FilterCountersCloudService,
|
||||
private readonly counterAdapter: FilterCounterAdapter<TFilter>,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
private readonly callbacks: FilterCountersManagerCallbacks = {}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Initialize counters for all filters to 0
|
||||
*
|
||||
* @param filters List of filters to initialize counters for
|
||||
*/
|
||||
initCounters(filters: TFilter[]): void {
|
||||
this.filters = filters;
|
||||
filters.forEach((filter) => (this.counters[filter.key] = 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load filter counters on initial page load using the batched endpoint
|
||||
*
|
||||
* @param appName Name of the target app
|
||||
*/
|
||||
loadCounters(appName: string): void {
|
||||
this.filterCountersService
|
||||
.loadFilterCounters(appName)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((counters) => this.applyBatchedCounters(counters));
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to real-time counter updates via notifications
|
||||
*
|
||||
* @param appName Name of the target app
|
||||
*/
|
||||
subscribeToNotifications(appName: string): void {
|
||||
if (!appName) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.filterCountersService
|
||||
.getFilterCountersNotifications(appName)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(({ counters }) => {
|
||||
this.applyBatchedCounters(counters);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply counters from the batched response, falling back to individual requests
|
||||
* for filters not resolved by the batch.
|
||||
*
|
||||
* @param counters Batched filter counters
|
||||
*/
|
||||
private applyBatchedCounters(counters: FilterCounters): void {
|
||||
this.filters.forEach((filter) => {
|
||||
if (!filter?.showCounter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filterCounter = this.filterCountersService.resolveFilterCounter(counters, this.entityType, filter);
|
||||
if (filterCounter === undefined) {
|
||||
this.updateSingleCounter(filter);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setCounter(filter.key, filterCounter);
|
||||
});
|
||||
|
||||
this.callbacks.onCountersUpdated?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update counter for a single filter using individual request
|
||||
*
|
||||
* @param filter Filter to update the counter for
|
||||
*/
|
||||
updateSingleCounter(filter: TFilter): void {
|
||||
if (!filter?.showCounter) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.counterAdapter
|
||||
.getFilterCounter(filter)
|
||||
.pipe(
|
||||
tap((filterCounter) => {
|
||||
this.setCounter(filter.key, filterCounter);
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update counters for all filters
|
||||
*/
|
||||
updateAllCounters(): void {
|
||||
this.filters.forEach((filter) => this.updateSingleCounter(filter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set counter value and track if it changed
|
||||
*
|
||||
* @param filterKey Key of the filter to update
|
||||
* @param filterValue New counter value for the filter
|
||||
*/
|
||||
private setCounter(filterKey: string, filterValue: number): void {
|
||||
if (this.currentFiltersValues[filterKey] === undefined || this.currentFiltersValues[filterKey] !== filterValue) {
|
||||
this.currentFiltersValues[filterKey] = filterValue;
|
||||
this.updatedFiltersSet.add(filterKey);
|
||||
this.callbacks.onFilterUpdated?.(filterKey);
|
||||
}
|
||||
|
||||
this.counters = {
|
||||
...this.counters,
|
||||
[filterKey]: filterValue
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a filter has been updated
|
||||
*
|
||||
* @param filterKey Key of the filter to check
|
||||
* @returns True if the filter has been updated, false otherwise
|
||||
*/
|
||||
isFilterUpdated(filterKey: string): boolean {
|
||||
return this.updatedFiltersSet.has(filterKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a filter as viewed/not updated
|
||||
*
|
||||
* @param filterKey Key of the filter to reset
|
||||
*/
|
||||
resetFilterUpdate(filterKey: string): void {
|
||||
this.updatedFiltersSet.delete(filterKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to external refresh events from the filter service
|
||||
*
|
||||
* @param refreshSignal$ Observable emitting filter keys to refresh
|
||||
*/
|
||||
subscribeToExternalRefresh(refreshSignal$: Observable<string>): void {
|
||||
refreshSignal$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((filterKey: string) => {
|
||||
this.updatedFiltersSet.delete(filterKey);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
export * from './base-cloud.service';
|
||||
export * from './cloud-token.service';
|
||||
export * from './filter-counters-cloud.service';
|
||||
export * from './filter-counters-manager';
|
||||
export * from './form-fields.interfaces';
|
||||
export * from './local-preference-cloud.service';
|
||||
export * from './notification-cloud.service';
|
||||
|
||||
+144
-15
@@ -17,12 +17,12 @@
|
||||
|
||||
import { AppConfigService, NoopAuthModule } from '@alfresco/adf-core';
|
||||
import { Component, SimpleChange } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, fakeAsync, flush, tick } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, fakeAsync, flush } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { first, of, Subject, throwError } from 'rxjs';
|
||||
import { TASK_FILTERS_SERVICE_TOKEN } from '../../../../services/cloud-token.service';
|
||||
import { LocalPreferenceCloudService } from '../../../../services/local-preference-cloud.service';
|
||||
import { defaultTaskFiltersMock, fakeGlobalFilter, taskNotifications } from '../../mock/task-filters-cloud.mock';
|
||||
import { defaultTaskFiltersMock, fakeAllTaskFilter, fakeGlobalFilter, taskNotifications } from '../../mock/task-filters-cloud.mock';
|
||||
import { TaskFilterCloudService } from '../../services/task-filter-cloud.service';
|
||||
import { TaskFiltersCloudComponent } from './task-filters-cloud.component';
|
||||
import { TaskListCloudService } from '../../../task-list/services/task-list-cloud.service';
|
||||
@@ -35,6 +35,8 @@ import { TaskFilterCloudModel } from '../../models/filter-cloud.model';
|
||||
import { MatIconHarness } from '@angular/material/icon/testing';
|
||||
import { ActivatedRoute, provideRouter, Router } from '@angular/router';
|
||||
import { RouterTestingHarness } from '@angular/router/testing';
|
||||
import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service';
|
||||
import { FilterCountersNotification } from '../../../../models/filter-counters-cloud.model';
|
||||
|
||||
@Component({ selector: 'adf-cloud-dummy', template: '' })
|
||||
class DummyComponent {}
|
||||
@@ -50,9 +52,15 @@ describe('TaskFiltersCloudComponent', () => {
|
||||
let getTaskFilterCounterSpy: jasmine.Spy;
|
||||
let getTaskListFiltersSpy: jasmine.Spy;
|
||||
let getTaskListCountSpy: jasmine.Spy;
|
||||
let getTaskNotificationSubscriptionSpy: jasmine.Spy;
|
||||
let getFilterCountersNotificationsSpy: jasmine.Spy;
|
||||
let filterCountersService: FilterCountersCloudService;
|
||||
let router: Router;
|
||||
|
||||
const filterCountersNotificationMock: FilterCountersNotification = {
|
||||
events: taskNotifications,
|
||||
counters: { TASK: { ASSIGNED: 11, CREATED: 0 } }
|
||||
};
|
||||
|
||||
const configureTestingModule = async (searchApiMethod: 'GET' | 'POST') => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAuthModule, TaskFiltersCloudComponent, ApolloTestingModule],
|
||||
@@ -76,9 +84,12 @@ describe('TaskFiltersCloudComponent', () => {
|
||||
});
|
||||
taskFilterService = TestBed.inject(TaskFilterCloudService);
|
||||
taskListService = TestBed.inject(TaskListCloudService);
|
||||
filterCountersService = TestBed.inject(FilterCountersCloudService);
|
||||
getTaskFilterCounterSpy = spyOn(taskFilterService, 'getTaskFilterCounter').and.returnValue(of(11));
|
||||
getTaskListCountSpy = spyOn(taskListService, 'getTaskListCount').and.returnValue(of(11));
|
||||
getTaskNotificationSubscriptionSpy = spyOn(taskFilterService, 'getTaskNotificationSubscription').and.returnValue(of(taskNotifications));
|
||||
getFilterCountersNotificationsSpy = spyOn(filterCountersService, 'getFilterCountersNotifications').and.returnValue(
|
||||
of(filterCountersNotificationMock)
|
||||
);
|
||||
getTaskListFiltersSpy = spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(of(fakeGlobalFilter));
|
||||
|
||||
appConfigService = TestBed.inject(AppConfigService);
|
||||
@@ -306,30 +317,60 @@ describe('TaskFiltersCloudComponent', () => {
|
||||
});
|
||||
|
||||
it('should not subscribe to notifications when appName is missing', () => {
|
||||
getTaskNotificationSubscriptionSpy.calls.reset();
|
||||
getFilterCountersNotificationsSpy.calls.reset();
|
||||
component.appName = '';
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getTaskNotificationSubscriptionSpy).not.toHaveBeenCalled();
|
||||
expect(getFilterCountersNotificationsSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should debounce notification subscription using the configured debounce time', fakeAsync(() => {
|
||||
const notifications$ = new Subject<typeof taskNotifications>();
|
||||
getTaskNotificationSubscriptionSpy.and.returnValue(notifications$.asObservable());
|
||||
it('should subscribe to the notifications of the bound app', () => {
|
||||
component.appName = 'my-app-1';
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const updateFilterCountersSpy = spyOn(component, 'updateFilterCounters');
|
||||
expect(getFilterCountersNotificationsSpy).toHaveBeenCalledWith('my-app-1');
|
||||
});
|
||||
|
||||
notifications$.next(taskNotifications);
|
||||
tick(1000);
|
||||
expect(updateFilterCountersSpy).not.toHaveBeenCalled();
|
||||
it('should update the counters with the counts resolved by the batched count request', fakeAsync(() => {
|
||||
const notifications$ = new Subject<FilterCountersNotification>();
|
||||
getFilterCountersNotificationsSpy.and.returnValue(notifications$.asObservable());
|
||||
component.appName = 'my-app-1';
|
||||
|
||||
tick(2000);
|
||||
expect(updateFilterCountersSpy).toHaveBeenCalledTimes(1);
|
||||
fixture.detectChanges();
|
||||
|
||||
notifications$.next({ events: [], counters: { TASK: { ASSIGNED: 7 } } });
|
||||
|
||||
expect(component.counters['fake-involved-tasks']).toBe(7);
|
||||
flush();
|
||||
}));
|
||||
|
||||
it('should not update the counters of the filters not resolved by the batched count request', fakeAsync(() => {
|
||||
const notifications$ = new Subject<FilterCountersNotification>();
|
||||
getFilterCountersNotificationsSpy.and.returnValue(notifications$.asObservable());
|
||||
component.appName = 'my-app-1';
|
||||
|
||||
fixture.detectChanges();
|
||||
component.counters = { ...component.counters, 'fake-my-task1': 3 };
|
||||
|
||||
notifications$.next({ events: [], counters: { TASK: { ASSIGNED: 7 } } });
|
||||
|
||||
expect(component.counters['fake-my-task1']).toBe(3);
|
||||
flush();
|
||||
}));
|
||||
|
||||
it('should emit the events of the batch that triggered the count request', fakeAsync(() => {
|
||||
const notifications$ = new Subject<FilterCountersNotification>();
|
||||
getFilterCountersNotificationsSpy.and.returnValue(notifications$.asObservable());
|
||||
const filterCounterUpdatedSpy = spyOn(component.filterCounterUpdated, 'emit');
|
||||
component.appName = 'my-app-1';
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
notifications$.next(filterCountersNotificationMock);
|
||||
|
||||
expect(filterCounterUpdatedSpy).toHaveBeenCalledWith(taskNotifications);
|
||||
flush();
|
||||
}));
|
||||
});
|
||||
@@ -671,6 +712,94 @@ describe('TaskFiltersCloudComponent', () => {
|
||||
expect(fetchSpy).not.toHaveBeenCalledWith(filterWithoutCounter);
|
||||
});
|
||||
|
||||
describe('Batched counters registration', () => {
|
||||
let registerFiltersSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
registerFiltersSpy = spyOn(filterCountersService, 'registerFilters');
|
||||
});
|
||||
|
||||
const registeredQueries = () => registerFiltersSpy.calls.mostRecent().args[1];
|
||||
|
||||
it('should register every filter with a counter enabled', async () => {
|
||||
getTaskListFiltersSpy.and.returnValue(
|
||||
of([
|
||||
new TaskFilterCloudModel({ ...defaultTaskFiltersMock[0], showCounter: true, sort: 'createdDate', order: 'DESC' }),
|
||||
new TaskFilterCloudModel({ ...defaultTaskFiltersMock[1], showCounter: true, sort: 'createdDate', order: 'DESC' }),
|
||||
new TaskFilterCloudModel({ ...defaultTaskFiltersMock[2], showCounter: true, sort: 'createdDate', order: 'DESC' })
|
||||
])
|
||||
);
|
||||
|
||||
await bindAppName();
|
||||
|
||||
expect(registerFiltersSpy).toHaveBeenCalledWith('TASK', jasmine.any(Array));
|
||||
expect(registeredQueries().length).toBe(3);
|
||||
expect(registeredQueries().map((query: any) => query.status)).toEqual([['CREATED'], ['ASSIGNED'], ['COMPLETED']]);
|
||||
});
|
||||
|
||||
it('should not register a filter without a counter enabled', async () => {
|
||||
getTaskListFiltersSpy.and.returnValue(
|
||||
of([
|
||||
new TaskFilterCloudModel({ ...defaultTaskFiltersMock[0], showCounter: true, sort: 'createdDate', order: 'DESC' }),
|
||||
new TaskFilterCloudModel({ ...defaultTaskFiltersMock[1], showCounter: false, sort: 'createdDate', order: 'DESC' })
|
||||
])
|
||||
);
|
||||
|
||||
await bindAppName();
|
||||
|
||||
expect(registeredQueries().length).toBe(1);
|
||||
expect(registeredQueries()[0].status).toEqual(['CREATED']);
|
||||
});
|
||||
|
||||
it('should register the full criteria of a filter', async () => {
|
||||
getTaskListFiltersSpy.and.returnValue(
|
||||
of([
|
||||
new TaskFilterCloudModel({
|
||||
...defaultTaskFiltersMock[1],
|
||||
showCounter: true,
|
||||
sort: 'createdDate',
|
||||
order: 'DESC',
|
||||
assignee: 'mock-user',
|
||||
priority: 4
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
await bindAppName();
|
||||
|
||||
const query = registeredQueries()[0];
|
||||
expect(query.status).toEqual(['ASSIGNED']);
|
||||
expect(query.assignee).toEqual(['mock-user']);
|
||||
expect(query.priority).toEqual(['4']);
|
||||
expect(query.sort).toEqual({ field: 'createdDate', direction: 'desc', isProcessVariable: false });
|
||||
});
|
||||
|
||||
it('should not register a filter targeting every status', async () => {
|
||||
getTaskListFiltersSpy.and.returnValue(
|
||||
of([new TaskFilterCloudModel({ ...fakeAllTaskFilter, showCounter: true, sort: 'createdDate', order: 'DESC' })])
|
||||
);
|
||||
|
||||
await bindAppName();
|
||||
|
||||
expect(registeredQueries().length).toBe(0);
|
||||
});
|
||||
|
||||
it('should not break the filter list when the query of a filter cannot be built', async () => {
|
||||
getTaskListFiltersSpy.and.returnValue(
|
||||
of([
|
||||
new TaskFilterCloudModel({ ...defaultTaskFiltersMock[0], showCounter: true, sort: undefined, order: undefined }),
|
||||
new TaskFilterCloudModel({ ...defaultTaskFiltersMock[1], showCounter: true, sort: 'createdDate', order: 'DESC' })
|
||||
])
|
||||
);
|
||||
|
||||
await bindAppName();
|
||||
|
||||
expect(component.filters.length).toBe(2);
|
||||
expect(registeredQueries().length).toBe(1);
|
||||
expect(registeredQueries()[0].status).toEqual(['ASSIGNED']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Highlight Selected Filter', () => {
|
||||
const assignedTasksFilterKey = defaultTaskFiltersMock[0].key;
|
||||
|
||||
|
||||
+68
-63
@@ -16,16 +16,19 @@
|
||||
*/
|
||||
|
||||
import { Component, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
|
||||
import { EMPTY, Observable } from 'rxjs';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { TaskFilterCloudService } from '../../services/task-filter-cloud.service';
|
||||
import { FilterParamsModel, TaskFilterCloudModel } from '../../models/filter-cloud.model';
|
||||
import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core';
|
||||
import { catchError, debounceTime, map, shareReplay, tap } from 'rxjs/operators';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { BaseTaskFiltersCloudComponent } from '../base-task-filters-cloud.component';
|
||||
import { TaskDetailsCloudModel } from '../../../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 { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service';
|
||||
import { FilterCounterEntityType } from '../../../../models/filter-counters-cloud.model';
|
||||
import { FilterCountersManager, FilterCounterAdapter } from '../../../../services/filter-counters-manager';
|
||||
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
@@ -67,11 +70,12 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
||||
currentFilter: TaskFilterCloudModel;
|
||||
enableNotifications = true;
|
||||
notificationDebounceTime = 3000;
|
||||
currentFiltersValues: { [key: string]: number } = {};
|
||||
private filtersLoadedFor?: string;
|
||||
private countersManager: FilterCountersManager<TaskFilterCloudModel>;
|
||||
|
||||
private readonly taskFilterCloudService = inject(TaskFilterCloudService);
|
||||
private readonly taskListCloudService = inject(TaskListCloudService);
|
||||
private readonly filterCountersCloudService = inject(FilterCountersCloudService);
|
||||
private readonly translationService = inject(TranslationService);
|
||||
private readonly appConfigService = inject(AppConfigService);
|
||||
private readonly activatedRoute = inject(ActivatedRoute);
|
||||
@@ -80,17 +84,50 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
||||
ngOnInit() {
|
||||
this.enableNotifications = this.appConfigService.get('notifications', true);
|
||||
this.notificationDebounceTime = this.appConfigService.get('notificationDebounceTime', 3000);
|
||||
|
||||
if (!this.countersManager) {
|
||||
this.initCountersManager();
|
||||
}
|
||||
|
||||
if (!this.filtersLoadedFor) {
|
||||
this.getFilters(this.appName);
|
||||
}
|
||||
this.initFilterCounterNotifications();
|
||||
this.getFilterKeysAfterExternalRefreshing();
|
||||
this.countersManager.subscribeToExternalRefresh(this.taskFilterCloudService.filterKeyToBeRefreshed$);
|
||||
}
|
||||
|
||||
private initCountersManager(): void {
|
||||
const counterAdapter: FilterCounterAdapter<TaskFilterCloudModel> = {
|
||||
getFilterCounter: (filter) =>
|
||||
this.searchApiMethod === 'POST'
|
||||
? this.taskListCloudService.getTaskListCount(new TaskFilterCloudAdapter(filter))
|
||||
: this.taskFilterCloudService.getTaskFilterCounter(filter)
|
||||
};
|
||||
|
||||
this.countersManager = new FilterCountersManager(
|
||||
FilterCounterEntityType.TASK,
|
||||
this.filterCountersCloudService,
|
||||
counterAdapter,
|
||||
this.destroyRef,
|
||||
{
|
||||
onFilterUpdated: (filterKey) => {
|
||||
this.updatedFilter.emit(filterKey);
|
||||
this.counters = this.countersManager.counters;
|
||||
},
|
||||
onCountersUpdated: () => {
|
||||
this.counters = this.countersManager.counters;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
const appName = changes['appName'];
|
||||
const filter = changes['filterParam'];
|
||||
if (appName && appName.currentValue !== appName.previousValue) {
|
||||
if (!this.countersManager) {
|
||||
this.initCountersManager();
|
||||
}
|
||||
this.getFilters(appName.currentValue);
|
||||
} else if (filter && filter.currentValue !== filter.previousValue) {
|
||||
this.selectFilterAndEmit(filter.currentValue);
|
||||
@@ -104,16 +141,18 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
||||
*/
|
||||
getFilters(appName: string): void {
|
||||
this.filtersLoadedFor = appName;
|
||||
const filters$ = this.taskFilterCloudService.getTaskListFilters(appName).pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
||||
this.filters$ = filters$.pipe(catchError(() => EMPTY));
|
||||
const filters$ = this.filterCountersCloudService
|
||||
.getFilters(appName)
|
||||
.pipe(map((filters) => filters[FilterCounterEntityType.TASK] as TaskFilterCloudModel[]));
|
||||
this.filters$ = filters$.pipe(catchError(() => of([])));
|
||||
|
||||
filters$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
||||
next: (res) => {
|
||||
this.resetFilter();
|
||||
this.filters = res || [];
|
||||
this.initFilterCounters();
|
||||
this.countersManager.initCounters(this.filters);
|
||||
this.selectFilterAndEmit(this.filterParam);
|
||||
this.updateFilterCounters();
|
||||
this.countersManager.loadCounters(appName);
|
||||
this.success.emit(res);
|
||||
},
|
||||
error: (err) => {
|
||||
@@ -122,18 +161,11 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize counter collection for filters
|
||||
*/
|
||||
initFilterCounters(): void {
|
||||
this.filters.forEach((filter) => (this.counters[filter.key] = 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over filters and update counters
|
||||
*/
|
||||
updateFilterCounters(): void {
|
||||
this.filters.forEach((filter: TaskFilterCloudModel) => this.updateFilterCounter(filter));
|
||||
this.countersManager.updateAllCounters();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,48 +174,27 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
||||
* @param filter filter
|
||||
*/
|
||||
updateFilterCounter(filter: TaskFilterCloudModel): void {
|
||||
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.searchApiMethod === 'POST'
|
||||
? this.taskListCloudService.getTaskListCount(new TaskFilterCloudAdapter(filter))
|
||||
: this.taskFilterCloudService.getTaskFilterCounter(filter);
|
||||
this.countersManager.updateSingleCounter(filter);
|
||||
this.counters = this.countersManager.counters;
|
||||
}
|
||||
|
||||
initFilterCounterNotifications() {
|
||||
if (!this.appName) {
|
||||
if (!this.appName || !this.enableNotifications) {
|
||||
return;
|
||||
}
|
||||
if (this.enableNotifications) {
|
||||
this.taskFilterCloudService
|
||||
.getTaskNotificationSubscription(this.appName)
|
||||
.pipe(debounceTime(this.notificationDebounceTime), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((result) => {
|
||||
result.forEach((taskEvent) => {
|
||||
this.checkFilterCounter(taskEvent.entity);
|
||||
});
|
||||
|
||||
this.updateFilterCounters();
|
||||
this.filterCounterUpdated.emit(result);
|
||||
this.filterCountersCloudService
|
||||
.getFilterCountersNotifications(this.appName)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(({ events }) => {
|
||||
events.forEach((taskEvent) => {
|
||||
this.checkFilterCounter(taskEvent.entity);
|
||||
});
|
||||
} else {
|
||||
this.counters = {};
|
||||
}
|
||||
|
||||
this.filterCounterUpdated.emit(events);
|
||||
});
|
||||
|
||||
this.countersManager.subscribeToNotifications(this.appName);
|
||||
}
|
||||
|
||||
checkFilterCounter(filterNotification: TaskDetailsCloudModel) {
|
||||
@@ -224,6 +235,7 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
||||
this.selectFilter(newParamFilter);
|
||||
|
||||
if (this.currentFilter) {
|
||||
this.countersManager.resetFilterUpdate(this.currentFilter.key);
|
||||
this.resetFilterCounter(this.currentFilter.key);
|
||||
this.filterSelected.emit(this.currentFilter);
|
||||
}
|
||||
@@ -242,6 +254,7 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
||||
this.selectFilter(filter);
|
||||
this.updateFilterCounter(this.currentFilter);
|
||||
this.filterClicked.emit(this.currentFilter);
|
||||
this.countersManager.resetFilterUpdate(filter.key);
|
||||
this.updatedCountersSet.delete(filter.key);
|
||||
} else {
|
||||
this.currentFilter = undefined;
|
||||
@@ -276,20 +289,12 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
||||
}
|
||||
|
||||
checkIfFilterValuesHasBeenUpdated(filterKey: string, filterValue: number) {
|
||||
if (this.currentFiltersValues[filterKey] === undefined || this.currentFiltersValues[filterKey] !== filterValue) {
|
||||
this.currentFiltersValues[filterKey] = filterValue;
|
||||
if (
|
||||
this.countersManager.currentFiltersValues[filterKey] === undefined ||
|
||||
this.countersManager.currentFiltersValues[filterKey] !== filterValue
|
||||
) {
|
||||
this.updatedFilter.emit(filterKey);
|
||||
this.updatedCountersSet.add(filterKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filer key when filter was refreshed by external action
|
||||
*
|
||||
*/
|
||||
getFilterKeysAfterExternalRefreshing(): void {
|
||||
this.taskFilterCloudService.filterKeyToBeRefreshed$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((filterKey: string) => {
|
||||
this.updatedCountersSet.delete(filterKey);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -361,6 +361,12 @@ export class TaskFilterCloudService extends BaseCloudService {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use FilterCountersCloudService.getFilterCountersNotifications instead, which shares a single
|
||||
* subscription with the process filters and resolves the counters with a single request.
|
||||
* @param appName Name of the target app
|
||||
* @returns Task engine events
|
||||
*/
|
||||
getTaskNotificationSubscription(appName: string): Observable<TaskCloudEngineEvent[]> {
|
||||
return this.notificationCloudService
|
||||
.makeGQLQuery(appName, TASK_EVENT_SUBSCRIPTION_QUERY)
|
||||
|
||||
+7
-1
@@ -135,7 +135,13 @@ export class TaskListCloudService extends BaseCloudService implements TaskListCl
|
||||
return this.post<object, number>(queryUrl, queryData).pipe(map((response) => response || 0));
|
||||
}
|
||||
|
||||
protected buildQueryData(requestNode: TaskListRequestModel) {
|
||||
/**
|
||||
* Builds the body of a task query, with the empty properties of the request stripped out.
|
||||
*
|
||||
* @param requestNode Query object
|
||||
* @returns Body of the query
|
||||
*/
|
||||
buildQueryData(requestNode: TaskListRequestModel) {
|
||||
const queryData: any = {
|
||||
id: requestNode.id,
|
||||
parentId: requestNode.parentId,
|
||||
|
||||
@@ -33,6 +33,7 @@ export * from './lib/models/application-version.model';
|
||||
export * from './lib/models/engine-event-cloud.model';
|
||||
export * from './lib/models/task-cloud.model';
|
||||
export * from './lib/models/filter-cloud-model';
|
||||
export * from './lib/models/filter-counters-cloud.model';
|
||||
export * from './lib/models/task-list-sorting.model';
|
||||
export * from './lib/models/process-instance-variable.model';
|
||||
export * from './lib/models/variable-definition';
|
||||
|
||||
Reference in New Issue
Block a user