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 (#12186)
* AAE-49653 Migrating to batch count endpoint * AAE-49653 Updating code with the latest BE contract * AAE-49653 Improving type safety * AAE-49653 Code improvement * AAE-49653 fixed the DI coupling and type safety * AAE-49653 Code improvements * AAE-49653 Adding more unit tests and fixing a subscription * AAE-49653 Updating the comment * AAE-49653 Code improvement * AAE-49653 Removing the non-batched counter fallback * AAE-49653 Making filter key mandatory string * AAE-49653 Removing dead code and improvements * Revert "AAE-49653 Removing dead code and improvements" This reverts commita8fc99b044. * Revert "AAE-49653 Making filter key mandatory string" This reverts commitba916b7c22. * Revert "AAE-49653 Removing the non-batched counter fallback" This reverts commit416a61fb9e. * AAE-49653 Putting implementation under FF * AAE-49653 Adding new input to filter components, moving FF handling to consumer app * AAE-49653 Removing extra comments
This commit is contained in:
@@ -27,6 +27,7 @@ Lists all available process filters and allows to select a filter.
|
|||||||
| appName | `string` | "" | (required) The application name |
|
| appName | `string` | "" | (required) The application name |
|
||||||
| filterParam | `UserTaskFilterRepresentation` | | (optional) The filter to be selected by default |
|
| filterParam | `UserTaskFilterRepresentation` | | (optional) The filter to be selected by default |
|
||||||
| showIcons | `boolean` | false | (optional) Toggles showing an icon by the side of each filter |
|
| showIcons | `boolean` | false | (optional) Toggles showing an icon by the side of each filter |
|
||||||
|
| useBatchedCounters | `boolean` | false | Get all the filter counters with one call to `POST /query/v1/count` (needs Activiti 8.7.0). Turn it on for both filter components. |
|
||||||
|
|
||||||
### Events
|
### Events
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ Shows all available filters.
|
|||||||
| appName | `string` | "" | Display filters available to the current user for the application with the specified name. |
|
| appName | `string` | "" | Display filters available to the current user for the application with the specified name. |
|
||||||
| filterParam | `FilterParamsModel` | | Parameters to use for the task filter cloud. If there is no match then the default filter (the first one in the list) is selected. |
|
| filterParam | `FilterParamsModel` | | Parameters to use for the task filter cloud. If there is no match then the default filter (the first one in the list) is selected. |
|
||||||
| showIcons | `boolean` | false | Toggles display of the filter's icons. |
|
| showIcons | `boolean` | false | Toggles display of the filter's icons. |
|
||||||
|
| useBatchedCounters | `boolean` | false | Get all the filter counters with one call to `POST /query/v1/count` (needs Activiti 8.7.0). Turn it on for both filter components. |
|
||||||
|
|
||||||
### Events
|
### Events
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/*!
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterCountersQuery {
|
||||||
|
requestId: string;
|
||||||
|
status?: string[];
|
||||||
|
assignee?: string[];
|
||||||
|
sort?: FilterCountersQuerySort;
|
||||||
|
[criteria: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FilterCountersRequest = {
|
||||||
|
[entityType in FilterCounterEntityType]?: FilterCountersQuery[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface FilterCounterCandidate {
|
||||||
|
key?: string | null;
|
||||||
|
showCounter?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FilterCounters = {
|
||||||
|
[entityType in FilterCounterEntityType]?: { [requestId: string]: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface FilterCountersResult {
|
||||||
|
counters: { [filterKey: string]: number };
|
||||||
|
batched: boolean;
|
||||||
|
}
|
||||||
+523
-40
@@ -16,15 +16,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, SimpleChange } from '@angular/core';
|
import { Component, SimpleChange } from '@angular/core';
|
||||||
import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing';
|
||||||
import { first, of, Subject, throwError } from 'rxjs';
|
import { first, of, Subject, throwError } from 'rxjs';
|
||||||
import { ProcessFilterCloudService } from '../../services/process-filter-cloud.service';
|
import { ProcessFilterCloudService } from '../../services/process-filter-cloud.service';
|
||||||
import { ProcessFiltersCloudComponent } from './process-filters-cloud.component';
|
import { ProcessFiltersCloudComponent } from './process-filters-cloud.component';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { PROCESS_FILTERS_SERVICE_TOKEN } from '../../../../services/cloud-token.service';
|
import { PROCESS_FILTERS_SERVICE_TOKEN, TASK_FILTERS_SERVICE_TOKEN } from '../../../../services/cloud-token.service';
|
||||||
import { LocalPreferenceCloudService } from '../../../../services/local-preference-cloud.service';
|
import { LocalPreferenceCloudService } from '../../../../services/local-preference-cloud.service';
|
||||||
import { mockProcessFilters } from '../../mock/process-filters-cloud.mock';
|
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 { ProcessListCloudService } from '../../../process-list/services/process-list-cloud.service';
|
||||||
import { ApolloTestingModule } from 'apollo-angular/testing';
|
import { ApolloTestingModule } from 'apollo-angular/testing';
|
||||||
import { HarnessLoader } from '@angular/cdk/testing';
|
import { HarnessLoader } from '@angular/cdk/testing';
|
||||||
@@ -32,39 +32,39 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
|||||||
import { MatIconHarness } from '@angular/material/icon/testing';
|
import { MatIconHarness } from '@angular/material/icon/testing';
|
||||||
import { ActivatedRoute, provideRouter, Router } from '@angular/router';
|
import { ActivatedRoute, provideRouter, Router } from '@angular/router';
|
||||||
import { RouterTestingHarness } from '@angular/router/testing';
|
import { RouterTestingHarness } from '@angular/router/testing';
|
||||||
import { TaskCloudEngineEvent } from '../../../../models/engine-event-cloud.model';
|
import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service';
|
||||||
|
import { FilterCounterEntityType, FilterCountersResult } from '../../../../models/filter-counters-cloud.model';
|
||||||
|
import { ProcessFilterCloudModel } from '../../models/process-filter-cloud.model';
|
||||||
|
|
||||||
@Component({ selector: 'adf-cloud-dummy', template: '' })
|
@Component({ selector: 'adf-cloud-dummy', template: '' })
|
||||||
class DummyComponent {}
|
class DummyComponent {}
|
||||||
|
|
||||||
const ProcessFilterCloudServiceMock = {
|
const ProcessFilterCloudServiceMock = {
|
||||||
getProcessFilters: () => of(mockProcessFilters),
|
getProcessFilters: () => of(mockProcessFilters),
|
||||||
getProcessNotificationSubscription: () => of([]),
|
|
||||||
filterKeyToBeRefreshed$: of(mockProcessFilters[0].key)
|
filterKeyToBeRefreshed$: of(mockProcessFilters[0].key)
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('ProcessFiltersCloudComponent', () => {
|
describe('ProcessFiltersCloudComponent', () => {
|
||||||
let processFilterService: ProcessFilterCloudService;
|
let processFilterService: ProcessFilterCloudService;
|
||||||
|
let filterCountersService: FilterCountersCloudService;
|
||||||
|
let processListService: ProcessListCloudService;
|
||||||
let component: ProcessFiltersCloudComponent;
|
let component: ProcessFiltersCloudComponent;
|
||||||
let fixture: ComponentFixture<ProcessFiltersCloudComponent>;
|
let fixture: ComponentFixture<ProcessFiltersCloudComponent>;
|
||||||
let getProcessFiltersSpy: jasmine.Spy;
|
let getProcessFiltersSpy: jasmine.Spy;
|
||||||
let getProcessNotificationSubscriptionSpy: jasmine.Spy;
|
let getFilterCountersSpy: jasmine.Spy;
|
||||||
|
let refreshFilterCountersSpy: jasmine.Spy;
|
||||||
|
let getProcessCounterSpy: jasmine.Spy;
|
||||||
let loader: HarnessLoader;
|
let loader: HarnessLoader;
|
||||||
let router: Router;
|
let router: Router;
|
||||||
|
|
||||||
const configureTestingModule = async (searchApiMethod: 'GET' | 'POST') => {
|
const configureTestingModule = async (searchApiMethod: 'GET' | 'POST') => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [ProcessFiltersCloudComponent, ApolloTestingModule],
|
imports: [NoopAuthModule, ProcessFiltersCloudComponent, ApolloTestingModule],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService },
|
{ provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService },
|
||||||
|
{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService },
|
||||||
{ provide: AppConfigService, useClass: AppConfigServiceMock },
|
{ provide: AppConfigService, useClass: AppConfigServiceMock },
|
||||||
{
|
ProcessListCloudService,
|
||||||
provide: ProcessListCloudService,
|
|
||||||
useValue: {
|
|
||||||
getProcessCounter: () => of(10),
|
|
||||||
getProcessListCount: () => of(10)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ provide: ProcessFilterCloudService, useValue: ProcessFilterCloudServiceMock },
|
{ provide: ProcessFilterCloudService, useValue: ProcessFilterCloudServiceMock },
|
||||||
provideRouter([{ path: 'process-list-cloud', component: DummyComponent }]),
|
provideRouter([{ path: 'process-list-cloud', component: DummyComponent }]),
|
||||||
{
|
{
|
||||||
@@ -88,11 +88,16 @@ describe('ProcessFiltersCloudComponent', () => {
|
|||||||
component.searchApiMethod = searchApiMethod;
|
component.searchApiMethod = searchApiMethod;
|
||||||
|
|
||||||
processFilterService = TestBed.inject(ProcessFilterCloudService);
|
processFilterService = TestBed.inject(ProcessFilterCloudService);
|
||||||
|
filterCountersService = TestBed.inject(FilterCountersCloudService);
|
||||||
|
processListService = TestBed.inject(ProcessListCloudService);
|
||||||
TestBed.inject(ActivatedRoute);
|
TestBed.inject(ActivatedRoute);
|
||||||
router = TestBed.inject(Router);
|
router = TestBed.inject(Router);
|
||||||
await RouterTestingHarness.create();
|
await RouterTestingHarness.create();
|
||||||
getProcessFiltersSpy = spyOn(processFilterService, 'getProcessFilters').and.returnValue(of(mockProcessFilters));
|
getProcessFiltersSpy = spyOn(filterCountersService, 'getProcessFilters').and.returnValue(of(mockProcessFilters));
|
||||||
getProcessNotificationSubscriptionSpy = spyOn(processFilterService, 'getProcessNotificationSubscription').and.returnValue(of([]));
|
getFilterCountersSpy = spyOn(filterCountersService, 'getFilterCounters').and.returnValue(of({ counters: {}, batched: true }));
|
||||||
|
refreshFilterCountersSpy = spyOn(filterCountersService, 'refreshFilterCounters');
|
||||||
|
getProcessCounterSpy = spyOn(processListService, 'getProcessCounter').and.returnValue(of(10));
|
||||||
|
spyOn(processListService, 'getProcessListCount').and.returnValue(of(10));
|
||||||
};
|
};
|
||||||
|
|
||||||
const bindAppName = async (appName = 'my-app-1') => {
|
const bindAppName = async (appName = 'my-app-1') => {
|
||||||
@@ -463,17 +468,98 @@ describe('ProcessFiltersCloudComponent', () => {
|
|||||||
expect(component.updatedFiltersSet.has(filterKeyTest)).toBeFalsy();
|
expect(component.updatedFiltersSet.has(filterKeyTest)).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call fetchProcessFilterCounter only if filter.showCounter is true', () => {
|
it('should resolve the counter only of the filters with a counter enabled', () => {
|
||||||
const filterWithCounter = { ...mockProcessFilters[0], showCounter: true };
|
const filterWithCounter = new ProcessFilterCloudModel({ ...mockProcessFilters[1], showCounter: true });
|
||||||
const filterWithoutCounter = { ...mockProcessFilters[1], showCounter: false };
|
const filterWithoutCounter = new ProcessFilterCloudModel({ ...mockProcessFilters[2], showCounter: false });
|
||||||
const fetchSpy = spyOn<any>(component, 'fetchProcessFilterCounter').and.returnValue(of(42));
|
getProcessCounterSpy.calls.reset();
|
||||||
|
|
||||||
component.filters = [filterWithCounter, filterWithoutCounter];
|
component.filters = [filterWithCounter, filterWithoutCounter];
|
||||||
component.updateFilterCounters();
|
component.updateFilterCounters();
|
||||||
|
|
||||||
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
expect(getProcessCounterSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(fetchSpy).toHaveBeenCalledWith(filterWithCounter);
|
expect(getProcessCounterSpy).toHaveBeenCalledWith(filterWithCounter.appName, filterWithCounter.status);
|
||||||
expect(fetchSpy).not.toHaveBeenCalledWith(filterWithoutCounter);
|
});
|
||||||
|
|
||||||
|
describe('Batched counters', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
getProcessFiltersSpy.and.returnValue(
|
||||||
|
of(mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true })))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read the counters of the process filters of the bound app', async () => {
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not ask for the batched count endpoint by default', async () => {
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
expect(component.useBatchedCounters).toBeFalse();
|
||||||
|
expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ask for the batched count endpoint when the input is set', async () => {
|
||||||
|
fixture.componentRef.setInput('useBatchedCounters', true);
|
||||||
|
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hold the counters resolved by the batched count request', async () => {
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true }));
|
||||||
|
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
expect(component.counters['FakeRunningProcesses']).toBe(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit the filters whose counter changed', async () => {
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true }));
|
||||||
|
const updatedFilterSpy = spyOn(component.updatedFilter, 'emit');
|
||||||
|
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
expect(updatedFilterSpy).toHaveBeenCalledWith('FakeRunningProcesses');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the counters one filter at a time when the batched endpoint is not available', async () => {
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: false }));
|
||||||
|
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
expect(getProcessCounterSpy).toHaveBeenCalledTimes(3);
|
||||||
|
expect(component.counters['FakeRunningProcesses']).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the counters of the filters the batch left out on their own', async () => {
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true }));
|
||||||
|
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
expect(component.counters['FakeRunningProcesses']).toBe(9);
|
||||||
|
expect(getProcessCounterSpy.calls.allArgs().map(([, status]) => status)).toEqual([null, 'COMPLETED']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep the counters of the other filters when one counter cannot be resolved', async () => {
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true }));
|
||||||
|
getProcessCounterSpy.and.throwError('the query of the filter cannot be built');
|
||||||
|
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
expect(component.counters['FakeRunningProcesses']).toBe(9);
|
||||||
|
expect(component.counters['completed-processes']).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should refresh the counters of every filter when a filter is clicked', async () => {
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
component.onFilterClick(mockProcessFilters[1]);
|
||||||
|
|
||||||
|
expect(refreshFilterCountersSpy).toHaveBeenCalledWith('mock-app-name');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Notifications config', () => {
|
describe('Notifications config', () => {
|
||||||
@@ -507,39 +593,436 @@ describe('ProcessFiltersCloudComponent', () => {
|
|||||||
expect(component.notificationDebounceTime).toBe(5000);
|
expect(component.notificationDebounceTime).toBe(5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should debounce notification subscription using the configured debounce time', fakeAsync(() => {
|
it('should keep the counters in sync with the counters stream', fakeAsync(() => {
|
||||||
const notifications$ = new Subject<TaskCloudEngineEvent[]>();
|
const counters$ = new Subject<FilterCountersResult>();
|
||||||
getProcessNotificationSubscriptionSpy.and.returnValue(notifications$.asObservable());
|
getFilterCountersSpy.and.returnValue(counters$.asObservable());
|
||||||
component.appName = 'mock-app-name';
|
component.appName = 'mock-app-name';
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
component.filters = mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true }));
|
||||||
|
|
||||||
const updateFilterCountersSpy = spyOn(component, 'updateFilterCounters');
|
counters$.next({ counters: { FakeRunningProcesses: 7 }, batched: true });
|
||||||
|
|
||||||
notifications$.next([]);
|
|
||||||
tick(1000);
|
|
||||||
expect(updateFilterCountersSpy).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
tick(2000);
|
|
||||||
expect(updateFilterCountersSpy).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
|
expect(component.counters['FakeRunningProcesses']).toBe(7);
|
||||||
flush();
|
flush();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Highlight Selected Filter', () => {
|
describe('Highlight Selected Filter', () => {
|
||||||
it('should make subscription', async () => {
|
const allProcessesFilterKey = mockProcessFilters[0].key;
|
||||||
|
const allProcessesFilterId = mockProcessFilters[0].id;
|
||||||
|
|
||||||
|
it('should apply active CSS class on filter click', async () => {
|
||||||
component.enableNotifications = true;
|
component.enableNotifications = true;
|
||||||
await bindAppName('mock-app-name');
|
await bindAppName('mock-app-name');
|
||||||
expect(getProcessNotificationSubscriptionSpy).toHaveBeenCalled();
|
|
||||||
|
let link = fixture.debugElement.query(By.css(`[data-automation-id="${allProcessesFilterKey}_filter"]`)).nativeElement;
|
||||||
|
expect(link.getAttribute('href')).toBe(`/process-list-cloud?filterId=${allProcessesFilterId}`);
|
||||||
|
|
||||||
|
link.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
expect(router.url).toBe(`/process-list-cloud?filterId=${allProcessesFilterId}`);
|
||||||
|
|
||||||
|
link = fixture.debugElement.query(By.css(`[data-automation-id="${allProcessesFilterKey}_filter"]`)).nativeElement;
|
||||||
|
expect(link.classList).toContain('adf-active');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not make subscription when notifications are disabled', async () => {
|
it('should add aria-current attribute with value "page" to the active filter', async () => {
|
||||||
const appConfigService = TestBed.inject(AppConfigService);
|
component.enableNotifications = true;
|
||||||
spyOn(appConfigService, 'get').and.callFake((key: string, defaultValue: any) => (key === 'notifications' ? false : defaultValue));
|
|
||||||
await bindAppName('mock-app-name');
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
expect(getProcessNotificationSubscriptionSpy).not.toHaveBeenCalled();
|
const link = fixture.debugElement.query(By.css(`[data-automation-id="${allProcessesFilterKey}_filter"]`)).nativeElement;
|
||||||
|
expect(link.getAttribute('aria-current')).toBe('page');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not have aria-current attribute when filter is not active', async () => {
|
||||||
|
component.enableNotifications = true;
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
const link = fixture.debugElement.query(By.css(`[data-automation-id="${mockProcessFilters[1].key}_filter"]`)).nativeElement;
|
||||||
|
expect(link.getAttribute('aria-current')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('searchApiMethod set to POST', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await configureTestingModule('POST');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should attach specific icon for each filter if hasIcon is true', async () => {
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
|
component.showIcons = true;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
|
||||||
|
expect(component.filters.length).toBe(3);
|
||||||
|
const filterIcons = await loader.getAllHarnesses(MatIconHarness.with({ selector: '[data-automation-id="adf-filter-icon"]' }));
|
||||||
|
expect(filterIcons.length).toBe(3);
|
||||||
|
expect(await filterIcons[0].getName()).toContain('adjust');
|
||||||
|
expect(await filterIcons[1].getName()).toContain('inbox');
|
||||||
|
expect(await filterIcons[2].getName()).toContain('done');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not attach icons for each filter if hasIcon is false', async () => {
|
||||||
|
component.showIcons = false;
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
|
const filterIcons = await loader.getAllHarnesses(MatIconHarness.with({ selector: '[data-automation-id="adf-filter-icon"]' }));
|
||||||
|
expect(filterIcons.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the filters', async () => {
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
|
component.showIcons = true;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
|
||||||
|
const filters = fixture.debugElement.queryAll(By.css('.adf-process-filters__entry'));
|
||||||
|
expect(component.filters.length).toBe(3);
|
||||||
|
expect(filters.length).toBe(3);
|
||||||
|
expect(filters[0].nativeElement.innerText).toContain('FakeAllProcesses');
|
||||||
|
expect(filters[1].nativeElement.innerText).toContain('FakeRunningProcesses');
|
||||||
|
expect(filters[2].nativeElement.innerText).toContain('FakeCompletedProcesses');
|
||||||
|
expect(Object.keys(component.counters).length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit success with the filters when filters are loaded', async () => {
|
||||||
|
const successSpy = spyOn(component.success, 'emit');
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
|
expect(successSpy).toHaveBeenCalledWith(mockProcessFilters);
|
||||||
|
expect(component.filters).toBeDefined();
|
||||||
|
expect(component.filters[0].name).toEqual('FakeAllProcesses');
|
||||||
|
expect(component.filters[1].name).toEqual('FakeRunningProcesses');
|
||||||
|
expect(component.filters[2].name).toEqual('FakeCompletedProcesses');
|
||||||
|
expect(Object.keys(component.counters).length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not select any filter as default', async () => {
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
|
expect(component.currentFilter).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filterClicked emit when a filter is clicked from the UI', async () => {
|
||||||
|
const filterClickedSpy = spyOn(component.filterClicked, 'emit');
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
|
const filterButton = fixture.debugElement.nativeElement.querySelector(`[data-automation-id="${mockProcessFilters[0].key}_filter"]`);
|
||||||
|
filterButton.click();
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
|
||||||
|
expect(component.currentFilter).toEqual(mockProcessFilters[0]);
|
||||||
|
expect(filterClickedSpy).toHaveBeenCalledWith(mockProcessFilters[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('API agnostic', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await configureTestingModule('GET');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit an error with a bad response', async () => {
|
||||||
|
getProcessFiltersSpy.and.returnValue(throwError('wrong request'));
|
||||||
|
let lastValue: any;
|
||||||
|
component.error.subscribe((err) => (lastValue = err));
|
||||||
|
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
|
expect(lastValue).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not select any process filter if filter input does not exist', async () => {
|
||||||
|
const change = new SimpleChange(null, { name: 'nonexistentFilter' }, true);
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
component.ngOnChanges({ filterParam: change });
|
||||||
|
|
||||||
|
expect(component.currentFilter).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select the filter based on the input by name param', async () => {
|
||||||
|
const filterSelectedSpy = spyOn(component.filterSelected, 'emit');
|
||||||
|
const change = new SimpleChange(null, { name: 'FakeRunningProcesses' }, true);
|
||||||
|
|
||||||
|
await bindAppName();
|
||||||
|
component.ngOnChanges({ filterParam: change });
|
||||||
|
|
||||||
|
expect(component.currentFilter).toEqual(mockProcessFilters[1]);
|
||||||
|
expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select the filter based on the input by key param', async () => {
|
||||||
|
const filterSelectedSpy = spyOn(component.filterSelected, 'emit');
|
||||||
|
const change = new SimpleChange(null, { key: 'completed-processes' }, true);
|
||||||
|
|
||||||
|
await bindAppName();
|
||||||
|
component.ngOnChanges({ filterParam: change });
|
||||||
|
|
||||||
|
expect(component.currentFilter).toEqual(mockProcessFilters[2]);
|
||||||
|
expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select the filter based on the input by index param', async () => {
|
||||||
|
const filterSelectedSpy = spyOn(component.filterSelected, 'emit');
|
||||||
|
const change = new SimpleChange(null, { index: 2 }, true);
|
||||||
|
|
||||||
|
await bindAppName();
|
||||||
|
component.ngOnChanges({ filterParam: change });
|
||||||
|
|
||||||
|
expect(component.currentFilter).toEqual(mockProcessFilters[2]);
|
||||||
|
expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select the filter based on the input by id param', async () => {
|
||||||
|
const filterSelectedSpy = spyOn(component.filterSelected, 'emit');
|
||||||
|
const change = new SimpleChange(null, { id: '12' }, true);
|
||||||
|
|
||||||
|
await bindAppName();
|
||||||
|
component.ngOnChanges({ filterParam: change });
|
||||||
|
|
||||||
|
expect(component.currentFilter).toEqual(mockProcessFilters[2]);
|
||||||
|
expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset the filter when the param is undefined', () => {
|
||||||
|
const change = new SimpleChange(mockProcessFilters[0], undefined, false);
|
||||||
|
component.currentFilter = mockProcessFilters[0];
|
||||||
|
component.ngOnChanges({ filterParam: change });
|
||||||
|
|
||||||
|
expect(component.currentFilter).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not emit a filter clicked event when a filter is selected through the filterParam input (filterClicked emits only through a UI click action)', async () => {
|
||||||
|
const filterClickedSpy = spyOn(component.filterClicked, 'emit');
|
||||||
|
const change = new SimpleChange(null, { id: '10' }, true);
|
||||||
|
|
||||||
|
await bindAppName();
|
||||||
|
component.ngOnChanges({ filterParam: change });
|
||||||
|
|
||||||
|
expect(component.currentFilter).toBe(mockProcessFilters[0]);
|
||||||
|
expect(filterClickedSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reload filters by appName on binding changes', () => {
|
||||||
|
spyOn(component, 'getFilters').and.stub();
|
||||||
|
const appName = 'my-app-1';
|
||||||
|
|
||||||
|
const change = new SimpleChange(null, appName, true);
|
||||||
|
component.ngOnChanges({ appName: change });
|
||||||
|
|
||||||
|
expect(component.getFilters).toHaveBeenCalledWith(appName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not reload filters by appName null on binding changes', () => {
|
||||||
|
spyOn(component, 'getFilters').and.stub();
|
||||||
|
const appName = null;
|
||||||
|
|
||||||
|
const change = new SimpleChange(undefined, appName, true);
|
||||||
|
component.ngOnChanges({ appName: change });
|
||||||
|
|
||||||
|
expect(component.getFilters).not.toHaveBeenCalledWith(appName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reload filters by app name on binding changes', () => {
|
||||||
|
spyOn(component, 'getFilters').and.stub();
|
||||||
|
const appName = 'fake-app-name';
|
||||||
|
|
||||||
|
const change = new SimpleChange(null, appName, true);
|
||||||
|
component.ngOnChanges({ appName: change });
|
||||||
|
|
||||||
|
expect(component.getFilters).toHaveBeenCalledWith(appName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the current filter after one is selected', () => {
|
||||||
|
const filter = mockProcessFilters[1];
|
||||||
|
component.filters = mockProcessFilters;
|
||||||
|
|
||||||
|
expect(component.currentFilter).toBeUndefined();
|
||||||
|
component.selectFilter({ id: filter.id });
|
||||||
|
expect(component.getCurrentFilter()).toBe(filter);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove key from set of updated filters when received refreshed filter key', async () => {
|
||||||
|
const filterKeyTest = 'filter-key-test';
|
||||||
|
component.updatedFiltersSet.add(filterKeyTest);
|
||||||
|
|
||||||
|
expect(component.updatedFiltersSet.size).toBe(1);
|
||||||
|
processFilterService.filterKeyToBeRefreshed$ = of(filterKeyTest);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(component.updatedFiltersSet.has(filterKeyTest)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the counter only of the filters with a counter enabled', () => {
|
||||||
|
const filterWithCounter = new ProcessFilterCloudModel({ ...mockProcessFilters[1], showCounter: true });
|
||||||
|
const filterWithoutCounter = new ProcessFilterCloudModel({ ...mockProcessFilters[2], showCounter: false });
|
||||||
|
getProcessCounterSpy.calls.reset();
|
||||||
|
|
||||||
|
component.filters = [filterWithCounter, filterWithoutCounter];
|
||||||
|
component.updateFilterCounters();
|
||||||
|
|
||||||
|
expect(getProcessCounterSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(getProcessCounterSpy).toHaveBeenCalledWith(filterWithCounter.appName, filterWithCounter.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Batched counters', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
getProcessFiltersSpy.and.returnValue(
|
||||||
|
of(mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true })))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read the counters of the process filters of the bound app', async () => {
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not ask for the batched count endpoint by default', async () => {
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
expect(component.useBatchedCounters).toBeFalse();
|
||||||
|
expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ask for the batched count endpoint when the input is set', async () => {
|
||||||
|
fixture.componentRef.setInput('useBatchedCounters', true);
|
||||||
|
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hold the counters resolved by the batched count request', async () => {
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true }));
|
||||||
|
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
expect(component.counters['FakeRunningProcesses']).toBe(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit the filters whose counter changed', async () => {
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true }));
|
||||||
|
const updatedFilterSpy = spyOn(component.updatedFilter, 'emit');
|
||||||
|
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
expect(updatedFilterSpy).toHaveBeenCalledWith('FakeRunningProcesses');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the counters one filter at a time when the batched endpoint is not available', async () => {
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: false }));
|
||||||
|
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
expect(getProcessCounterSpy).toHaveBeenCalledTimes(3);
|
||||||
|
expect(component.counters['FakeRunningProcesses']).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the counters of the filters the batch left out on their own', async () => {
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true }));
|
||||||
|
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
expect(component.counters['FakeRunningProcesses']).toBe(9);
|
||||||
|
expect(getProcessCounterSpy.calls.allArgs().map(([, status]) => status)).toEqual([null, 'COMPLETED']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep the counters of the other filters when one counter cannot be resolved', async () => {
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true }));
|
||||||
|
getProcessCounterSpy.and.throwError('the query of the filter cannot be built');
|
||||||
|
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
expect(component.counters['FakeRunningProcesses']).toBe(9);
|
||||||
|
expect(component.counters['completed-processes']).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should refresh the counters of every filter when a filter is clicked', async () => {
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
component.onFilterClick(mockProcessFilters[1]);
|
||||||
|
|
||||||
|
expect(refreshFilterCountersSpy).toHaveBeenCalledWith('mock-app-name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Notifications config', () => {
|
||||||
|
it('should read enableNotifications and notificationDebounceTime from app config on init', () => {
|
||||||
|
const appConfigService = TestBed.inject(AppConfigService);
|
||||||
|
const getSpy = spyOn(appConfigService, 'get').and.callThrough();
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(getSpy).toHaveBeenCalledWith('notifications', true);
|
||||||
|
expect(getSpy).toHaveBeenCalledWith('notificationDebounceTime', 3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should default notificationDebounceTime to 3000 when not set in app config', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(component.notificationDebounceTime).toBe(3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use notificationDebounceTime from app config', () => {
|
||||||
|
const appConfigService: AppConfigService = TestBed.inject(AppConfigService);
|
||||||
|
spyOn(appConfigService, 'get').and.callFake((key: string, defaultValue: any) => {
|
||||||
|
if (key === 'notificationDebounceTime') {
|
||||||
|
return 5000;
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(component.notificationDebounceTime).toBe(5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep the counters in sync with the counters stream', fakeAsync(() => {
|
||||||
|
const counters$ = new Subject<FilterCountersResult>();
|
||||||
|
getFilterCountersSpy.and.returnValue(counters$.asObservable());
|
||||||
|
component.appName = 'mock-app-name';
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
component.filters = mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true }));
|
||||||
|
|
||||||
|
counters$.next({ counters: { FakeRunningProcesses: 7 }, batched: true });
|
||||||
|
|
||||||
|
expect(component.counters['FakeRunningProcesses']).toBe(7);
|
||||||
|
flush();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should resolve the counters one filter at a time when the batched endpoint is not available', fakeAsync(() => {
|
||||||
|
const counters$ = new Subject<FilterCountersResult>();
|
||||||
|
getFilterCountersSpy.and.returnValue(counters$.asObservable());
|
||||||
|
component.appName = 'mock-app-name';
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
component.filters = mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true }));
|
||||||
|
getProcessCounterSpy.calls.reset();
|
||||||
|
|
||||||
|
counters$.next({ counters: {}, batched: false });
|
||||||
|
|
||||||
|
expect(getProcessCounterSpy).toHaveBeenCalledTimes(3);
|
||||||
|
flush();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Highlight Selected Filter', () => {
|
||||||
|
it('should read the counters of the bound app', async () => {
|
||||||
|
component.enableNotifications = true;
|
||||||
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
|
expect(getFilterCountersSpy).toHaveBeenCalledWith('mock-app-name', FilterCounterEntityType.PROCESS_INSTANCE, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit filter key when filter counter is set for first time', () => {
|
it('should emit filter key when filter counter is set for first time', () => {
|
||||||
|
|||||||
+151
-86
@@ -16,14 +16,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, DestroyRef, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
|
import { Component, DestroyRef, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
|
||||||
import { EMPTY, Observable } from 'rxjs';
|
import { combineLatest, defer, EMPTY, Observable, of, Subscription } from 'rxjs';
|
||||||
import { ProcessFilterCloudService } from '../../services/process-filter-cloud.service';
|
import { ProcessFilterCloudService } from '../../services/process-filter-cloud.service';
|
||||||
import { ProcessFilterCloudModel } from '../../models/process-filter-cloud.model';
|
import { ProcessFilterCloudModel } from '../../models/process-filter-cloud.model';
|
||||||
import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core';
|
import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core';
|
||||||
import { FilterParamsModel } from '../../../../task/task-filters/models/filter-cloud.model';
|
import { FilterParamsModel } from '../../../../task/task-filters/models/filter-cloud.model';
|
||||||
import { 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 { ProcessListCloudService } from '../../../process-list/services/process-list-cloud.service';
|
||||||
import { ProcessFilterCloudAdapter } from '../../../process-list/models/process-cloud-query-request.model';
|
import { ProcessFilterCloudAdapter } from '../../../process-list/models/process-cloud-query-request.model';
|
||||||
|
import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service';
|
||||||
|
import { FilterCounterEntityType } from '../../../../models/filter-counters-cloud.model';
|
||||||
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
||||||
import { TranslatePipe } from '@ngx-translate/core';
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
import { AsyncPipe } from '@angular/common';
|
import { AsyncPipe } from '@angular/common';
|
||||||
@@ -43,10 +45,21 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
@Input()
|
@Input()
|
||||||
appName: string = '';
|
appName: string = '';
|
||||||
|
|
||||||
/** (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the process count */
|
/**
|
||||||
|
* (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the process count.
|
||||||
|
*
|
||||||
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
searchApiMethod: 'GET' | 'POST' = 'GET';
|
searchApiMethod: 'GET' | 'POST' = 'GET';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (optional) Resolves the counters of the task and the process filters with a single call to
|
||||||
|
* `POST /query/v1/count`. Both filter components have to
|
||||||
|
* ask for it, otherwise the counters are resolved one filter at a time.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
useBatchedCounters = false;
|
||||||
|
|
||||||
/** (optional) The filter to be selected by default */
|
/** (optional) The filter to be selected by default */
|
||||||
@Input()
|
@Input()
|
||||||
filterParam: FilterParamsModel;
|
filterParam: FilterParamsModel;
|
||||||
@@ -79,27 +92,31 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
currentFilter?: ProcessFilterCloudModel;
|
currentFilter?: ProcessFilterCloudModel;
|
||||||
filters: ProcessFilterCloudModel[] = [];
|
filters: ProcessFilterCloudModel[] = [];
|
||||||
counters: { [key: string]: number } = {};
|
counters: { [key: string]: number } = {};
|
||||||
enableNotifications = true;
|
|
||||||
notificationDebounceTime = 3000;
|
|
||||||
currentFiltersValues: { [key: string]: number } = {};
|
currentFiltersValues: { [key: string]: number } = {};
|
||||||
updatedFiltersSet = new Set<string>();
|
updatedFiltersSet = new Set<string>();
|
||||||
|
enableNotifications = true;
|
||||||
|
notificationDebounceTime = 3000;
|
||||||
private filtersLoadedFor?: string;
|
private filtersLoadedFor?: string;
|
||||||
|
private countersSubscription?: Subscription;
|
||||||
|
private countersFilters$?: Observable<ProcessFilterCloudModel[]>;
|
||||||
|
private batchedCounters = true;
|
||||||
|
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
private readonly processFilterCloudService = inject(ProcessFilterCloudService);
|
private readonly processFilterCloudService = inject(ProcessFilterCloudService);
|
||||||
private readonly translationService = inject(TranslationService);
|
private readonly translationService = inject(TranslationService);
|
||||||
private readonly appConfigService = inject(AppConfigService);
|
private readonly appConfigService = inject(AppConfigService);
|
||||||
private readonly processListCloudService = inject(ProcessListCloudService);
|
private readonly processListCloudService = inject(ProcessListCloudService);
|
||||||
|
private readonly filterCountersCloudService = inject(FilterCountersCloudService);
|
||||||
private readonly activatedRoute = inject(ActivatedRoute);
|
private readonly activatedRoute = inject(ActivatedRoute);
|
||||||
protected readonly currentRouteFilterId = toSignal(this.activatedRoute.queryParamMap.pipe(map((params) => params.get('filterId'))));
|
protected readonly currentRouteFilterId = toSignal(this.activatedRoute.queryParamMap.pipe(map((params) => params.get('filterId'))));
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.enableNotifications = this.appConfigService.get('notifications', true);
|
this.enableNotifications = this.appConfigService.get('notifications', true);
|
||||||
this.notificationDebounceTime = this.appConfigService.get('notificationDebounceTime', 3000);
|
this.notificationDebounceTime = this.appConfigService.get('notificationDebounceTime', 3000);
|
||||||
|
|
||||||
if (!this.filtersLoadedFor) {
|
if (!this.filtersLoadedFor) {
|
||||||
this.getFilters(this.appName);
|
this.getFilters(this.appName);
|
||||||
}
|
}
|
||||||
this.initProcessNotification();
|
|
||||||
this.getFilterKeysAfterExternalRefreshing();
|
this.getFilterKeysAfterExternalRefreshing();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +127,8 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
this.getFilters(appName.currentValue);
|
this.getFilters(appName.currentValue);
|
||||||
} else if (filter && filter.currentValue !== filter.previousValue) {
|
} else if (filter && filter.currentValue !== filter.previousValue) {
|
||||||
this.selectFilterAndEmit(filter.currentValue);
|
this.selectFilterAndEmit(filter.currentValue);
|
||||||
|
} else if (changes['useBatchedCounters'] && !changes['useBatchedCounters'].firstChange && this.filtersLoadedFor) {
|
||||||
|
this.loadFilterCounters(this.filtersLoadedFor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,8 +139,8 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
*/
|
*/
|
||||||
getFilters(appName: string): void {
|
getFilters(appName: string): void {
|
||||||
this.filtersLoadedFor = appName;
|
this.filtersLoadedFor = appName;
|
||||||
const filters$ = this.processFilterCloudService.getProcessFilters(appName).pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
const filters$ = this.filterCountersCloudService.getProcessFilters(appName);
|
||||||
this.filters$ = filters$.pipe(catchError(() => EMPTY));
|
this.filters$ = filters$.pipe(catchError(() => of([])));
|
||||||
|
|
||||||
filters$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
filters$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
@@ -130,19 +149,25 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
this.initFilterCounters();
|
this.initFilterCounters();
|
||||||
this.selectFilterAndEmit(this.filterParam);
|
this.selectFilterAndEmit(this.filterParam);
|
||||||
this.success.emit(res);
|
this.success.emit(res);
|
||||||
this.updateFilterCounters();
|
|
||||||
},
|
},
|
||||||
error: (err: any) => {
|
error: (err: unknown) => {
|
||||||
this.error.emit(err);
|
this.error.emit(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.countersFilters$ = filters$;
|
||||||
|
this.loadFilterCounters(appName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize counter collection for filters
|
* Initialize counter collection for filters
|
||||||
*/
|
*/
|
||||||
initFilterCounters() {
|
initFilterCounters(): void {
|
||||||
this.filters.forEach((filter) => (this.counters[filter.key] = 0));
|
this.filters.forEach((filter) => {
|
||||||
|
if (filter.key) {
|
||||||
|
this.counters[filter.key] = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -167,20 +192,6 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
); // fallback to preserve the previous behavior
|
); // fallback to preserve the previous behavior
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check equality of the filter names by translating the given name strings
|
|
||||||
*
|
|
||||||
* @param name1 source name
|
|
||||||
* @param name2 target name
|
|
||||||
* @returns `true` if filter names are equal, otherwise `false`
|
|
||||||
*/
|
|
||||||
private checkFilterNamesEquality(name1: string, name2: string): boolean {
|
|
||||||
const translatedName1 = this.translationService.instant(name1);
|
|
||||||
const translatedName2 = this.translationService.instant(name2);
|
|
||||||
|
|
||||||
return translatedName1.toLocaleLowerCase() === translatedName2.toLocaleLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects and emits the given filter
|
* Selects and emits the given filter
|
||||||
*
|
*
|
||||||
@@ -213,7 +224,7 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
if (filter) {
|
if (filter) {
|
||||||
this.selectFilter(filter);
|
this.selectFilter(filter);
|
||||||
this.filterClicked.emit(this.currentFilter);
|
this.filterClicked.emit(this.currentFilter);
|
||||||
this.updateFilterCounter(this.currentFilter);
|
this.refreshFilterCounter(this.currentFilter);
|
||||||
this.updatedFiltersSet.delete(filter.key);
|
this.updatedFiltersSet.delete(filter.key);
|
||||||
} else {
|
} else {
|
||||||
this.currentFilter = undefined;
|
this.currentFilter = undefined;
|
||||||
@@ -247,6 +258,83 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
return this.filters === undefined || (this.filters && this.filters.length === 0);
|
return this.filters === undefined || (this.filters && this.filters.length === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isActiveFilter(filter: ProcessFilterCloudModel): boolean {
|
||||||
|
return this.currentFilter.name === filter.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated does nothing: the counters keep themselves in sync. Removed in ADF 10.0.0.
|
||||||
|
*/
|
||||||
|
initProcessNotification(): void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterate over filters and update counters
|
||||||
|
*
|
||||||
|
* @deprecated counts one filter at a time. Removed in ADF 10.0.0.
|
||||||
|
*/
|
||||||
|
updateFilterCounters(): void {
|
||||||
|
this.filters.forEach((filter) => this.updateFilterCounter(filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current value for filter and check if value has changed
|
||||||
|
*
|
||||||
|
* @param filter filter
|
||||||
|
* @deprecated counts one filter at a time. Removed in ADF 10.0.0.
|
||||||
|
*/
|
||||||
|
updateFilterCounter(filter: ProcessFilterCloudModel): void {
|
||||||
|
const filterKey = filter?.showCounter ? filter.key : undefined;
|
||||||
|
if (!filterKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
defer(() => this.fetchProcessFilterCounter(filter))
|
||||||
|
.pipe(
|
||||||
|
catchError(() => EMPTY),
|
||||||
|
takeUntilDestroyed(this.destroyRef)
|
||||||
|
)
|
||||||
|
.subscribe((counter) => {
|
||||||
|
this.checkIfFilterValuesHasBeenUpdated(filterKey, counter);
|
||||||
|
this.counters = { ...this.counters, [filterKey]: counter };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIfFilterValuesHasBeenUpdated(filterKey: string, filterValue: number): void {
|
||||||
|
if (this.currentFiltersValues[filterKey] === undefined || this.currentFiltersValues[filterKey] !== filterValue) {
|
||||||
|
this.currentFiltersValues = { ...this.currentFiltersValues, [filterKey]: filterValue };
|
||||||
|
this.updatedFilter.emit(filterKey);
|
||||||
|
this.updatedFiltersSet.add(filterKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
isFilterUpdated(filterName: string): boolean {
|
||||||
|
return this.updatedFiltersSet.has(filterName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check equality of the filter names by translating the given name strings
|
||||||
|
*
|
||||||
|
* @param name1 source name
|
||||||
|
* @param name2 target name
|
||||||
|
* @returns `true` if filter names are equal, otherwise `false`
|
||||||
|
*/
|
||||||
|
private checkFilterNamesEquality(name1: string, name2: string): boolean {
|
||||||
|
const translatedName1 = this.translationService.instant(name1);
|
||||||
|
const translatedName2 = this.translationService.instant(name2);
|
||||||
|
|
||||||
|
return translatedName1.toLocaleLowerCase() === translatedName2.toLocaleLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the filters
|
* Reset the filters
|
||||||
*/
|
*/
|
||||||
@@ -255,76 +343,53 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
this.currentFilter = undefined;
|
this.currentFilter = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
isActiveFilter(filter: ProcessFilterCloudModel): boolean {
|
private loadFilterCounters(appName: string): void {
|
||||||
return this.currentFilter.name === filter.name;
|
if (!this.countersFilters$) {
|
||||||
}
|
|
||||||
|
|
||||||
initProcessNotification(): void {
|
|
||||||
if (this.appName && this.enableNotifications) {
|
|
||||||
this.processFilterCloudService
|
|
||||||
.getProcessNotificationSubscription(this.appName)
|
|
||||||
.pipe(debounceTime(this.notificationDebounceTime), takeUntilDestroyed(this.destroyRef))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.updateFilterCounters();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterate over filters and update counters
|
|
||||||
*/
|
|
||||||
updateFilterCounters(): void {
|
|
||||||
this.filters.forEach((filter: ProcessFilterCloudModel) => {
|
|
||||||
this.updateFilterCounter(filter);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current value for filter and check if value has changed
|
|
||||||
*
|
|
||||||
* @param filter filter
|
|
||||||
*/
|
|
||||||
updateFilterCounter(filter: ProcessFilterCloudModel): void {
|
|
||||||
if (!filter?.showCounter) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fetchProcessFilterCounter(filter)
|
this.countersSubscription?.unsubscribe();
|
||||||
.pipe(
|
this.countersSubscription = combineLatest([
|
||||||
tap((filterCounter) => {
|
this.countersFilters$.pipe(catchError(() => of([]))),
|
||||||
this.checkIfFilterValuesHasBeenUpdated(filter.key, filterCounter);
|
this.filterCountersCloudService.getFilterCounters(appName, FilterCounterEntityType.PROCESS_INSTANCE, this.useBatchedCounters)
|
||||||
})
|
])
|
||||||
)
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
.subscribe((data) => {
|
.subscribe(([, { counters, batched }]) => {
|
||||||
this.counters = {
|
this.batchedCounters = batched;
|
||||||
...this.counters,
|
if (batched) {
|
||||||
[filter.key]: data
|
this.applyFilterCounters(counters);
|
||||||
};
|
} else {
|
||||||
|
this.updateFilterCounters();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
checkIfFilterValuesHasBeenUpdated(filterKey: string, filterValue: number): void {
|
private applyFilterCounters(counters: { [filterKey: string]: number }): void {
|
||||||
if (this.currentFiltersValues[filterKey] === undefined || this.currentFiltersValues[filterKey] !== filterValue) {
|
this.filters.forEach((filter) => {
|
||||||
this.currentFiltersValues[filterKey] = filterValue;
|
const filterKey = filter?.showCounter ? filter.key : undefined;
|
||||||
this.updatedFilter.emit(filterKey);
|
if (!filterKey) {
|
||||||
this.updatedFiltersSet.add(filterKey);
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
isFilterUpdated(filterName: string): boolean {
|
const counter = counters[filterKey];
|
||||||
return this.updatedFiltersSet.has(filterName);
|
if (counter === undefined) {
|
||||||
}
|
this.updateFilterCounter(filter);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
this.checkIfFilterValuesHasBeenUpdated(filterKey, counter);
|
||||||
* Get filer key when filter was refreshed by external action
|
this.counters = { ...this.counters, [filterKey]: counter };
|
||||||
*
|
|
||||||
*/
|
|
||||||
getFilterKeysAfterExternalRefreshing(): void {
|
|
||||||
this.processFilterCloudService.filterKeyToBeRefreshed$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((filterKey: string) => {
|
|
||||||
this.updatedFiltersSet.delete(filterKey);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private refreshFilterCounter(filter?: ProcessFilterCloudModel): void {
|
||||||
|
if (this.batchedCounters) {
|
||||||
|
this.filterCountersCloudService.refreshFilterCounters(this.appName);
|
||||||
|
} else if (filter) {
|
||||||
|
this.updateFilterCounter(filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fetchProcessFilterCounter(filter: ProcessFilterCloudModel): Observable<number> {
|
private fetchProcessFilterCounter(filter: ProcessFilterCloudModel): Observable<number> {
|
||||||
return this.searchApiMethod === 'POST'
|
return this.searchApiMethod === 'POST'
|
||||||
? this.processListCloudService.getProcessListCount(new ProcessFilterCloudAdapter(filter))
|
? this.processListCloudService.getProcessListCount(new ProcessFilterCloudAdapter(filter))
|
||||||
|
|||||||
+6
@@ -404,6 +404,12 @@ export class ProcessFilterCloudService {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use FilterCountersCloudService.getEngineEvents instead.
|
||||||
|
*
|
||||||
|
* @param appName Name of the target app
|
||||||
|
* @returns Process engine events
|
||||||
|
*/
|
||||||
getProcessNotificationSubscription(appName: string): Observable<TaskCloudEngineEvent[]> {
|
getProcessNotificationSubscription(appName: string): Observable<TaskCloudEngineEvent[]> {
|
||||||
return this.notificationCloudService
|
return this.notificationCloudService
|
||||||
.makeGQLQuery(appName, PROCESS_EVENT_SUBSCRIPTION_QUERY)
|
.makeGQLQuery(appName, PROCESS_EVENT_SUBSCRIPTION_QUERY)
|
||||||
|
|||||||
+1
-1
@@ -100,7 +100,7 @@ export class ProcessListCloudService extends BaseCloudService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected buildQueryData(requestNode: ProcessListRequestModel): { [key: string]: any } {
|
buildQueryData(requestNode: ProcessListRequestModel): { [key: string]: any } {
|
||||||
const queryData: { [key: string]: any } = {
|
const queryData: { [key: string]: any } = {
|
||||||
name: requestNode.name,
|
name: requestNode.name,
|
||||||
id: requestNode.id,
|
id: requestNode.id,
|
||||||
|
|||||||
@@ -0,0 +1,651 @@
|
|||||||
|
/*!
|
||||||
|
* @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 { BehaviorSubject, combineLatest, firstValueFrom, Observable, of, Subject, throwError } from 'rxjs';
|
||||||
|
import { ApolloTestingModule } from 'apollo-angular/testing';
|
||||||
|
import { FilterCountersCloudService } from './filter-counters-cloud.service';
|
||||||
|
import { NotificationCloudService } from './notification-cloud.service';
|
||||||
|
import { LocalPreferenceCloudService } from './local-preference-cloud.service';
|
||||||
|
import { PROCESS_FILTERS_SERVICE_TOKEN, TASK_FILTERS_SERVICE_TOKEN } from './cloud-token.service';
|
||||||
|
import { TaskFilterCloudService } from '../task/task-filters/services/task-filter-cloud.service';
|
||||||
|
import { ProcessFilterCloudService } from '../process/process-filters/services/process-filter-cloud.service';
|
||||||
|
import { TaskFilterCloudModel } from '../task/task-filters/models/filter-cloud.model';
|
||||||
|
import { ProcessFilterCloudModel } from '../process/process-filters/models/process-filter-cloud.model';
|
||||||
|
import {
|
||||||
|
FilterCounterEntityType,
|
||||||
|
FilterCounters,
|
||||||
|
FilterCountersQuery,
|
||||||
|
FilterCountersRequest,
|
||||||
|
FilterCountersResult
|
||||||
|
} from '../models/filter-counters-cloud.model';
|
||||||
|
import { TaskCloudEngineEvent } from '../models/engine-event-cloud.model';
|
||||||
|
import { FetchResult } from '@apollo/client/core';
|
||||||
|
|
||||||
|
type EngineEventsResult = FetchResult<{ engineEvents?: TaskCloudEngineEvent[] }>;
|
||||||
|
|
||||||
|
interface CountEndpoint {
|
||||||
|
post: (url: string, request: FilterCountersRequest) => Observable<FilterCounters>;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('FilterCountersCloudService', () => {
|
||||||
|
let service: FilterCountersCloudService;
|
||||||
|
let notificationCloudService: NotificationCloudService;
|
||||||
|
let appConfigService: AppConfigService;
|
||||||
|
let taskEvents$: Subject<EngineEventsResult>;
|
||||||
|
let processEvents$: Subject<EngineEventsResult>;
|
||||||
|
let makeGQLQuerySpy: jasmine.Spy;
|
||||||
|
let postSpy: jasmine.Spy;
|
||||||
|
let getTaskListFiltersSpy: jasmine.Spy;
|
||||||
|
let getProcessFiltersSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
const countRequest = (): FilterCountersRequest => postSpy.calls.mostRecent().args[1];
|
||||||
|
const countUrl = (): string => postSpy.calls.mostRecent().args[0];
|
||||||
|
const countQueries = (entityType: FilterCounterEntityType): FilterCountersQuery[] => countRequest()[entityType] ?? [];
|
||||||
|
const countRequestIds = (entityType: FilterCounterEntityType): string[] => countQueries(entityType).map((query) => query.requestId);
|
||||||
|
|
||||||
|
const countersMock: FilterCounters = {
|
||||||
|
TASK: { 'my-tasks': 5, 'queued-tasks': 0 },
|
||||||
|
PROCESS_INSTANCE: { 'running-processes': 5 }
|
||||||
|
};
|
||||||
|
|
||||||
|
const taskFilter = (filter: Partial<TaskFilterCloudModel>) =>
|
||||||
|
new TaskFilterCloudModel({ appName: 'mock-app', sort: 'createdDate', order: 'DESC', ...filter });
|
||||||
|
const processFilter = (filter: Partial<ProcessFilterCloudModel>) =>
|
||||||
|
new ProcessFilterCloudModel({ appName: 'mock-app', sort: 'startDate', order: 'DESC', ...filter });
|
||||||
|
|
||||||
|
const taskFiltersMock = [
|
||||||
|
taskFilter({ key: 'my-tasks', status: 'ASSIGNED', assignee: 'mock-user', showCounter: true }),
|
||||||
|
taskFilter({ key: 'queued-tasks', status: 'CREATED', showCounter: true }),
|
||||||
|
taskFilter({ key: 'completed-tasks', status: 'COMPLETED', showCounter: false })
|
||||||
|
];
|
||||||
|
const processFiltersMock = [
|
||||||
|
processFilter({ key: 'running-processes', status: 'RUNNING', showCounter: true }),
|
||||||
|
processFilter({ key: 'all-processes', status: '', showCounter: false })
|
||||||
|
];
|
||||||
|
|
||||||
|
const engineEvents = (eventType: string): EngineEventsResult => ({
|
||||||
|
data: { engineEvents: [{ eventType, entity: {} } as TaskCloudEngineEvent] }
|
||||||
|
});
|
||||||
|
const emitTaskEvent = (eventType = 'TASK_CREATED') => taskEvents$.next(engineEvents(eventType));
|
||||||
|
const emitProcessEvent = (eventType = 'PROCESS_STARTED') => processEvents$.next(engineEvents(eventType));
|
||||||
|
|
||||||
|
const counters = (entityType: FilterCounterEntityType, appName = 'mock-app') =>
|
||||||
|
firstValueFrom(service.getFilterCounters(appName, entityType, true));
|
||||||
|
const taskCounters = (appName = 'mock-app') => counters(FilterCounterEntityType.TASK, appName);
|
||||||
|
const processCounters = (appName = 'mock-app') => counters(FilterCounterEntityType.PROCESS_INSTANCE, appName);
|
||||||
|
const bothCounters = (appName = 'mock-app') =>
|
||||||
|
firstValueFrom(
|
||||||
|
combineLatest([
|
||||||
|
service.getFilterCounters(appName, FilterCounterEntityType.TASK, true),
|
||||||
|
service.getFilterCounters(appName, FilterCounterEntityType.PROCESS_INSTANCE, true)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [NoopAuthModule, ApolloTestingModule],
|
||||||
|
providers: [
|
||||||
|
{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService },
|
||||||
|
{ provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(FilterCountersCloudService);
|
||||||
|
notificationCloudService = TestBed.inject(NotificationCloudService);
|
||||||
|
appConfigService = TestBed.inject(AppConfigService);
|
||||||
|
appConfigService.config.bpmHost = 'https://fake-bpm-host.com';
|
||||||
|
|
||||||
|
taskEvents$ = new Subject<EngineEventsResult>();
|
||||||
|
processEvents$ = new Subject<EngineEventsResult>();
|
||||||
|
makeGQLQuerySpy = spyOn(notificationCloudService, 'makeGQLQuery');
|
||||||
|
makeGQLQuerySpy.and.callFake((_appName: string, query: string) =>
|
||||||
|
(query.includes('TASK_CREATED') ? taskEvents$ : processEvents$).asObservable()
|
||||||
|
);
|
||||||
|
postSpy = spyOn(service as unknown as CountEndpoint, 'post').and.returnValue(of(countersMock));
|
||||||
|
getTaskListFiltersSpy = spyOn(TestBed.inject(TaskFilterCloudService), 'getTaskListFilters').and.returnValue(of(taskFiltersMock));
|
||||||
|
getProcessFiltersSpy = spyOn(TestBed.inject(ProcessFilterCloudService), 'getProcessFilters').and.returnValue(of(processFiltersMock));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTaskFilters / getProcessFilters', () => {
|
||||||
|
it('should load the filters of every entity type', async () => {
|
||||||
|
expect(await firstValueFrom(service.getTaskFilters('mock-app'))).toEqual(taskFiltersMock);
|
||||||
|
expect(await firstValueFrom(service.getProcessFilters('mock-app'))).toEqual(processFiltersMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load the filters of an app once for concurrent subscribers', async () => {
|
||||||
|
await firstValueFrom(combineLatest([service.getTaskFilters('mock-app'), service.getTaskFilters('mock-app')]));
|
||||||
|
await firstValueFrom(combineLatest([service.getProcessFilters('mock-app'), service.getProcessFilters('mock-app')]));
|
||||||
|
|
||||||
|
expect(getTaskListFiltersSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(getProcessFiltersSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load the filters of every app', async () => {
|
||||||
|
await firstValueFrom(service.getTaskFilters('mock-app'));
|
||||||
|
await firstValueFrom(service.getTaskFilters('other-app'));
|
||||||
|
|
||||||
|
expect(getTaskListFiltersSpy.calls.allArgs()).toEqual([['mock-app'], ['other-app']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should share the filters with the batched count request', async () => {
|
||||||
|
const subscription = service.getTaskFilters('mock-app').subscribe();
|
||||||
|
await taskCounters();
|
||||||
|
subscription.unsubscribe();
|
||||||
|
|
||||||
|
expect(getTaskListFiltersSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should propagate the error of the filters that fail to load', async () => {
|
||||||
|
getTaskListFiltersSpy.and.returnValue(throwError(() => new Error('filters failed')));
|
||||||
|
|
||||||
|
await expectAsync(firstValueFrom(service.getTaskFilters('mock-app'))).toBeRejectedWithError('filters failed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFilterCounters', () => {
|
||||||
|
it('should return EMPTY when appName is not set', () => {
|
||||||
|
let completed = false;
|
||||||
|
service.getFilterCounters('', FilterCounterEntityType.TASK).subscribe({ complete: () => (completed = true) });
|
||||||
|
|
||||||
|
expect(completed).toBeTrue();
|
||||||
|
expect(postSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the counters of both entity types with a single request', async () => {
|
||||||
|
expect(await bothCounters()).toEqual([
|
||||||
|
{ counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true },
|
||||||
|
{ counters: { 'running-processes': 5 }, batched: true }
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call the batched count endpoint of the app', async () => {
|
||||||
|
await taskCounters();
|
||||||
|
|
||||||
|
expect(countUrl()).toBe('https://fake-bpm-host.com/mock-app/query/v1/count');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should identify the query of every filter by the key of the filter', async () => {
|
||||||
|
await bothCounters();
|
||||||
|
|
||||||
|
expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['my-tasks', 'queued-tasks']);
|
||||||
|
expect(countRequestIds(FilterCounterEntityType.PROCESS_INSTANCE)).toEqual(['running-processes']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send the criteria of every filter along with its request id', async () => {
|
||||||
|
await taskCounters();
|
||||||
|
|
||||||
|
expect(countQueries(FilterCounterEntityType.TASK)[0]).toEqual({
|
||||||
|
requestId: 'my-tasks',
|
||||||
|
status: ['ASSIGNED'],
|
||||||
|
assignee: ['mock-user'],
|
||||||
|
sort: { field: 'createdDate', direction: 'desc', isProcessVariable: false }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not send the filters without a counter enabled', async () => {
|
||||||
|
await taskCounters();
|
||||||
|
|
||||||
|
expect(countRequestIds(FilterCounterEntityType.TASK)).not.toContain('completed-tasks');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send the query of a filter targeting every status', async () => {
|
||||||
|
getProcessFiltersSpy.and.returnValue(of([processFilter({ key: 'all-processes', status: '', showCounter: true })]));
|
||||||
|
|
||||||
|
await processCounters();
|
||||||
|
|
||||||
|
expect(countRequestIds(FilterCounterEntityType.PROCESS_INSTANCE)).toEqual(['all-processes']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should omit an entity type without filters with a counter enabled', async () => {
|
||||||
|
getProcessFiltersSpy.and.returnValue(of([]));
|
||||||
|
|
||||||
|
await bothCounters();
|
||||||
|
|
||||||
|
expect(countRequest().PROCESS_INSTANCE).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should leave out a filter the query cannot be built for', async () => {
|
||||||
|
getTaskListFiltersSpy.and.returnValue(
|
||||||
|
of([taskFilter({ key: 'broken', status: 'ASSIGNED', showCounter: true, sort: undefined, order: undefined }), taskFiltersMock[1]])
|
||||||
|
);
|
||||||
|
|
||||||
|
await taskCounters();
|
||||||
|
|
||||||
|
expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['queued-tasks']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should leave out a filter without a key, since it holds no request id', async () => {
|
||||||
|
getProcessFiltersSpy.and.returnValue(of([processFilter({ key: null, status: 'RUNNING', showCounter: true })]));
|
||||||
|
|
||||||
|
expect(await processCounters()).toEqual({ counters: {}, batched: true });
|
||||||
|
expect(postSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the counters of an entity type when the filters of the other one fail to load', async () => {
|
||||||
|
getProcessFiltersSpy.and.returnValue(throwError(() => new Error('filters failed')));
|
||||||
|
|
||||||
|
await bothCounters();
|
||||||
|
|
||||||
|
expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['my-tasks', 'queued-tasks']);
|
||||||
|
expect(countRequest().PROCESS_INSTANCE).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve no counter when no filter has a counter enabled', async () => {
|
||||||
|
getTaskListFiltersSpy.and.returnValue(of([]));
|
||||||
|
getProcessFiltersSpy.and.returnValue(of([]));
|
||||||
|
|
||||||
|
expect(await taskCounters()).toEqual({ counters: {}, batched: true });
|
||||||
|
expect(postSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the batched count endpoint is not available', () => {
|
||||||
|
it('should report the counters as not batched', async () => {
|
||||||
|
postSpy.and.returnValue(throwError(() => ({ status: 404 })));
|
||||||
|
|
||||||
|
expect(await taskCounters()).toEqual({ counters: {}, batched: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call the endpoint again for the same app', async () => {
|
||||||
|
postSpy.and.returnValue(throwError(() => ({ status: 404 })));
|
||||||
|
|
||||||
|
await taskCounters();
|
||||||
|
expect(await processCounters()).toEqual({ counters: {}, batched: false });
|
||||||
|
|
||||||
|
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep calling the endpoint of the apps that do hold it', async () => {
|
||||||
|
postSpy.and.returnValue(throwError(() => ({ status: 404 })));
|
||||||
|
await taskCounters();
|
||||||
|
|
||||||
|
postSpy.and.returnValue(of(countersMock));
|
||||||
|
expect(await taskCounters('other-app')).toEqual({ counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep calling the endpoint after a transient failure', async () => {
|
||||||
|
postSpy.and.returnValue(throwError(() => ({ status: 500 })));
|
||||||
|
expect(await taskCounters()).toEqual({ counters: {}, batched: false });
|
||||||
|
|
||||||
|
postSpy.and.returnValue(of(countersMock));
|
||||||
|
expect(await taskCounters()).toEqual({ counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true });
|
||||||
|
expect(postSpy).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('batched counters opted in by the filter components', () => {
|
||||||
|
it('should not call the batched count endpoint when it was not asked for', async () => {
|
||||||
|
const result = await firstValueFrom(service.getFilterCounters('mock-app', FilterCounterEntityType.TASK));
|
||||||
|
|
||||||
|
expect(result).toEqual({ counters: {}, batched: false });
|
||||||
|
expect(postSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not load the filters when the batched count endpoint was not asked for', async () => {
|
||||||
|
await firstValueFrom(service.getFilterCounters('mock-app', FilterCounterEntityType.TASK));
|
||||||
|
|
||||||
|
expect(getTaskListFiltersSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call the batched count endpoint when every entity type on screen asked for it', async () => {
|
||||||
|
await bothCounters();
|
||||||
|
|
||||||
|
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call the batched count endpoint when one entity type on screen did not ask for it', fakeAsync(() => {
|
||||||
|
const results: FilterCountersResult[] = [];
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe((result) => results.push(result));
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, false).subscribe();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
expect(postSpy).not.toHaveBeenCalled();
|
||||||
|
expect(results).toEqual([{ counters: {}, batched: false }]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should call the batched count endpoint once the entity type that opted out leaves the screen', fakeAsync(() => {
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe();
|
||||||
|
const processSubscription = service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, false).subscribe();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
processSubscription.unsubscribe();
|
||||||
|
service.refreshFilterCounters('mock-app');
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK]);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('counters scoped to the entity types on screen', () => {
|
||||||
|
it('should send the queries of the entity type on screen alone', async () => {
|
||||||
|
await taskCounters();
|
||||||
|
|
||||||
|
expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not load the filters of an entity type that is not on screen', async () => {
|
||||||
|
await taskCounters();
|
||||||
|
|
||||||
|
expect(getTaskListFiltersSpy).toHaveBeenCalled();
|
||||||
|
expect(getProcessFiltersSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send the queries of both entity types when both are on screen', async () => {
|
||||||
|
await bothCounters();
|
||||||
|
|
||||||
|
expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK, FilterCounterEntityType.PROCESS_INSTANCE]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the counters again when an entity type joins the ones on screen', fakeAsync(() => {
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK]);
|
||||||
|
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, true).subscribe();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
expect(postSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK, FilterCounterEntityType.PROCESS_INSTANCE]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should stop covering an entity type once its counters hold no subscriber', fakeAsync(() => {
|
||||||
|
const taskSubscription = service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe();
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, true).subscribe();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
taskSubscription.unsubscribe();
|
||||||
|
service.refreshFilterCounters('mock-app');
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.PROCESS_INSTANCE]);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('teardown', () => {
|
||||||
|
it('should close the engine event subscription once the counters hold no subscriber', fakeAsync(() => {
|
||||||
|
const subscription = service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe();
|
||||||
|
tick(0);
|
||||||
|
expect(makeGQLQuerySpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
subscription.unsubscribe();
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
expect(makeGQLQuerySpy).toHaveBeenCalledTimes(2);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should keep the engine event subscription while another subscriber holds the same entity type', fakeAsync(() => {
|
||||||
|
const subscription = service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe();
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
subscription.unsubscribe();
|
||||||
|
emitTaskEvent();
|
||||||
|
tick(3000);
|
||||||
|
|
||||||
|
expect(makeGQLQuerySpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(postSpy).toHaveBeenCalledTimes(2);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should release the filters subscription once nothing reads them', fakeAsync(() => {
|
||||||
|
const filters$ = new BehaviorSubject(taskFiltersMock);
|
||||||
|
getTaskListFiltersSpy.and.returnValue(filters$.asObservable());
|
||||||
|
|
||||||
|
const subscriptions = [
|
||||||
|
service.getTaskFilters('mock-app').subscribe(),
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe()
|
||||||
|
];
|
||||||
|
tick(0);
|
||||||
|
expect(filters$.observed).toBeTrue();
|
||||||
|
|
||||||
|
subscriptions.forEach((subscription) => subscription.unsubscribe());
|
||||||
|
|
||||||
|
expect(filters$.observed).toBeFalse();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should resolve the counters again for a subscriber that comes after a full teardown', fakeAsync(() => {
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe().unsubscribe();
|
||||||
|
tick(0);
|
||||||
|
postSpy.calls.reset();
|
||||||
|
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('refreshFilterCounters', () => {
|
||||||
|
it('should resolve the counters again with a single request', fakeAsync(() => {
|
||||||
|
const results: FilterCountersResult[] = [];
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe((result) => results.push(result));
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, true).subscribe();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
service.refreshFilterCounters('mock-app');
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
expect(postSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(results.length).toBe(2);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not resolve the counters of an app without subscribers', fakeAsync(() => {
|
||||||
|
service.refreshFilterCounters('mock-app');
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
expect(postSpy).not.toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when only one of the two filter families is wired', () => {
|
||||||
|
const configureTasksOnly = () => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [NoopAuthModule, ApolloTestingModule],
|
||||||
|
providers: [{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }]
|
||||||
|
});
|
||||||
|
|
||||||
|
const tasksOnlyService = TestBed.inject(FilterCountersCloudService);
|
||||||
|
TestBed.inject(AppConfigService).config.bpmHost = 'https://fake-bpm-host.com';
|
||||||
|
spyOn(TestBed.inject(NotificationCloudService), 'makeGQLQuery').and.returnValue(new Subject<EngineEventsResult>().asObservable());
|
||||||
|
spyOn(TestBed.inject(TaskFilterCloudService), 'getTaskListFilters').and.returnValue(of(taskFiltersMock));
|
||||||
|
postSpy = spyOn(tasksOnlyService as unknown as CountEndpoint, 'post').and.returnValue(of(countersMock));
|
||||||
|
|
||||||
|
return tasksOnlyService;
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should resolve the counters of the wired family', async () => {
|
||||||
|
const tasksOnlyService = configureTasksOnly();
|
||||||
|
|
||||||
|
const result = await firstValueFrom(tasksOnlyService.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true));
|
||||||
|
|
||||||
|
expect(result).toEqual({ counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should leave the filters of the family that is not wired out of the request', async () => {
|
||||||
|
const tasksOnlyService = configureTasksOnly();
|
||||||
|
|
||||||
|
await firstValueFrom(tasksOnlyService.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true));
|
||||||
|
|
||||||
|
expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['my-tasks', 'queued-tasks']);
|
||||||
|
expect(countRequest().PROCESS_INSTANCE).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getEngineEvents', () => {
|
||||||
|
it('should return EMPTY when appName is not set', () => {
|
||||||
|
let completed = false;
|
||||||
|
service.getEngineEvents('', FilterCounterEntityType.TASK).subscribe({ complete: () => (completed = true) });
|
||||||
|
|
||||||
|
expect(completed).toBeTrue();
|
||||||
|
expect(makeGQLQuerySpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should subscribe to the events of the task entity type alone', () => {
|
||||||
|
service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
|
|
||||||
|
const [appName, query] = makeGQLQuerySpy.calls.mostRecent().args;
|
||||||
|
expect(appName).toBe('mock-app');
|
||||||
|
expect(query).toContain('TASK_CREATED');
|
||||||
|
expect(query).not.toContain('PROCESS_STARTED');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should subscribe to the events of the process entity type alone', () => {
|
||||||
|
service.getEngineEvents('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe();
|
||||||
|
|
||||||
|
const [, query] = makeGQLQuerySpy.calls.mostRecent().args;
|
||||||
|
expect(query).toContain('PROCESS_STARTED');
|
||||||
|
expect(query).not.toContain('TASK_CREATED');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open a single subscription for multiple subscribers of the same entity type', () => {
|
||||||
|
service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
|
service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
|
|
||||||
|
expect(makeGQLQuerySpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open a separate subscription per entity type', () => {
|
||||||
|
service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
|
service.getEngineEvents('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe();
|
||||||
|
|
||||||
|
expect(makeGQLQuerySpy).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open a separate subscription per app', () => {
|
||||||
|
service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
|
service.getEngineEvents('other-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
|
|
||||||
|
expect(makeGQLQuerySpy).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit the debounced batch of events', fakeAsync(() => {
|
||||||
|
const batches: TaskCloudEngineEvent[][] = [];
|
||||||
|
service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe((events) => batches.push(events));
|
||||||
|
|
||||||
|
emitTaskEvent('TASK_CREATED');
|
||||||
|
emitTaskEvent('TASK_ASSIGNED');
|
||||||
|
tick(3000);
|
||||||
|
|
||||||
|
expect(batches.length).toBe(1);
|
||||||
|
expect(batches[0][0].eventType).toBe('TASK_ASSIGNED');
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should debounce the events using the configured debounce time', fakeAsync(() => {
|
||||||
|
spyOnProperty(service, 'notificationDebounceTime', 'get').and.returnValue(5000);
|
||||||
|
let emitted = false;
|
||||||
|
service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe(() => (emitted = true));
|
||||||
|
|
||||||
|
emitTaskEvent();
|
||||||
|
tick(3000);
|
||||||
|
expect(emitted).toBeFalse();
|
||||||
|
|
||||||
|
tick(2000);
|
||||||
|
expect(emitted).toBeTrue();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('counters driven by the engine events', () => {
|
||||||
|
it('should make a single count request for a batch of events of both entity types', fakeAsync(() => {
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe();
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, true).subscribe();
|
||||||
|
tick(0);
|
||||||
|
postSpy.calls.reset();
|
||||||
|
|
||||||
|
emitTaskEvent();
|
||||||
|
emitProcessEvent();
|
||||||
|
tick(3000);
|
||||||
|
|
||||||
|
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should make a single count request for the events of both entity types arriving apart', fakeAsync(() => {
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe();
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, true).subscribe();
|
||||||
|
tick(0);
|
||||||
|
postSpy.calls.reset();
|
||||||
|
|
||||||
|
emitTaskEvent();
|
||||||
|
tick(1000);
|
||||||
|
emitProcessEvent();
|
||||||
|
tick(3000);
|
||||||
|
|
||||||
|
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should emit the counters resolved for the batch of events', fakeAsync(() => {
|
||||||
|
const results: FilterCountersResult[] = [];
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe((result) => results.push(result));
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
postSpy.and.returnValue(of({ TASK: { 'my-tasks': 9 } }));
|
||||||
|
emitTaskEvent();
|
||||||
|
tick(3000);
|
||||||
|
|
||||||
|
expect(results.length).toBe(2);
|
||||||
|
expect(results[1]).toEqual({ counters: { 'my-tasks': 9 }, batched: true });
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not subscribe to the events of an entity type that is not on screen', fakeAsync(() => {
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
expect(makeGQLQuerySpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(makeGQLQuerySpy.calls.mostRecent().args[1]).toContain('TASK_CREATED');
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not resolve the counters again on the events of an entity type that is not on screen', fakeAsync(() => {
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe();
|
||||||
|
tick(0);
|
||||||
|
postSpy.calls.reset();
|
||||||
|
|
||||||
|
emitProcessEvent();
|
||||||
|
tick(3000);
|
||||||
|
|
||||||
|
expect(postSpy).not.toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should stop resolving the counters on the events of an entity type that left the screen', fakeAsync(() => {
|
||||||
|
const taskSubscription = service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe();
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE, true).subscribe();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
taskSubscription.unsubscribe();
|
||||||
|
postSpy.calls.reset();
|
||||||
|
emitTaskEvent();
|
||||||
|
tick(3000);
|
||||||
|
|
||||||
|
expect(postSpy).not.toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not subscribe to the engine events when notifications are disabled', fakeAsync(() => {
|
||||||
|
appConfigService.config.notifications = false;
|
||||||
|
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK, true).subscribe();
|
||||||
|
tick(3000);
|
||||||
|
|
||||||
|
expect(makeGQLQuerySpy).not.toHaveBeenCalled();
|
||||||
|
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
/*!
|
||||||
|
* @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, Injector } from '@angular/core';
|
||||||
|
import { asapScheduler, combineLatest, defer, EMPTY, merge, Observable, of, Subject, Subscription } from 'rxjs';
|
||||||
|
import { catchError, debounceTime, finalize, 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 { TaskFilterCloudModel } from '../task/task-filters/models/filter-cloud.model';
|
||||||
|
import { ProcessFilterCloudModel } from '../process/process-filters/models/process-filter-cloud.model';
|
||||||
|
import { ProcessFilterCloudAdapter } from '../process/process-list/models/process-cloud-query-request.model';
|
||||||
|
import {
|
||||||
|
FilterCounterCandidate,
|
||||||
|
FilterCounterEntityType,
|
||||||
|
FilterCounters,
|
||||||
|
FilterCountersQuery,
|
||||||
|
FilterCountersRequest,
|
||||||
|
FilterCountersResult
|
||||||
|
} from '../models/filter-counters-cloud.model';
|
||||||
|
const BATCHED_COUNTERS_UNAVAILABLE_STATUSES = [404, 501];
|
||||||
|
|
||||||
|
interface FilterCountersFilters {
|
||||||
|
[FilterCounterEntityType.TASK]: TaskFilterCloudModel[];
|
||||||
|
[FilterCounterEntityType.PROCESS_INSTANCE]: ProcessFilterCloudModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EngineEventsData {
|
||||||
|
engineEvents?: TaskCloudEngineEvent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ENGINE_EVENTS_SUBSCRIPTION_QUERIES: Record<FilterCounterEntityType, string> = {
|
||||||
|
[FilterCounterEntityType.TASK]: `
|
||||||
|
subscription {
|
||||||
|
engineEvents(eventType: [
|
||||||
|
TASK_COMPLETED
|
||||||
|
TASK_ASSIGNED
|
||||||
|
TASK_ACTIVATED
|
||||||
|
TASK_SUSPENDED
|
||||||
|
TASK_CANCELLED
|
||||||
|
TASK_CREATED
|
||||||
|
]) {
|
||||||
|
eventType
|
||||||
|
entity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
[FilterCounterEntityType.PROCESS_INSTANCE]: `
|
||||||
|
subscription {
|
||||||
|
engineEvents(eventType: [
|
||||||
|
PROCESS_CANCELLED
|
||||||
|
PROCESS_COMPLETED
|
||||||
|
PROCESS_CREATED
|
||||||
|
PROCESS_RESUMED
|
||||||
|
PROCESS_SUSPENDED
|
||||||
|
PROCESS_STARTED
|
||||||
|
]) {
|
||||||
|
eventType
|
||||||
|
entity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class FilterCountersCloudService extends BaseCloudService {
|
||||||
|
private readonly notificationCloudService = inject(NotificationCloudService);
|
||||||
|
private readonly taskListCloudService = inject(TaskListCloudService);
|
||||||
|
private readonly processListCloudService = inject(ProcessListCloudService);
|
||||||
|
private readonly injector = inject(Injector);
|
||||||
|
|
||||||
|
private readonly eventsPerEntityType = new Map<string, Observable<TaskCloudEngineEvent[]>>();
|
||||||
|
private readonly rawEventsPerEntityType = new Map<string, Observable<TaskCloudEngineEvent[]>>();
|
||||||
|
private readonly recountPerApp = new Map<string, Subject<void>>();
|
||||||
|
private readonly eventRecountPerApp = new Map<string, Subject<void>>();
|
||||||
|
private readonly activeEntityTypesPerApp = new Map<string, Set<FilterCounterEntityType>>();
|
||||||
|
private readonly subscribersPerEntityType = new Map<string, number>();
|
||||||
|
private readonly batchedCountersPerEntityType = new Map<string, boolean>();
|
||||||
|
private readonly eventSubscriptionsPerEntityType = new Map<string, Subscription>();
|
||||||
|
private readonly appsWithoutBatchedCounters = new Set<string>();
|
||||||
|
private readonly taskFiltersPerApp = new Map<string, Observable<TaskFilterCloudModel[]>>();
|
||||||
|
private readonly processFiltersPerApp = new Map<string, Observable<ProcessFilterCloudModel[]>>();
|
||||||
|
private readonly countersPerApp = new Map<string, Observable<{ counters: FilterCounters; batched: boolean }>>();
|
||||||
|
|
||||||
|
get notificationDebounceTime(): number {
|
||||||
|
return this.appConfigService.get('notificationDebounceTime', 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTaskFilters(appName: string): Observable<TaskFilterCloudModel[]> {
|
||||||
|
return this.shareFilters(this.taskFiltersPerApp, appName, () => this.injector.get(TaskFilterCloudService).getTaskListFilters(appName));
|
||||||
|
}
|
||||||
|
|
||||||
|
getProcessFilters(appName: string): Observable<ProcessFilterCloudModel[]> {
|
||||||
|
return this.shareFilters(this.processFiltersPerApp, appName, () => this.injector.get(ProcessFilterCloudService).getProcessFilters(appName));
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilterCounters(appName: string, entityType: FilterCounterEntityType, batchedCounters = false): Observable<FilterCountersResult> {
|
||||||
|
if (!appName) {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return defer(() => {
|
||||||
|
this.activateEntityType(appName, entityType, batchedCounters);
|
||||||
|
|
||||||
|
return this.getCounters(appName);
|
||||||
|
}).pipe(
|
||||||
|
map(({ counters, batched }) => ({ counters: counters[entityType] ?? {}, batched })),
|
||||||
|
finalize(() => this.deactivateEntityType(appName, entityType))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshFilterCounters(appName: string): void {
|
||||||
|
this.recount(appName);
|
||||||
|
}
|
||||||
|
|
||||||
|
getEngineEvents(appName: string, entityType: FilterCounterEntityType): Observable<TaskCloudEngineEvent[]> {
|
||||||
|
if (!appName) {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = this.entityTypeKey(appName, entityType);
|
||||||
|
let events$ = this.eventsPerEntityType.get(key);
|
||||||
|
if (!events$) {
|
||||||
|
events$ = this.rawEngineEvents(appName, entityType).pipe(
|
||||||
|
debounceTime(this.notificationDebounceTime),
|
||||||
|
shareReplay({ bufferSize: 1, refCount: true })
|
||||||
|
);
|
||||||
|
this.eventsPerEntityType.set(key, events$);
|
||||||
|
}
|
||||||
|
|
||||||
|
return events$;
|
||||||
|
}
|
||||||
|
|
||||||
|
private rawEngineEvents(appName: string, entityType: FilterCounterEntityType): Observable<TaskCloudEngineEvent[]> {
|
||||||
|
const key = this.entityTypeKey(appName, entityType);
|
||||||
|
let events$ = this.rawEventsPerEntityType.get(key);
|
||||||
|
if (!events$) {
|
||||||
|
events$ = defer(() =>
|
||||||
|
this.notificationCloudService.makeGQLQuery<EngineEventsData>(appName, ENGINE_EVENTS_SUBSCRIPTION_QUERIES[entityType])
|
||||||
|
).pipe(
|
||||||
|
map((result) => result.data?.engineEvents ?? []),
|
||||||
|
catchError(() => EMPTY),
|
||||||
|
shareReplay({ bufferSize: 1, refCount: true })
|
||||||
|
);
|
||||||
|
this.rawEventsPerEntityType.set(key, events$);
|
||||||
|
}
|
||||||
|
|
||||||
|
return events$;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get notificationsEnabled(): boolean {
|
||||||
|
return this.appConfigService.get('notifications', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private activateEntityType(appName: string, entityType: FilterCounterEntityType, batchedCounters: boolean): void {
|
||||||
|
const key = this.entityTypeKey(appName, entityType);
|
||||||
|
const subscribers = (this.subscribersPerEntityType.get(key) ?? 0) + 1;
|
||||||
|
this.subscribersPerEntityType.set(key, subscribers);
|
||||||
|
|
||||||
|
if (subscribers > 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.batchedCountersPerEntityType.set(key, batchedCounters);
|
||||||
|
|
||||||
|
const activeEntityTypes = this.activeEntityTypes(appName);
|
||||||
|
const joinsResolvedCounters = activeEntityTypes.size > 0;
|
||||||
|
activeEntityTypes.add(entityType);
|
||||||
|
|
||||||
|
if (this.notificationsEnabled) {
|
||||||
|
this.eventSubscriptionsPerEntityType.set(
|
||||||
|
key,
|
||||||
|
this.rawEngineEvents(appName, entityType).subscribe(() => this.eventRecountTrigger(appName).next())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (joinsResolvedCounters) {
|
||||||
|
this.recount(appName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private deactivateEntityType(appName: string, entityType: FilterCounterEntityType): void {
|
||||||
|
const key = this.entityTypeKey(appName, entityType);
|
||||||
|
const subscribers = (this.subscribersPerEntityType.get(key) ?? 1) - 1;
|
||||||
|
|
||||||
|
if (subscribers > 0) {
|
||||||
|
this.subscribersPerEntityType.set(key, subscribers);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subscribersPerEntityType.delete(key);
|
||||||
|
this.batchedCountersPerEntityType.delete(key);
|
||||||
|
this.activeEntityTypes(appName).delete(entityType);
|
||||||
|
this.eventSubscriptionsPerEntityType.get(key)?.unsubscribe();
|
||||||
|
this.eventSubscriptionsPerEntityType.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private activeEntityTypes(appName: string): Set<FilterCounterEntityType> {
|
||||||
|
let activeEntityTypes = this.activeEntityTypesPerApp.get(appName);
|
||||||
|
if (!activeEntityTypes) {
|
||||||
|
activeEntityTypes = new Set<FilterCounterEntityType>();
|
||||||
|
this.activeEntityTypesPerApp.set(appName, activeEntityTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeEntityTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private entityTypeKey(appName: string, entityType: FilterCounterEntityType): string {
|
||||||
|
return `${appName}|${entityType}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private recount(appName: string): void {
|
||||||
|
this.recountTrigger(appName).next();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFiltersForCounters(appName: string): Observable<FilterCountersFilters> {
|
||||||
|
const activeEntityTypes = this.activeEntityTypes(appName);
|
||||||
|
|
||||||
|
return combineLatest({
|
||||||
|
[FilterCounterEntityType.TASK]: activeEntityTypes.has(FilterCounterEntityType.TASK)
|
||||||
|
? this.getTaskFilters(appName).pipe(catchError(() => of([])))
|
||||||
|
: of([]),
|
||||||
|
[FilterCounterEntityType.PROCESS_INSTANCE]: activeEntityTypes.has(FilterCounterEntityType.PROCESS_INSTANCE)
|
||||||
|
? this.getProcessFilters(appName).pipe(catchError(() => of([])))
|
||||||
|
: of([])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private shareFilters<T>(cache: Map<string, Observable<T[]>>, appName: string, loadFilters: () => Observable<T[]>): Observable<T[]> {
|
||||||
|
let filters$ = cache.get(appName);
|
||||||
|
if (!filters$) {
|
||||||
|
filters$ = defer(loadFilters).pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
||||||
|
cache.set(appName, filters$);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters$;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCounters(appName: string): Observable<{ counters: FilterCounters; batched: boolean }> {
|
||||||
|
let counters$ = this.countersPerApp.get(appName);
|
||||||
|
if (!counters$) {
|
||||||
|
counters$ = this.recounts(appName).pipe(
|
||||||
|
switchMap(() => this.resolveCounters(appName)),
|
||||||
|
shareReplay({ bufferSize: 1, refCount: true })
|
||||||
|
);
|
||||||
|
this.countersPerApp.set(appName, counters$);
|
||||||
|
}
|
||||||
|
|
||||||
|
return counters$;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveCounters(appName: string): Observable<{ counters: FilterCounters; batched: boolean }> {
|
||||||
|
if (!this.batchedCountersEnabled(appName) || this.appsWithoutBatchedCounters.has(appName)) {
|
||||||
|
return of({ counters: {}, batched: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getFiltersForCounters(appName).pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap((filters) => this.fetchFilterCounters(appName, this.buildRequest(filters))),
|
||||||
|
map((counters) => ({ counters, batched: true })),
|
||||||
|
catchError((error) => {
|
||||||
|
if (BATCHED_COUNTERS_UNAVAILABLE_STATUSES.includes(error?.status)) {
|
||||||
|
this.appsWithoutBatchedCounters.add(appName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return of({ counters: {}, batched: false });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private batchedCountersEnabled(appName: string): boolean {
|
||||||
|
const activeEntityTypes = [...this.activeEntityTypes(appName)];
|
||||||
|
|
||||||
|
return (
|
||||||
|
activeEntityTypes.length > 0 &&
|
||||||
|
activeEntityTypes.every((entityType) => this.batchedCountersPerEntityType.get(this.entityTypeKey(appName, entityType)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private recounts(appName: string): Observable<unknown> {
|
||||||
|
return merge(
|
||||||
|
merge(of(undefined), this.recountTrigger(appName)).pipe(debounceTime(0, asapScheduler)),
|
||||||
|
this.eventRecountTrigger(appName).pipe(debounceTime(this.notificationDebounceTime))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private recountTrigger(appName: string): Subject<void> {
|
||||||
|
let recount$ = this.recountPerApp.get(appName);
|
||||||
|
if (!recount$) {
|
||||||
|
recount$ = new Subject<void>();
|
||||||
|
this.recountPerApp.set(appName, recount$);
|
||||||
|
}
|
||||||
|
|
||||||
|
return recount$;
|
||||||
|
}
|
||||||
|
|
||||||
|
private eventRecountTrigger(appName: string): Subject<void> {
|
||||||
|
let eventRecount$ = this.eventRecountPerApp.get(appName);
|
||||||
|
if (!eventRecount$) {
|
||||||
|
eventRecount$ = new Subject<void>();
|
||||||
|
this.eventRecountPerApp.set(appName, eventRecount$);
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventRecount$;
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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) => Omit<FilterCountersQuery, 'requestId'>
|
||||||
|
): FilterCountersQuery[] {
|
||||||
|
return (filters ?? [])
|
||||||
|
.filter((filter) => filter?.showCounter && this.isCounterBatched(filter))
|
||||||
|
.map((filter) => {
|
||||||
|
try {
|
||||||
|
return { ...buildQuery(filter), requestId: filter.key as string };
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((query): query is FilterCountersQuery => !!query);
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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 || {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private isCounterBatched(filter: FilterCounterCandidate): boolean {
|
||||||
|
return !!filter?.key;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,8 +15,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { gql } from '@apollo/client/core';
|
import { FetchResult, gql } from '@apollo/client/core';
|
||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
import { WebSocketService } from './web-socket.service';
|
import { WebSocketService } from './web-socket.service';
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -24,8 +25,8 @@ import { WebSocketService } from './web-socket.service';
|
|||||||
export class NotificationCloudService {
|
export class NotificationCloudService {
|
||||||
private readonly webSocketService = inject(WebSocketService);
|
private readonly webSocketService = inject(WebSocketService);
|
||||||
|
|
||||||
makeGQLQuery(appName: string, gqlQuery: string) {
|
makeGQLQuery<T = unknown>(appName: string, gqlQuery: string): Observable<FetchResult<T>> {
|
||||||
return this.webSocketService.getSubscription({
|
return this.webSocketService.getSubscription<T>({
|
||||||
apolloClientName: appName,
|
apolloClientName: appName,
|
||||||
wsUrl: `${appName}/notifications`,
|
wsUrl: `${appName}/notifications`,
|
||||||
httpUrl: `${appName}/notifications/v2/ws/graphql`,
|
httpUrl: `${appName}/notifications/v2/ws/graphql`,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
export * from './base-cloud.service';
|
export * from './base-cloud.service';
|
||||||
export * from './cloud-token.service';
|
export * from './cloud-token.service';
|
||||||
|
export * from './filter-counters-cloud.service';
|
||||||
export * from './form-fields.interfaces';
|
export * from './form-fields.interfaces';
|
||||||
export * from './local-preference-cloud.service';
|
export * from './local-preference-cloud.service';
|
||||||
export * from './notification-cloud.service';
|
export * from './notification-cloud.service';
|
||||||
|
|||||||
+168
-25
@@ -17,10 +17,10 @@
|
|||||||
|
|
||||||
import { AppConfigService, NoopAuthModule } from '@alfresco/adf-core';
|
import { AppConfigService, NoopAuthModule } from '@alfresco/adf-core';
|
||||||
import { Component, SimpleChange } from '@angular/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 { By } from '@angular/platform-browser';
|
||||||
import { first, of, Subject, throwError } from 'rxjs';
|
import { first, NEVER, of, Subject, throwError } from 'rxjs';
|
||||||
import { TASK_FILTERS_SERVICE_TOKEN } from '../../../../services/cloud-token.service';
|
import { PROCESS_FILTERS_SERVICE_TOKEN, TASK_FILTERS_SERVICE_TOKEN } from '../../../../services/cloud-token.service';
|
||||||
import { LocalPreferenceCloudService } from '../../../../services/local-preference-cloud.service';
|
import { LocalPreferenceCloudService } from '../../../../services/local-preference-cloud.service';
|
||||||
import { defaultTaskFiltersMock, fakeGlobalFilter, taskNotifications } from '../../mock/task-filters-cloud.mock';
|
import { defaultTaskFiltersMock, fakeGlobalFilter, taskNotifications } from '../../mock/task-filters-cloud.mock';
|
||||||
import { TaskFilterCloudService } from '../../services/task-filter-cloud.service';
|
import { TaskFilterCloudService } from '../../services/task-filter-cloud.service';
|
||||||
@@ -35,6 +35,9 @@ import { TaskFilterCloudModel } from '../../models/filter-cloud.model';
|
|||||||
import { MatIconHarness } from '@angular/material/icon/testing';
|
import { MatIconHarness } from '@angular/material/icon/testing';
|
||||||
import { ActivatedRoute, provideRouter, Router } from '@angular/router';
|
import { ActivatedRoute, provideRouter, Router } from '@angular/router';
|
||||||
import { RouterTestingHarness } from '@angular/router/testing';
|
import { RouterTestingHarness } from '@angular/router/testing';
|
||||||
|
import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service';
|
||||||
|
import { FilterCounterEntityType } from '../../../../models/filter-counters-cloud.model';
|
||||||
|
import { TaskCloudEngineEvent } from '../../../../models/engine-event-cloud.model';
|
||||||
|
|
||||||
@Component({ selector: 'adf-cloud-dummy', template: '' })
|
@Component({ selector: 'adf-cloud-dummy', template: '' })
|
||||||
class DummyComponent {}
|
class DummyComponent {}
|
||||||
@@ -50,7 +53,10 @@ describe('TaskFiltersCloudComponent', () => {
|
|||||||
let getTaskFilterCounterSpy: jasmine.Spy;
|
let getTaskFilterCounterSpy: jasmine.Spy;
|
||||||
let getTaskListFiltersSpy: jasmine.Spy;
|
let getTaskListFiltersSpy: jasmine.Spy;
|
||||||
let getTaskListCountSpy: jasmine.Spy;
|
let getTaskListCountSpy: jasmine.Spy;
|
||||||
let getTaskNotificationSubscriptionSpy: jasmine.Spy;
|
let getEngineEventsSpy: jasmine.Spy;
|
||||||
|
let filterCountersService: FilterCountersCloudService;
|
||||||
|
let getFilterCountersSpy: jasmine.Spy;
|
||||||
|
let refreshFilterCountersSpy: jasmine.Spy;
|
||||||
let router: Router;
|
let router: Router;
|
||||||
|
|
||||||
const configureTestingModule = async (searchApiMethod: 'GET' | 'POST') => {
|
const configureTestingModule = async (searchApiMethod: 'GET' | 'POST') => {
|
||||||
@@ -58,6 +64,7 @@ describe('TaskFiltersCloudComponent', () => {
|
|||||||
imports: [NoopAuthModule, TaskFiltersCloudComponent, ApolloTestingModule],
|
imports: [NoopAuthModule, TaskFiltersCloudComponent, ApolloTestingModule],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService },
|
{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService },
|
||||||
|
{ provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService },
|
||||||
provideRouter([{ path: 'task-list-cloud', component: DummyComponent }]),
|
provideRouter([{ path: 'task-list-cloud', component: DummyComponent }]),
|
||||||
{
|
{
|
||||||
provide: ActivatedRoute,
|
provide: ActivatedRoute,
|
||||||
@@ -76,10 +83,15 @@ describe('TaskFiltersCloudComponent', () => {
|
|||||||
});
|
});
|
||||||
taskFilterService = TestBed.inject(TaskFilterCloudService);
|
taskFilterService = TestBed.inject(TaskFilterCloudService);
|
||||||
taskListService = TestBed.inject(TaskListCloudService);
|
taskListService = TestBed.inject(TaskListCloudService);
|
||||||
|
filterCountersService = TestBed.inject(FilterCountersCloudService);
|
||||||
getTaskFilterCounterSpy = spyOn(taskFilterService, 'getTaskFilterCounter').and.returnValue(of(11));
|
getTaskFilterCounterSpy = spyOn(taskFilterService, 'getTaskFilterCounter').and.returnValue(of(11));
|
||||||
getTaskListCountSpy = spyOn(taskListService, 'getTaskListCount').and.returnValue(of(11));
|
getTaskListCountSpy = spyOn(taskListService, 'getTaskListCount').and.returnValue(of(11));
|
||||||
getTaskNotificationSubscriptionSpy = spyOn(taskFilterService, 'getTaskNotificationSubscription').and.returnValue(of(taskNotifications));
|
getEngineEventsSpy = spyOn(filterCountersService, 'getEngineEvents').and.returnValue(of(taskNotifications));
|
||||||
getTaskListFiltersSpy = spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(of(fakeGlobalFilter));
|
getTaskListFiltersSpy = spyOn(filterCountersService, 'getTaskFilters').and.returnValue(of(fakeGlobalFilter));
|
||||||
|
getFilterCountersSpy = spyOn(filterCountersService, 'getFilterCounters').and.returnValue(
|
||||||
|
of({ counters: { 'fake-involved-tasks': 11 }, batched: true })
|
||||||
|
);
|
||||||
|
refreshFilterCountersSpy = spyOn(filterCountersService, 'refreshFilterCounters');
|
||||||
|
|
||||||
appConfigService = TestBed.inject(AppConfigService);
|
appConfigService = TestBed.inject(AppConfigService);
|
||||||
|
|
||||||
@@ -261,7 +273,7 @@ describe('TaskFiltersCloudComponent', () => {
|
|||||||
expect(updatedFilterCounters.length).toBe(0);
|
expect(updatedFilterCounters.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update filter counter when filter is selected', async () => {
|
it('should refresh the filter counters when a filter is selected', async () => {
|
||||||
component.showIcons = true;
|
component.showIcons = true;
|
||||||
await bindAppName();
|
await bindAppName();
|
||||||
|
|
||||||
@@ -269,7 +281,7 @@ describe('TaskFiltersCloudComponent', () => {
|
|||||||
filterButton.click();
|
filterButton.click();
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(getTaskFilterCounterSpy).toHaveBeenCalledWith(fakeGlobalFilter[0]);
|
expect(refreshFilterCountersSpy).toHaveBeenCalledWith('my-app-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Notifications config', () => {
|
describe('Notifications config', () => {
|
||||||
@@ -306,30 +318,33 @@ describe('TaskFiltersCloudComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not subscribe to notifications when appName is missing', () => {
|
it('should not subscribe to notifications when appName is missing', () => {
|
||||||
getTaskNotificationSubscriptionSpy.calls.reset();
|
getEngineEventsSpy.calls.reset();
|
||||||
component.appName = '';
|
component.appName = '';
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(getTaskNotificationSubscriptionSpy).not.toHaveBeenCalled();
|
expect(getEngineEventsSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should debounce notification subscription using the configured debounce time', fakeAsync(() => {
|
it('should subscribe to the notifications of the bound app', () => {
|
||||||
const notifications$ = new Subject<typeof taskNotifications>();
|
|
||||||
getTaskNotificationSubscriptionSpy.and.returnValue(notifications$.asObservable());
|
|
||||||
component.appName = 'my-app-1';
|
component.appName = 'my-app-1';
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const updateFilterCountersSpy = spyOn(component, 'updateFilterCounters');
|
expect(getEngineEventsSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK);
|
||||||
|
});
|
||||||
|
|
||||||
notifications$.next(taskNotifications);
|
it('should emit the events of the debounced batch', fakeAsync(() => {
|
||||||
tick(1000);
|
const events$ = new Subject<TaskCloudEngineEvent[]>();
|
||||||
expect(updateFilterCountersSpy).not.toHaveBeenCalled();
|
getEngineEventsSpy.and.returnValue(events$.asObservable());
|
||||||
|
const filterCounterUpdatedSpy = spyOn(component.filterCounterUpdated, 'emit');
|
||||||
|
component.appName = 'my-app-1';
|
||||||
|
|
||||||
tick(2000);
|
fixture.detectChanges();
|
||||||
expect(updateFilterCountersSpy).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
|
events$.next(taskNotifications);
|
||||||
|
|
||||||
|
expect(filterCounterUpdatedSpy).toHaveBeenCalledWith(taskNotifications);
|
||||||
flush();
|
flush();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@@ -438,7 +453,7 @@ describe('TaskFiltersCloudComponent', () => {
|
|||||||
expect(updatedFilterCounters.length).toBe(0);
|
expect(updatedFilterCounters.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update filter counter when filter is selected', async () => {
|
it('should refresh the filter counters when a filter is selected', async () => {
|
||||||
await bindAppName();
|
await bindAppName();
|
||||||
|
|
||||||
const filterButton = await loader.getHarness(
|
const filterButton = await loader.getHarness(
|
||||||
@@ -446,6 +461,14 @@ describe('TaskFiltersCloudComponent', () => {
|
|||||||
);
|
);
|
||||||
await filterButton.click();
|
await filterButton.click();
|
||||||
|
|
||||||
|
expect(refreshFilterCountersSpy).toHaveBeenCalledWith('my-app-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the counters with the POST method when the batched endpoint is not available', async () => {
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: false }));
|
||||||
|
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
expect(getTaskListCountSpy).toHaveBeenCalledWith(new TaskFilterCloudAdapter(fakeGlobalFilter[0]));
|
expect(getTaskListCountSpy).toHaveBeenCalledWith(new TaskFilterCloudAdapter(fakeGlobalFilter[0]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -658,17 +681,137 @@ describe('TaskFiltersCloudComponent', () => {
|
|||||||
expect(component.updatedCountersSet.has(fakeFilterKey)).toBe(true);
|
expect(component.updatedCountersSet.has(fakeFilterKey)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call fetchTaskFilterCounter only if filter.showCounter is true', () => {
|
it('should resolve the counter only of the filters with a counter enabled', () => {
|
||||||
const filterWithCounter = new TaskFilterCloudModel({ ...defaultTaskFiltersMock[0], showCounter: true });
|
const filterWithCounter = new TaskFilterCloudModel({ ...defaultTaskFiltersMock[0], showCounter: true });
|
||||||
const filterWithoutCounter = new TaskFilterCloudModel({ ...defaultTaskFiltersMock[1], showCounter: false });
|
const filterWithoutCounter = new TaskFilterCloudModel({ ...defaultTaskFiltersMock[1], showCounter: false });
|
||||||
const fetchSpy = spyOn<any>(component, 'fetchTaskFilterCounter').and.returnValue(of(42));
|
getTaskFilterCounterSpy.calls.reset();
|
||||||
|
|
||||||
component.filters = [filterWithCounter, filterWithoutCounter];
|
component.filters = [filterWithCounter, filterWithoutCounter];
|
||||||
component.updateFilterCounters();
|
component.updateFilterCounters();
|
||||||
|
|
||||||
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
expect(getTaskFilterCounterSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(fetchSpy).toHaveBeenCalledWith(filterWithCounter);
|
expect(getTaskFilterCounterSpy).toHaveBeenCalledWith(filterWithCounter);
|
||||||
expect(fetchSpy).not.toHaveBeenCalledWith(filterWithoutCounter);
|
});
|
||||||
|
|
||||||
|
describe('Batched counters', () => {
|
||||||
|
it('should read the counters without waiting for the filters', async () => {
|
||||||
|
getTaskListFiltersSpy.and.returnValue(NEVER);
|
||||||
|
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
|
expect(getFilterCountersSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hold the counters until the filters they belong to arrive', async () => {
|
||||||
|
const filters$ = new Subject<TaskFilterCloudModel[]>();
|
||||||
|
getTaskListFiltersSpy.and.returnValue(filters$.asObservable());
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: { 'fake-involved-tasks': 9 }, batched: true }));
|
||||||
|
|
||||||
|
await bindAppName();
|
||||||
|
expect(component.counters['fake-involved-tasks']).toBeUndefined();
|
||||||
|
|
||||||
|
filters$.next(fakeGlobalFilter);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(component.counters['fake-involved-tasks']).toBe(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read the counters of the task filters of the bound app', async () => {
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
|
expect(getFilterCountersSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not ask for the batched count endpoint by default', async () => {
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
|
expect(component.useBatchedCounters).toBeFalse();
|
||||||
|
expect(getFilterCountersSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ask for the batched count endpoint when the input is set', async () => {
|
||||||
|
fixture.componentRef.setInput('useBatchedCounters', true);
|
||||||
|
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
|
expect(getFilterCountersSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read the counters again when the input changes', async () => {
|
||||||
|
await bindAppName();
|
||||||
|
getFilterCountersSpy.calls.reset();
|
||||||
|
|
||||||
|
fixture.componentRef.setInput('useBatchedCounters', true);
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
|
||||||
|
expect(getFilterCountersSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hold the counters resolved by the batched count request', async () => {
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: { 'fake-involved-tasks': 9 }, batched: true }));
|
||||||
|
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
|
expect(component.counters['fake-involved-tasks']).toBe(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit the filters whose counter changed', async () => {
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: { 'fake-involved-tasks': 9 }, batched: true }));
|
||||||
|
const updatedFilterSpy = spyOn(component.updatedFilter, 'emit');
|
||||||
|
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
|
expect(updatedFilterSpy).toHaveBeenCalledWith('fake-involved-tasks');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the counter of a filter the batch left out on its own', async () => {
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: true }));
|
||||||
|
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
|
expect(getTaskFilterCounterSpy).toHaveBeenCalledWith(fakeGlobalFilter[0]);
|
||||||
|
expect(component.counters['fake-involved-tasks']).toBe(11);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep the counters of the other filters when one counter cannot be resolved', async () => {
|
||||||
|
getTaskListFiltersSpy.and.returnValue(of([fakeGlobalFilter[0], { ...fakeGlobalFilter[1], showCounter: true }]));
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: { 'fake-involved-tasks': 4 }, batched: true }));
|
||||||
|
getTaskFilterCounterSpy.and.throwError('the query of the filter cannot be built');
|
||||||
|
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
|
expect(component.counters['fake-involved-tasks']).toBe(4);
|
||||||
|
expect(component.counters['fake-my-task1']).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the counters one filter at a time when the batched endpoint is not available', async () => {
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: false }));
|
||||||
|
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
|
expect(getTaskFilterCounterSpy).toHaveBeenCalled();
|
||||||
|
expect(component.counters['fake-involved-tasks']).toBe(11);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should refresh the counters of every filter when a filter is clicked', async () => {
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
|
component.onFilterClick(fakeGlobalFilter[0]);
|
||||||
|
|
||||||
|
expect(refreshFilterCountersSpy).toHaveBeenCalledWith('my-app-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should refresh the counter of the clicked filter alone when the batched endpoint is not available', async () => {
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: false }));
|
||||||
|
await bindAppName();
|
||||||
|
getTaskFilterCounterSpy.calls.reset();
|
||||||
|
|
||||||
|
component.onFilterClick(fakeGlobalFilter[0]);
|
||||||
|
|
||||||
|
expect(refreshFilterCountersSpy).not.toHaveBeenCalled();
|
||||||
|
expect(getTaskFilterCounterSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Highlight Selected Filter', () => {
|
describe('Highlight Selected Filter', () => {
|
||||||
|
|||||||
+112
-45
@@ -16,16 +16,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
|
import { Component, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
|
||||||
import { EMPTY, Observable } from 'rxjs';
|
import { combineLatest, defer, EMPTY, Observable, of, Subscription } from 'rxjs';
|
||||||
import { TaskFilterCloudService } from '../../services/task-filter-cloud.service';
|
import { TaskFilterCloudService } from '../../services/task-filter-cloud.service';
|
||||||
import { FilterParamsModel, TaskFilterCloudModel } from '../../models/filter-cloud.model';
|
import { FilterParamsModel, TaskFilterCloudModel } from '../../models/filter-cloud.model';
|
||||||
import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core';
|
import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core';
|
||||||
import { catchError, debounceTime, map, shareReplay, tap } from 'rxjs/operators';
|
import { catchError, map } from 'rxjs/operators';
|
||||||
import { BaseTaskFiltersCloudComponent } from '../base-task-filters-cloud.component';
|
import { BaseTaskFiltersCloudComponent } from '../base-task-filters-cloud.component';
|
||||||
import { TaskDetailsCloudModel } from '../../../models/task-details-cloud.model';
|
import { TaskDetailsCloudModel } from '../../../models/task-details-cloud.model';
|
||||||
import { TaskCloudEngineEvent } from '../../../../models/engine-event-cloud.model';
|
import { TaskCloudEngineEvent } from '../../../../models/engine-event-cloud.model';
|
||||||
import { TaskListCloudService } from '../../../task-list/services/task-list-cloud.service';
|
import { TaskListCloudService } from '../../../task-list/services/task-list-cloud.service';
|
||||||
import { TaskFilterCloudAdapter } from '../../../../models/filter-cloud-model';
|
import { TaskFilterCloudAdapter } from '../../../../models/filter-cloud-model';
|
||||||
|
import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service';
|
||||||
|
import { FilterCounterEntityType } from '../../../../models/filter-counters-cloud.model';
|
||||||
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { TranslatePipe } from '@ngx-translate/core';
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
@@ -42,10 +44,21 @@ import { AsyncPipe } from '@angular/common';
|
|||||||
export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent implements OnInit, OnChanges {
|
export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent implements OnInit, OnChanges {
|
||||||
protected readonly TASKS_ROUTE = '/task-list-cloud';
|
protected readonly TASKS_ROUTE = '/task-list-cloud';
|
||||||
|
|
||||||
/** (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the task count. */
|
/**
|
||||||
|
* (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the task count.
|
||||||
|
*
|
||||||
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
searchApiMethod: 'GET' | 'POST' = 'GET';
|
searchApiMethod: 'GET' | 'POST' = 'GET';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (optional) Resolves the counters of the task and the process filters with a single call to
|
||||||
|
* `POST /query/v1/count`. Both filter components have to
|
||||||
|
* ask for it, otherwise the counters are resolved one filter at a time.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
useBatchedCounters = false;
|
||||||
|
|
||||||
/** Emitted when a filter is being selected based on the filterParam input. */
|
/** Emitted when a filter is being selected based on the filterParam input. */
|
||||||
@Output()
|
@Output()
|
||||||
filterSelected = new EventEmitter<TaskFilterCloudModel>();
|
filterSelected = new EventEmitter<TaskFilterCloudModel>();
|
||||||
@@ -69,9 +82,13 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
|||||||
notificationDebounceTime = 3000;
|
notificationDebounceTime = 3000;
|
||||||
currentFiltersValues: { [key: string]: number } = {};
|
currentFiltersValues: { [key: string]: number } = {};
|
||||||
private filtersLoadedFor?: string;
|
private filtersLoadedFor?: string;
|
||||||
|
private countersSubscription?: Subscription;
|
||||||
|
private countersFilters$?: Observable<TaskFilterCloudModel[]>;
|
||||||
|
private batchedCounters = true;
|
||||||
|
|
||||||
private readonly taskFilterCloudService = inject(TaskFilterCloudService);
|
private readonly taskFilterCloudService = inject(TaskFilterCloudService);
|
||||||
private readonly taskListCloudService = inject(TaskListCloudService);
|
private readonly taskListCloudService = inject(TaskListCloudService);
|
||||||
|
private readonly filterCountersCloudService = inject(FilterCountersCloudService);
|
||||||
private readonly translationService = inject(TranslationService);
|
private readonly translationService = inject(TranslationService);
|
||||||
private readonly appConfigService = inject(AppConfigService);
|
private readonly appConfigService = inject(AppConfigService);
|
||||||
private readonly activatedRoute = inject(ActivatedRoute);
|
private readonly activatedRoute = inject(ActivatedRoute);
|
||||||
@@ -80,6 +97,7 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.enableNotifications = this.appConfigService.get('notifications', true);
|
this.enableNotifications = this.appConfigService.get('notifications', true);
|
||||||
this.notificationDebounceTime = this.appConfigService.get('notificationDebounceTime', 3000);
|
this.notificationDebounceTime = this.appConfigService.get('notificationDebounceTime', 3000);
|
||||||
|
|
||||||
if (!this.filtersLoadedFor) {
|
if (!this.filtersLoadedFor) {
|
||||||
this.getFilters(this.appName);
|
this.getFilters(this.appName);
|
||||||
}
|
}
|
||||||
@@ -94,6 +112,8 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
|||||||
this.getFilters(appName.currentValue);
|
this.getFilters(appName.currentValue);
|
||||||
} else if (filter && filter.currentValue !== filter.previousValue) {
|
} else if (filter && filter.currentValue !== filter.previousValue) {
|
||||||
this.selectFilterAndEmit(filter.currentValue);
|
this.selectFilterAndEmit(filter.currentValue);
|
||||||
|
} else if (changes['useBatchedCounters'] && !changes['useBatchedCounters'].firstChange && this.filtersLoadedFor) {
|
||||||
|
this.loadFilterCounters(this.filtersLoadedFor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,8 +124,8 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
|||||||
*/
|
*/
|
||||||
getFilters(appName: string): void {
|
getFilters(appName: string): void {
|
||||||
this.filtersLoadedFor = appName;
|
this.filtersLoadedFor = appName;
|
||||||
const filters$ = this.taskFilterCloudService.getTaskListFilters(appName).pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
const filters$ = this.filterCountersCloudService.getTaskFilters(appName);
|
||||||
this.filters$ = filters$.pipe(catchError(() => EMPTY));
|
this.filters$ = filters$.pipe(catchError(() => of([])));
|
||||||
|
|
||||||
filters$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
filters$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
@@ -113,13 +133,15 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
|||||||
this.filters = res || [];
|
this.filters = res || [];
|
||||||
this.initFilterCounters();
|
this.initFilterCounters();
|
||||||
this.selectFilterAndEmit(this.filterParam);
|
this.selectFilterAndEmit(this.filterParam);
|
||||||
this.updateFilterCounters();
|
|
||||||
this.success.emit(res);
|
this.success.emit(res);
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
this.error.emit(err);
|
this.error.emit(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.countersFilters$ = filters$;
|
||||||
|
this.loadFilterCounters(appName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -131,55 +153,47 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Iterate over filters and update counters
|
* Iterate over filters and update counters
|
||||||
|
*
|
||||||
|
* @deprecated counts one filter at a time. Removed in ADF 10.0.0.
|
||||||
*/
|
*/
|
||||||
updateFilterCounters(): void {
|
updateFilterCounters(): void {
|
||||||
this.filters.forEach((filter: TaskFilterCloudModel) => this.updateFilterCounter(filter));
|
this.filters.forEach((filter) => this.updateFilterCounter(filter));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current value for filter and check if value has changed
|
* Get current value for filter and check if value has changed
|
||||||
*
|
*
|
||||||
* @param filter filter
|
* @param filter filter
|
||||||
|
* @deprecated counts one filter at a time. Removed in ADF 10.0.0.
|
||||||
*/
|
*/
|
||||||
updateFilterCounter(filter: TaskFilterCloudModel): void {
|
updateFilterCounter(filter: TaskFilterCloudModel): void {
|
||||||
if (!filter?.showCounter) {
|
if (!filter?.showCounter) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.fetchTaskFilterCounter(filter)
|
|
||||||
|
defer(() => this.fetchTaskFilterCounter(filter))
|
||||||
.pipe(
|
.pipe(
|
||||||
tap((filterCounter) => {
|
catchError(() => EMPTY),
|
||||||
this.checkIfFilterValuesHasBeenUpdated(filter.key, filterCounter);
|
takeUntilDestroyed(this.destroyRef)
|
||||||
})
|
|
||||||
)
|
)
|
||||||
.subscribe((data) => {
|
.subscribe((counter) => {
|
||||||
this.counters = {
|
this.checkIfFilterValuesHasBeenUpdated(filter.key, counter);
|
||||||
...this.counters,
|
this.counters = { ...this.counters, [filter.key]: counter };
|
||||||
[filter.key]: data
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchTaskFilterCounter(filter: TaskFilterCloudModel): Observable<number> {
|
initFilterCounterNotifications(): void {
|
||||||
return this.searchApiMethod === 'POST'
|
|
||||||
? this.taskListCloudService.getTaskListCount(new TaskFilterCloudAdapter(filter))
|
|
||||||
: this.taskFilterCloudService.getTaskFilterCounter(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
initFilterCounterNotifications() {
|
|
||||||
if (!this.appName) {
|
if (!this.appName) {
|
||||||
return;
|
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();
|
if (this.enableNotifications) {
|
||||||
this.filterCounterUpdated.emit(result);
|
this.filterCountersCloudService
|
||||||
|
.getEngineEvents(this.appName, FilterCounterEntityType.TASK)
|
||||||
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
|
.subscribe((events) => {
|
||||||
|
events.forEach((taskEvent) => this.checkFilterCounter(taskEvent.entity));
|
||||||
|
this.filterCounterUpdated.emit(events);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.counters = {};
|
this.counters = {};
|
||||||
@@ -240,7 +254,7 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
|||||||
onFilterClick(filter: FilterParamsModel) {
|
onFilterClick(filter: FilterParamsModel) {
|
||||||
if (filter) {
|
if (filter) {
|
||||||
this.selectFilter(filter);
|
this.selectFilter(filter);
|
||||||
this.updateFilterCounter(this.currentFilter);
|
this.refreshFilterCounter(this.currentFilter);
|
||||||
this.filterClicked.emit(this.currentFilter);
|
this.filterClicked.emit(this.currentFilter);
|
||||||
this.updatedCountersSet.delete(filter.key);
|
this.updatedCountersSet.delete(filter.key);
|
||||||
} else {
|
} else {
|
||||||
@@ -267,17 +281,9 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
|||||||
return this.filters === undefined || (this.filters && this.filters.length === 0);
|
return this.filters === undefined || (this.filters && this.filters.length === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset the filters properties
|
|
||||||
*/
|
|
||||||
private resetFilter() {
|
|
||||||
this.filters = [];
|
|
||||||
this.currentFilter = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
checkIfFilterValuesHasBeenUpdated(filterKey: string, filterValue: number) {
|
checkIfFilterValuesHasBeenUpdated(filterKey: string, filterValue: number) {
|
||||||
if (this.currentFiltersValues[filterKey] === undefined || this.currentFiltersValues[filterKey] !== filterValue) {
|
if (this.currentFiltersValues[filterKey] === undefined || this.currentFiltersValues[filterKey] !== filterValue) {
|
||||||
this.currentFiltersValues[filterKey] = filterValue;
|
this.currentFiltersValues = { ...this.currentFiltersValues, [filterKey]: filterValue };
|
||||||
this.updatedFilter.emit(filterKey);
|
this.updatedFilter.emit(filterKey);
|
||||||
this.updatedCountersSet.add(filterKey);
|
this.updatedCountersSet.add(filterKey);
|
||||||
}
|
}
|
||||||
@@ -288,8 +294,69 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
getFilterKeysAfterExternalRefreshing(): void {
|
getFilterKeysAfterExternalRefreshing(): void {
|
||||||
this.taskFilterCloudService.filterKeyToBeRefreshed$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((filterKey: string) => {
|
this.taskFilterCloudService.filterKeyToBeRefreshed$
|
||||||
this.updatedCountersSet.delete(filterKey);
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
|
.subscribe((filterKey: string) => this.updatedCountersSet.delete(filterKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadFilterCounters(appName: string): void {
|
||||||
|
if (!this.countersFilters$) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.countersSubscription?.unsubscribe();
|
||||||
|
this.countersSubscription = combineLatest([
|
||||||
|
this.countersFilters$.pipe(catchError(() => of([]))),
|
||||||
|
this.filterCountersCloudService.getFilterCounters(appName, FilterCounterEntityType.TASK, this.useBatchedCounters)
|
||||||
|
])
|
||||||
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
|
.subscribe(([, { counters, batched }]) => {
|
||||||
|
this.batchedCounters = batched;
|
||||||
|
if (batched) {
|
||||||
|
this.applyFilterCounters(counters);
|
||||||
|
} else {
|
||||||
|
this.updateFilterCounters();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyFilterCounters(counters: { [filterKey: string]: number }): void {
|
||||||
|
this.filters.forEach((filter) => {
|
||||||
|
const filterKey = filter?.showCounter ? filter.key : undefined;
|
||||||
|
if (!filterKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const counter = counters[filterKey];
|
||||||
|
if (counter === undefined) {
|
||||||
|
this.updateFilterCounter(filter);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkIfFilterValuesHasBeenUpdated(filterKey, counter);
|
||||||
|
this.counters = { ...this.counters, [filterKey]: counter };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fetchTaskFilterCounter(filter: TaskFilterCloudModel): Observable<number> {
|
||||||
|
return this.searchApiMethod === 'POST'
|
||||||
|
? this.taskListCloudService.getTaskListCount(new TaskFilterCloudAdapter(filter))
|
||||||
|
: this.taskFilterCloudService.getTaskFilterCounter(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the filters properties
|
||||||
|
*/
|
||||||
|
private resetFilter() {
|
||||||
|
this.filters = [];
|
||||||
|
this.currentFilter = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshFilterCounter(filter: TaskFilterCloudModel): void {
|
||||||
|
if (this.batchedCounters) {
|
||||||
|
this.filterCountersCloudService.refreshFilterCounters(this.appName);
|
||||||
|
} else {
|
||||||
|
this.updateFilterCounter(filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -361,6 +361,11 @@ export class TaskFilterCloudService extends BaseCloudService {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use FilterCountersCloudService.getEngineEvents instead.
|
||||||
|
* @param appName Name of the target app
|
||||||
|
* @returns Task engine events
|
||||||
|
*/
|
||||||
getTaskNotificationSubscription(appName: string): Observable<TaskCloudEngineEvent[]> {
|
getTaskNotificationSubscription(appName: string): Observable<TaskCloudEngineEvent[]> {
|
||||||
return this.notificationCloudService
|
return this.notificationCloudService
|
||||||
.makeGQLQuery(appName, TASK_EVENT_SUBSCRIPTION_QUERY)
|
.makeGQLQuery(appName, TASK_EVENT_SUBSCRIPTION_QUERY)
|
||||||
|
|||||||
+1
-1
@@ -135,7 +135,7 @@ export class TaskListCloudService extends BaseCloudService implements TaskListCl
|
|||||||
return this.post<object, number>(queryUrl, queryData).pipe(map((response) => response || 0));
|
return this.post<object, number>(queryUrl, queryData).pipe(map((response) => response || 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected buildQueryData(requestNode: TaskListRequestModel) {
|
buildQueryData(requestNode: TaskListRequestModel) {
|
||||||
const queryData: any = {
|
const queryData: any = {
|
||||||
id: requestNode.id,
|
id: requestNode.id,
|
||||||
parentId: requestNode.parentId,
|
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/engine-event-cloud.model';
|
||||||
export * from './lib/models/task-cloud.model';
|
export * from './lib/models/task-cloud.model';
|
||||||
export * from './lib/models/filter-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/task-list-sorting.model';
|
||||||
export * from './lib/models/process-instance-variable.model';
|
export * from './lib/models/process-instance-variable.model';
|
||||||
export * from './lib/models/variable-definition';
|
export * from './lib/models/variable-definition';
|
||||||
|
|||||||
Reference in New Issue
Block a user