mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
AAE-49653 Removing the non-batched counter fallback
This commit is contained in:
@@ -27,7 +27,6 @@ Lists all available process filters and allows to select a filter.
|
||||
| appName | `string` | "" | (required) The application name |
|
||||
| filterParam | `UserTaskFilterRepresentation` | | (optional) The filter to be selected by default |
|
||||
| showIcons | `boolean` | false | (optional) Toggles showing an icon by the side of each filter |
|
||||
| searchApiMethod | `'GET' \| 'POST'` | "GET" | **Deprecated:** the counters are resolved by a single `POST /query/v1/count` call, which requires Activiti 8.7.0 forward. This input is only used to resolve the counters one filter at a time, on the backends without that endpoint, and will be removed along with the `GET` method in ADF 10.0.0. |
|
||||
|
||||
### Events
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ Shows all available filters.
|
||||
| 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. |
|
||||
| showIcons | `boolean` | false | Toggles display of the filter's icons. |
|
||||
| searchApiMethod | `'GET' \| 'POST'` | "GET" | **Deprecated:** the counters are resolved by a single `POST /query/v1/count` call, which requires Activiti 8.7.0 forward. This input is only used to resolve the counters one filter at a time, on the backends without that endpoint, and will be removed along with the `GET` method in ADF 10.0.0. |
|
||||
|
||||
### Events
|
||||
|
||||
|
||||
@@ -31,10 +31,6 @@ Manages task filters.
|
||||
- _appName:_ `string` - Name of the target app
|
||||
- _id:_ `string` - ID of the task
|
||||
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`TaskFilterCloudModel`](../../../lib/process-services-cloud/src/lib/task/task-filters/models/filter-cloud.model.ts)`>` - Details of the task filter
|
||||
- **getTaskFilterCounter**(taskFilter: [`TaskFilterCloudModel`](../../../lib/process-services-cloud/src/lib/task/task-filters/models/filter-cloud.model.ts)): [`Observable`](http://reactivex.io/documentation/observable.html)`<any>`<br/>
|
||||
Finds a task using an object with optional query properties.
|
||||
- _taskFilter:_ [`TaskFilterCloudModel`](../../../lib/process-services-cloud/src/lib/task/task-filters/models/filter-cloud.model.ts) -
|
||||
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<any>` - Task information
|
||||
- **getTaskListFilters**(appName?: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`TaskFilterCloudModel`](../../../lib/process-services-cloud/src/lib/task/task-filters/models/filter-cloud.model.ts)`[]>`<br/>
|
||||
Gets all task filters for a task app.
|
||||
- _appName:_ `string` - (Optional) Name of the target app
|
||||
|
||||
@@ -62,9 +62,7 @@ export type FilterCounters = {
|
||||
[entityType in FilterCounterEntityType]?: { [requestId: string]: number };
|
||||
};
|
||||
|
||||
/** Counters of the filters of one entity type, keyed by filter key. */
|
||||
export interface FilterCountersResult {
|
||||
/** Counters keyed by filter key. Empty when the batched count endpoint is not available. */
|
||||
counters: { [filterKey: string]: number };
|
||||
/** When `false`, the backend holds no batched count endpoint: count one filter at a time. */
|
||||
batched: boolean;
|
||||
[filterKey: string]: number;
|
||||
}
|
||||
|
||||
+58
-517
@@ -25,7 +25,6 @@ import { PROCESS_FILTERS_SERVICE_TOKEN, TASK_FILTERS_SERVICE_TOKEN } from '../..
|
||||
import { LocalPreferenceCloudService } from '../../../../services/local-preference-cloud.service';
|
||||
import { mockProcessFilters } from '../../mock/process-filters-cloud.mock';
|
||||
import { AppConfigService, AppConfigServiceMock, NoopAuthModule } from '@alfresco/adf-core';
|
||||
import { ProcessListCloudService } from '../../../process-list/services/process-list-cloud.service';
|
||||
import { ApolloTestingModule } from 'apollo-angular/testing';
|
||||
import { HarnessLoader } from '@angular/cdk/testing';
|
||||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
@@ -47,24 +46,21 @@ const ProcessFilterCloudServiceMock = {
|
||||
describe('ProcessFiltersCloudComponent', () => {
|
||||
let processFilterService: ProcessFilterCloudService;
|
||||
let filterCountersService: FilterCountersCloudService;
|
||||
let processListService: ProcessListCloudService;
|
||||
let component: ProcessFiltersCloudComponent;
|
||||
let fixture: ComponentFixture<ProcessFiltersCloudComponent>;
|
||||
let getProcessFiltersSpy: jasmine.Spy;
|
||||
let getFilterCountersSpy: jasmine.Spy;
|
||||
let refreshFilterCountersSpy: jasmine.Spy;
|
||||
let getProcessCounterSpy: jasmine.Spy;
|
||||
let loader: HarnessLoader;
|
||||
let router: Router;
|
||||
|
||||
const configureTestingModule = async (searchApiMethod: 'GET' | 'POST') => {
|
||||
const configureTestingModule = async () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAuthModule, ProcessFiltersCloudComponent, ApolloTestingModule],
|
||||
providers: [
|
||||
{ provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService },
|
||||
{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService },
|
||||
{ provide: AppConfigService, useClass: AppConfigServiceMock },
|
||||
ProcessListCloudService,
|
||||
{ provide: ProcessFilterCloudService, useValue: ProcessFilterCloudServiceMock },
|
||||
provideRouter([{ path: 'process-list-cloud', component: DummyComponent }]),
|
||||
{
|
||||
@@ -85,19 +81,15 @@ describe('ProcessFiltersCloudComponent', () => {
|
||||
fixture = TestBed.createComponent(ProcessFiltersCloudComponent);
|
||||
loader = TestbedHarnessEnvironment.loader(fixture);
|
||||
component = fixture.componentInstance;
|
||||
component.searchApiMethod = searchApiMethod;
|
||||
|
||||
processFilterService = TestBed.inject(ProcessFilterCloudService);
|
||||
filterCountersService = TestBed.inject(FilterCountersCloudService);
|
||||
processListService = TestBed.inject(ProcessListCloudService);
|
||||
TestBed.inject(ActivatedRoute);
|
||||
router = TestBed.inject(Router);
|
||||
await RouterTestingHarness.create();
|
||||
getProcessFiltersSpy = spyOn(filterCountersService, 'getProcessFilters').and.returnValue(of(mockProcessFilters));
|
||||
getFilterCountersSpy = spyOn(filterCountersService, 'getFilterCounters').and.returnValue(of({ counters: {}, batched: true }));
|
||||
getFilterCountersSpy = spyOn(filterCountersService, 'getFilterCounters').and.returnValue(of({}));
|
||||
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') => {
|
||||
@@ -110,9 +102,9 @@ describe('ProcessFiltersCloudComponent', () => {
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
describe('searchApiMethod set to GET', () => {
|
||||
describe('filters on screen', () => {
|
||||
beforeEach(async () => {
|
||||
await configureTestingModule('GET');
|
||||
await configureTestingModule();
|
||||
});
|
||||
|
||||
it('should attach specific icon for each filter if hasIcon is true', async () => {
|
||||
@@ -252,88 +244,9 @@ describe('ProcessFiltersCloudComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('searchApiMethod set to POST', () => {
|
||||
describe('filter selection and counters', () => {
|
||||
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');
|
||||
await configureTestingModule();
|
||||
});
|
||||
|
||||
it('should emit an error with a bad response', async () => {
|
||||
@@ -468,16 +381,16 @@ describe('ProcessFiltersCloudComponent', () => {
|
||||
expect(component.updatedFiltersSet.has(filterKeyTest)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should resolve the counter only of the filters with a counter enabled', () => {
|
||||
it('should hold the counter only of the filters with a counter enabled', async () => {
|
||||
const filterWithCounter = new ProcessFilterCloudModel({ ...mockProcessFilters[1], showCounter: true });
|
||||
const filterWithoutCounter = new ProcessFilterCloudModel({ ...mockProcessFilters[2], showCounter: false });
|
||||
getProcessCounterSpy.calls.reset();
|
||||
getProcessFiltersSpy.and.returnValue(of([filterWithCounter, filterWithoutCounter]));
|
||||
getFilterCountersSpy.and.returnValue(of({ [filterWithCounter.key]: 7, [filterWithoutCounter.key]: 9 }));
|
||||
|
||||
component.filters = [filterWithCounter, filterWithoutCounter];
|
||||
component.updateFilterCounters();
|
||||
await bindAppName('mock-app-name');
|
||||
|
||||
expect(getProcessCounterSpy).toHaveBeenCalledTimes(1);
|
||||
expect(getProcessCounterSpy).toHaveBeenCalledWith(filterWithCounter.appName, filterWithCounter.status);
|
||||
expect(component.counters[filterWithCounter.key]).toBe(7);
|
||||
expect(component.counters[filterWithoutCounter.key]).toBe(0);
|
||||
});
|
||||
|
||||
describe('Batched counters', () => {
|
||||
@@ -494,7 +407,7 @@ describe('ProcessFiltersCloudComponent', () => {
|
||||
});
|
||||
|
||||
it('should hold the counters resolved by the batched count request', async () => {
|
||||
getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true }));
|
||||
getFilterCountersSpy.and.returnValue(of({ FakeRunningProcesses: 9 }));
|
||||
|
||||
await bindAppName('mock-app-name');
|
||||
|
||||
@@ -502,7 +415,7 @@ describe('ProcessFiltersCloudComponent', () => {
|
||||
});
|
||||
|
||||
it('should emit the filters whose counter changed', async () => {
|
||||
getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true }));
|
||||
getFilterCountersSpy.and.returnValue(of({ FakeRunningProcesses: 9 }));
|
||||
const updatedFilterSpy = spyOn(component.updatedFilter, 'emit');
|
||||
|
||||
await bindAppName('mock-app-name');
|
||||
@@ -510,27 +423,19 @@ describe('ProcessFiltersCloudComponent', () => {
|
||||
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 }));
|
||||
|
||||
it('should keep the counter of a filter the request left out', async () => {
|
||||
getFilterCountersSpy.and.returnValue(of({ FakeRunningProcesses: 9 }));
|
||||
await bindAppName('mock-app-name');
|
||||
expect(component.counters['FakeRunningProcesses']).toBe(9);
|
||||
|
||||
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 }));
|
||||
|
||||
getFilterCountersSpy.and.returnValue(of({}));
|
||||
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');
|
||||
getFilterCountersSpy.and.returnValue(of({ FakeRunningProcesses: 9 }));
|
||||
|
||||
await bindAppName('mock-app-name');
|
||||
|
||||
@@ -586,415 +491,14 @@ describe('ProcessFiltersCloudComponent', () => {
|
||||
fixture.detectChanges();
|
||||
component.filters = mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true }));
|
||||
|
||||
counters$.next({ counters: { FakeRunningProcesses: 7 }, batched: true });
|
||||
counters$.next({ FakeRunningProcesses: 7 });
|
||||
|
||||
expect(component.counters['FakeRunningProcesses']).toBe(7);
|
||||
flush();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('Highlight Selected Filter', () => {
|
||||
const allProcessesFilterKey = mockProcessFilters[0].key;
|
||||
const allProcessesFilterId = mockProcessFilters[0].id;
|
||||
|
||||
it('should apply active CSS class on filter click', async () => {
|
||||
component.enableNotifications = true;
|
||||
await bindAppName('mock-app-name');
|
||||
|
||||
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 add aria-current attribute with value "page" to the active filter', async () => {
|
||||
component.enableNotifications = true;
|
||||
await bindAppName('mock-app-name');
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
describe('Counter updates', () => {
|
||||
it('should emit filter key when filter counter is set for first time', () => {
|
||||
component.currentFiltersValues = {};
|
||||
const fakeFilterKey = 'testKey';
|
||||
@@ -1052,5 +556,42 @@ describe('ProcessFiltersCloudComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Highlight Selected Filter', () => {
|
||||
const allProcessesFilterKey = mockProcessFilters[0].key;
|
||||
const allProcessesFilterId = mockProcessFilters[0].id;
|
||||
|
||||
it('should apply active CSS class on filter click', async () => {
|
||||
component.enableNotifications = true;
|
||||
await bindAppName('mock-app-name');
|
||||
|
||||
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 add aria-current attribute with value "page" to the active filter', async () => {
|
||||
component.enableNotifications = true;
|
||||
await bindAppName('mock-app-name');
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+6
-84
@@ -16,16 +16,14 @@
|
||||
*/
|
||||
|
||||
import { Component, DestroyRef, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
|
||||
import { combineLatest, defer, EMPTY, Observable, of, Subscription } from 'rxjs';
|
||||
import { combineLatest, EMPTY, Observable, of, Subscription } from 'rxjs';
|
||||
import { ProcessFilterCloudService } from '../../services/process-filter-cloud.service';
|
||||
import { ProcessFilterCloudModel } from '../../models/process-filter-cloud.model';
|
||||
import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core';
|
||||
import { FilterParamsModel } from '../../../../task/task-filters/models/filter-cloud.model';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { ProcessListCloudService } from '../../../process-list/services/process-list-cloud.service';
|
||||
import { ProcessFilterCloudAdapter } from '../../../process-list/models/process-cloud-query-request.model';
|
||||
import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service';
|
||||
import { FilterCounterEntityType } from '../../../../models/filter-counters-cloud.model';
|
||||
import { FilterCounterEntityType, FilterCountersResult } from '../../../../models/filter-counters-cloud.model';
|
||||
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
@@ -45,15 +43,6 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
||||
@Input()
|
||||
appName: string = '';
|
||||
|
||||
/**
|
||||
* (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the process count.
|
||||
*
|
||||
* @deprecated only used by the backends without `POST /query/v1/count`. It will be removed,
|
||||
* along with the 'GET' method, in ADF 10.0.0.
|
||||
*/
|
||||
@Input()
|
||||
searchApiMethod: 'GET' | 'POST' = 'GET';
|
||||
|
||||
/** (optional) The filter to be selected by default */
|
||||
@Input()
|
||||
filterParam: FilterParamsModel;
|
||||
@@ -92,13 +81,11 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
||||
notificationDebounceTime = 3000;
|
||||
private filtersLoadedFor?: string;
|
||||
private countersSubscription?: Subscription;
|
||||
private batchedCounters = true;
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly processFilterCloudService = inject(ProcessFilterCloudService);
|
||||
private readonly translationService = inject(TranslationService);
|
||||
private readonly appConfigService = inject(AppConfigService);
|
||||
private readonly processListCloudService = inject(ProcessListCloudService);
|
||||
private readonly filterCountersCloudService = inject(FilterCountersCloudService);
|
||||
private readonly activatedRoute = inject(ActivatedRoute);
|
||||
protected readonly currentRouteFilterId = toSignal(this.activatedRoute.queryParamMap.pipe(map((params) => params.get('filterId'))));
|
||||
@@ -215,7 +202,7 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
||||
if (filter) {
|
||||
this.selectFilter(filter);
|
||||
this.filterClicked.emit(this.currentFilter);
|
||||
this.refreshFilterCounter(this.currentFilter);
|
||||
this.filterCountersCloudService.refreshFilterCounters(this.appName);
|
||||
this.updatedFiltersSet.delete(filter.key);
|
||||
} else {
|
||||
this.currentFilter = undefined;
|
||||
@@ -253,49 +240,6 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
||||
return this.currentFilter.name === filter.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated `getFilterCounters` keeps the counters in sync with the engine events, so nothing is
|
||||
* subscribed here anymore. It will be removed in ADF 10.0.0.
|
||||
*/
|
||||
initProcessNotification(): void {
|
||||
/* Kept for backwards compatibility. */
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over filters and update counters
|
||||
*
|
||||
* @deprecated resolves the counters one filter at a time, for the backends without the batched
|
||||
* count endpoint. It will be 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 resolves the counter of one filter, for the backends without the batched count
|
||||
* endpoint. It will be removed in ADF 10.0.0.
|
||||
*/
|
||||
updateFilterCounter(filter: ProcessFilterCloudModel): void {
|
||||
const filterKey = filter?.showCounter ? filter.key : undefined;
|
||||
if (!filterKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Building the query throws for a malformed filter: `defer` turns that into a stream error to catch. */
|
||||
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 };
|
||||
@@ -345,17 +289,10 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
||||
this.filterCountersCloudService.getFilterCounters(appName, FilterCounterEntityType.PROCESS_INSTANCE)
|
||||
])
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(([, { counters, batched }]) => {
|
||||
this.batchedCounters = batched;
|
||||
if (batched) {
|
||||
this.applyFilterCounters(counters);
|
||||
} else {
|
||||
this.updateFilterCounters();
|
||||
}
|
||||
});
|
||||
.subscribe(([, counters]) => this.applyFilterCounters(counters));
|
||||
}
|
||||
|
||||
private applyFilterCounters(counters: { [filterKey: string]: number }): void {
|
||||
private applyFilterCounters(counters: FilterCountersResult): void {
|
||||
this.filters.forEach((filter) => {
|
||||
/* A filter without a key holds no request id. */
|
||||
const filterKey = filter?.showCounter ? filter.key : undefined;
|
||||
@@ -364,9 +301,8 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
const counter = counters[filterKey];
|
||||
/* A filter the request left out keeps the counter it holds, rather than showing a wrong one. */
|
||||
if (counter === undefined) {
|
||||
/* Left out of the batch: counted on its own. */
|
||||
this.updateFilterCounter(filter);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -374,18 +310,4 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
||||
this.counters = { ...this.counters, [filterKey]: counter };
|
||||
});
|
||||
}
|
||||
|
||||
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> {
|
||||
return this.searchApiMethod === 'POST'
|
||||
? this.processListCloudService.getProcessListCount(new ProcessFilterCloudAdapter(filter))
|
||||
: this.processListCloudService.getProcessCounter(filter.appName, filter.status);
|
||||
}
|
||||
}
|
||||
|
||||
-36
@@ -401,40 +401,4 @@ describe('ProcessListCloudService', () => {
|
||||
expect(requestBodyParams.variableKeys[1]).toBe('test-two');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProcessListCount', () => {
|
||||
it('should concat the app name to the request url', async () => {
|
||||
const taskRequest = {
|
||||
appName: 'fakeName'
|
||||
} as ProcessListRequestModel;
|
||||
requestSpy.and.callFake(returnCallUrl);
|
||||
|
||||
const res = await firstValueFrom(service.getProcessListCount(taskRequest));
|
||||
|
||||
expect(res).toBeDefined();
|
||||
expect(res).not.toBeNull();
|
||||
expect(res).toContain('fakeName/query/v1/process-instances/count');
|
||||
});
|
||||
|
||||
it('should return 0 if response is falsy for getProcessListCount', async () => {
|
||||
const taskRequest = {
|
||||
appName: 'fakeName',
|
||||
pagination: { skipCount: 0, maxItems: 20 }
|
||||
} as ProcessListRequestModel;
|
||||
requestSpy.and.callFake(() => Promise.resolve(null));
|
||||
|
||||
const res = await firstValueFrom(service.getProcessListCount(taskRequest));
|
||||
|
||||
expect(res).toBe(0);
|
||||
});
|
||||
|
||||
it('should throw error if appName is not configured in getProcessListCount', async () => {
|
||||
const taskRequest = { appName: null } as ProcessListRequestModel;
|
||||
requestSpy.and.callFake(returnCallUrl);
|
||||
|
||||
const res = await firstValueFrom(service.getProcessListCount(taskRequest).pipe(catchError((error) => of(error.message))));
|
||||
|
||||
expect(res).toBe('Appname not configured');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
-53
@@ -174,48 +174,6 @@ export class ProcessListCloudService extends BaseCloudService {
|
||||
return this.fetchProcessList(requestNode).pipe(map((processes) => processes.list.pagination.totalItems));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a process using an object with optional query properties.
|
||||
*
|
||||
* @param appName app name
|
||||
* @param status filter status
|
||||
* @returns Total items
|
||||
*/
|
||||
getProcessCounter(appName: string, status: string): Observable<any> {
|
||||
const callback = (url: string, queryParams: any) => this.get(url, queryParams);
|
||||
let queryUrl: string;
|
||||
const defaultQueryUrl = 'query/v1/process-instances';
|
||||
const requestNode: ProcessQueryCloudRequestModel = {
|
||||
appName,
|
||||
appVersion: '',
|
||||
initiator: null,
|
||||
id: '',
|
||||
name: null,
|
||||
processDefinitionId: '',
|
||||
processDefinitionName: null,
|
||||
processDefinitionKey: '',
|
||||
status,
|
||||
businessKey: '',
|
||||
startFrom: null,
|
||||
startTo: null,
|
||||
completedFrom: null,
|
||||
completedTo: null,
|
||||
suspendedFrom: null,
|
||||
suspendedTo: null,
|
||||
completedDate: '',
|
||||
maxItems: 1,
|
||||
skipCount: 0,
|
||||
sorting: [
|
||||
{
|
||||
orderBy: 'startDate',
|
||||
direction: 'DESC'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return this.getProcess(callback, defaultQueryUrl, requestNode, queryUrl).pipe(map((tasks) => tasks?.list?.pagination?.totalItems));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a process using an object with optional query properties in admin app.
|
||||
*
|
||||
@@ -239,17 +197,6 @@ export class ProcessListCloudService extends BaseCloudService {
|
||||
return this.getProcess(callback, defaultQueryUrl, requestNode, queryUrl);
|
||||
}
|
||||
|
||||
getProcessListCount(requestNode: ProcessListRequestModel): Observable<number> {
|
||||
if (!requestNode?.appName) {
|
||||
return throwError(() => new Error('Appname not configured'));
|
||||
}
|
||||
|
||||
const queryUrl = `${this.getBasePath(requestNode.appName)}/query/v1/process-instances/count`;
|
||||
const queryData = this.buildQueryData(requestNode);
|
||||
|
||||
return this.post<object, number>(queryUrl, queryData).pipe(map((response) => response || 0));
|
||||
}
|
||||
|
||||
private getVariableKeysFromQueryParams(queryParams: any): string[] {
|
||||
if (!queryParams['variableKeys'] || queryParams['variableKeys'].length <= 0) {
|
||||
return [];
|
||||
|
||||
@@ -171,10 +171,7 @@ describe('FilterCountersCloudService', () => {
|
||||
});
|
||||
|
||||
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(await bothCounters()).toEqual([{ 'my-tasks': 5, 'queued-tasks': 0 }, { 'running-processes': 5 }]);
|
||||
|
||||
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -238,7 +235,7 @@ describe('FilterCountersCloudService', () => {
|
||||
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(await processCounters()).toEqual({});
|
||||
expect(postSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -255,40 +252,23 @@ describe('FilterCountersCloudService', () => {
|
||||
getTaskListFiltersSpy.and.returnValue(of([]));
|
||||
getProcessFiltersSpy.and.returnValue(of([]));
|
||||
|
||||
expect(await taskCounters()).toEqual({ counters: {}, batched: true });
|
||||
expect(await taskCounters()).toEqual({});
|
||||
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 () => {
|
||||
describe('when the count request fails', () => {
|
||||
it('should resolve no counter, rather than breaking the stream', async () => {
|
||||
postSpy.and.returnValue(throwError(() => ({ status: 500 })));
|
||||
expect(await taskCounters()).toEqual({ counters: {}, batched: false });
|
||||
|
||||
expect(await taskCounters()).toEqual({});
|
||||
});
|
||||
|
||||
it('should keep calling the endpoint afterwards', async () => {
|
||||
postSpy.and.returnValue(throwError(() => ({ status: 404 })));
|
||||
expect(await taskCounters()).toEqual({});
|
||||
|
||||
postSpy.and.returnValue(of(countersMock));
|
||||
expect(await taskCounters()).toEqual({ counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true });
|
||||
expect(await taskCounters()).toEqual({ 'my-tasks': 5, 'queued-tasks': 0 });
|
||||
expect(postSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -441,7 +421,7 @@ describe('FilterCountersCloudService', () => {
|
||||
|
||||
const result = await firstValueFrom(tasksOnlyService.getFilterCounters('mock-app', FilterCounterEntityType.TASK));
|
||||
|
||||
expect(result).toEqual({ counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true });
|
||||
expect(result).toEqual({ 'my-tasks': 5, 'queued-tasks': 0 });
|
||||
});
|
||||
|
||||
it('should leave the filters of the family that is not wired out of the request', async () => {
|
||||
@@ -565,7 +545,7 @@ describe('FilterCountersCloudService', () => {
|
||||
tick(3000);
|
||||
|
||||
expect(results.length).toBe(2);
|
||||
expect(results[1]).toEqual({ counters: { 'my-tasks': 9 }, batched: true });
|
||||
expect(results[1]).toEqual({ 'my-tasks': 9 });
|
||||
}));
|
||||
|
||||
it('should not subscribe to the events of an entity type that is not on screen', fakeAsync(() => {
|
||||
|
||||
@@ -38,8 +38,6 @@ import {
|
||||
FilterCountersResult
|
||||
} from '../models/filter-counters-cloud.model';
|
||||
|
||||
const BATCHED_COUNTERS_UNAVAILABLE_STATUSES = [404, 501];
|
||||
|
||||
interface FilterCountersFilters {
|
||||
[FilterCounterEntityType.TASK]: TaskFilterCloudModel[];
|
||||
[FilterCounterEntityType.PROCESS_INSTANCE]: ProcessFilterCloudModel[];
|
||||
@@ -102,10 +100,9 @@ export class FilterCountersCloudService extends BaseCloudService {
|
||||
private readonly activeEntityTypesPerApp = new Map<string, Set<FilterCounterEntityType>>();
|
||||
private readonly subscribersPerEntityType = new Map<string, number>();
|
||||
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 }>>();
|
||||
private readonly countersPerApp = new Map<string, Observable<FilterCounters>>();
|
||||
|
||||
get notificationDebounceTime(): number {
|
||||
return this.appConfigService.get('notificationDebounceTime', 3000);
|
||||
@@ -149,7 +146,7 @@ export class FilterCountersCloudService extends BaseCloudService {
|
||||
|
||||
return this.getCounters(appName);
|
||||
}).pipe(
|
||||
map(({ counters, batched }) => ({ counters: counters[entityType] ?? {}, batched })),
|
||||
map((counters) => counters[entityType] ?? {}),
|
||||
finalize(() => this.deactivateEntityType(appName, entityType))
|
||||
);
|
||||
}
|
||||
@@ -291,7 +288,7 @@ export class FilterCountersCloudService extends BaseCloudService {
|
||||
return filters$;
|
||||
}
|
||||
|
||||
private getCounters(appName: string): Observable<{ counters: FilterCounters; batched: boolean }> {
|
||||
private getCounters(appName: string): Observable<FilterCounters> {
|
||||
let counters$ = this.countersPerApp.get(appName);
|
||||
if (!counters$) {
|
||||
counters$ = this.recounts(appName).pipe(
|
||||
@@ -304,22 +301,12 @@ export class FilterCountersCloudService extends BaseCloudService {
|
||||
return counters$;
|
||||
}
|
||||
|
||||
private resolveCounters(appName: string): Observable<{ counters: FilterCounters; batched: boolean }> {
|
||||
if (this.appsWithoutBatchedCounters.has(appName)) {
|
||||
return of({ counters: {}, batched: false });
|
||||
}
|
||||
|
||||
private resolveCounters(appName: string): Observable<FilterCounters> {
|
||||
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 });
|
||||
})
|
||||
/* A failed count leaves the counters as they are, rather than breaking the stream. */
|
||||
catchError(() => of({}))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -382,7 +369,7 @@ export class FilterCountersCloudService extends BaseCloudService {
|
||||
try {
|
||||
return { ...buildQuery(filter), requestId: filter.key as string };
|
||||
} catch {
|
||||
/* Left out of the batch and counted on its own. */
|
||||
/* A malformed filter is left without a counter, so the others still hold one. */
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
|
||||
+22
-176
@@ -25,11 +25,8 @@ import { LocalPreferenceCloudService } from '../../../../services/local-preferen
|
||||
import { defaultTaskFiltersMock, fakeGlobalFilter, taskNotifications } from '../../mock/task-filters-cloud.mock';
|
||||
import { TaskFilterCloudService } from '../../services/task-filter-cloud.service';
|
||||
import { TaskFiltersCloudComponent } from './task-filters-cloud.component';
|
||||
import { TaskListCloudService } from '../../../task-list/services/task-list-cloud.service';
|
||||
import { HarnessLoader } from '@angular/cdk/testing';
|
||||
import { MatNavListItemHarness } from '@angular/material/list/testing';
|
||||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
import { TaskFilterCloudAdapter } from '../../../../models/filter-cloud-model';
|
||||
import { ApolloTestingModule } from 'apollo-angular/testing';
|
||||
import { TaskFilterCloudModel } from '../../models/filter-cloud.model';
|
||||
import { MatIconHarness } from '@angular/material/icon/testing';
|
||||
@@ -45,21 +42,18 @@ class DummyComponent {}
|
||||
describe('TaskFiltersCloudComponent', () => {
|
||||
let loader: HarnessLoader;
|
||||
let taskFilterService: TaskFilterCloudService;
|
||||
let taskListService: TaskListCloudService;
|
||||
let appConfigService: AppConfigService;
|
||||
|
||||
let component: TaskFiltersCloudComponent;
|
||||
let fixture: ComponentFixture<TaskFiltersCloudComponent>;
|
||||
let getTaskFilterCounterSpy: jasmine.Spy;
|
||||
let getTaskListFiltersSpy: jasmine.Spy;
|
||||
let getTaskListCountSpy: jasmine.Spy;
|
||||
let getEngineEventsSpy: jasmine.Spy;
|
||||
let filterCountersService: FilterCountersCloudService;
|
||||
let getFilterCountersSpy: jasmine.Spy;
|
||||
let refreshFilterCountersSpy: jasmine.Spy;
|
||||
let router: Router;
|
||||
|
||||
const configureTestingModule = async (searchApiMethod: 'GET' | 'POST') => {
|
||||
const configureTestingModule = async () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAuthModule, TaskFiltersCloudComponent, ApolloTestingModule],
|
||||
providers: [
|
||||
@@ -82,15 +76,10 @@ describe('TaskFiltersCloudComponent', () => {
|
||||
]
|
||||
});
|
||||
taskFilterService = TestBed.inject(TaskFilterCloudService);
|
||||
taskListService = TestBed.inject(TaskListCloudService);
|
||||
filterCountersService = TestBed.inject(FilterCountersCloudService);
|
||||
getTaskFilterCounterSpy = spyOn(taskFilterService, 'getTaskFilterCounter').and.returnValue(of(11));
|
||||
getTaskListCountSpy = spyOn(taskListService, 'getTaskListCount').and.returnValue(of(11));
|
||||
getEngineEventsSpy = spyOn(filterCountersService, 'getEngineEvents').and.returnValue(of(taskNotifications));
|
||||
getTaskListFiltersSpy = spyOn(filterCountersService, 'getTaskFilters').and.returnValue(of(fakeGlobalFilter));
|
||||
getFilterCountersSpy = spyOn(filterCountersService, 'getFilterCounters').and.returnValue(
|
||||
of({ counters: { 'fake-involved-tasks': 11 }, batched: true })
|
||||
);
|
||||
getFilterCountersSpy = spyOn(filterCountersService, 'getFilterCounters').and.returnValue(of({ 'fake-involved-tasks': 11 }));
|
||||
refreshFilterCountersSpy = spyOn(filterCountersService, 'refreshFilterCounters');
|
||||
|
||||
appConfigService = TestBed.inject(AppConfigService);
|
||||
@@ -99,7 +88,6 @@ describe('TaskFiltersCloudComponent', () => {
|
||||
component = fixture.componentInstance;
|
||||
loader = TestbedHarnessEnvironment.loader(fixture);
|
||||
|
||||
component.searchApiMethod = searchApiMethod;
|
||||
TestBed.inject(ActivatedRoute);
|
||||
router = TestBed.inject(Router);
|
||||
await RouterTestingHarness.create();
|
||||
@@ -115,9 +103,9 @@ describe('TaskFiltersCloudComponent', () => {
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
describe('searchApiMethod set to GET', () => {
|
||||
describe('filters on screen', () => {
|
||||
beforeEach(async () => {
|
||||
await configureTestingModule('GET');
|
||||
await configureTestingModule();
|
||||
});
|
||||
|
||||
it('should attach specific icon for each filter if hasIcon is true', async () => {
|
||||
@@ -350,132 +338,9 @@ describe('TaskFiltersCloudComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('searchApiMethod set to POST', () => {
|
||||
describe('filter selection and counters', () => {
|
||||
beforeEach(async () => {
|
||||
await configureTestingModule('POST');
|
||||
component.showIcons = true;
|
||||
});
|
||||
|
||||
it('should attach specific icon for each filter if hasIcon is true', async () => {
|
||||
await bindAppName();
|
||||
|
||||
const filterIcons = await loader.getAllHarnesses(MatIconHarness.with({ selector: '[data-automation-id="adf-filter-icon"]' }));
|
||||
|
||||
expect(component.filters.length).toBe(3);
|
||||
expect(filterIcons.length).toBe(3);
|
||||
expect(await filterIcons[0].getName()).toContain('adjust');
|
||||
expect(await filterIcons[1].getName()).toContain('done');
|
||||
expect(await filterIcons[2].getName()).toContain('inbox');
|
||||
});
|
||||
|
||||
it('should not attach icons for each filter if showIcons 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();
|
||||
|
||||
const filters = fixture.debugElement.queryAll(By.css('.adf-task-filters__entry'));
|
||||
|
||||
expect(component.filters.length).toBe(3);
|
||||
expect(filters.length).toBe(3);
|
||||
expect(filters[0].nativeElement.innerText).toContain('FakeInvolvedTasks');
|
||||
expect(filters[1].nativeElement.innerText).toContain('FakeMyTasks1');
|
||||
expect(filters[2].nativeElement.innerText).toContain('FakeMyTasks2');
|
||||
});
|
||||
|
||||
it('should not select any filter as default', async () => {
|
||||
await bindAppName();
|
||||
|
||||
expect(component.currentFilter).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should emit filterClicked when a filter is clicked from the UI', async () => {
|
||||
await bindAppName();
|
||||
const spy = spyOn(component.filterClicked, 'emit');
|
||||
|
||||
const filterButton = await loader.getHarness(
|
||||
MatNavListItemHarness.with({ selector: `[data-automation-id="${fakeGlobalFilter[0].key}_filter"]` })
|
||||
);
|
||||
await filterButton.click();
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(fakeGlobalFilter[0]);
|
||||
});
|
||||
|
||||
it('should display filter counter if property set to true', async () => {
|
||||
await bindAppName();
|
||||
|
||||
const filterCounters = fixture.debugElement.queryAll(By.css('.adf-task-filters__entry-counter'));
|
||||
|
||||
expect(component.filters.length).toBe(3);
|
||||
expect(filterCounters.length).toBe(1);
|
||||
expect(filterCounters[0].nativeElement.innerText).toContain('11');
|
||||
});
|
||||
|
||||
it('should update filter counter when notification received', async () => {
|
||||
await bindAppName();
|
||||
|
||||
const updatedFilterCounters = fixture.debugElement.queryAll(By.css('span.adf-active'));
|
||||
|
||||
expect(updatedFilterCounters.length).toBe(1);
|
||||
expect(Object.keys(component.counters).length).toBe(3);
|
||||
expect(component.counters['fake-involved-tasks']).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not update filter counter when notifications are disabled from app.config.json', async () => {
|
||||
spyOn(appConfigService, 'get').and.returnValue(false);
|
||||
await bindAppName();
|
||||
|
||||
expect(fixture.componentInstance.counters).toBeDefined();
|
||||
const updatedFilterCounters = fixture.debugElement.queryAll(By.css('span.adf-active'));
|
||||
expect(updatedFilterCounters.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should reset filter counter notification when filter is selected', async () => {
|
||||
await bindAppName();
|
||||
spyOn(appConfigService, 'get').and.returnValue(true);
|
||||
const change = new SimpleChange(null, { key: fakeGlobalFilter[0].key }, true);
|
||||
|
||||
let updatedFilterCounters = fixture.debugElement.queryAll(By.css('span.adf-active'));
|
||||
expect(updatedFilterCounters.length).toBe(1);
|
||||
|
||||
component.filters = fakeGlobalFilter;
|
||||
component.currentFilter = null;
|
||||
|
||||
component.ngOnChanges({ filterParam: change });
|
||||
fixture.detectChanges();
|
||||
|
||||
updatedFilterCounters = fixture.debugElement.queryAll(By.css('span.adf-active'));
|
||||
expect(updatedFilterCounters.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should refresh the filter counters when a filter is selected', async () => {
|
||||
await bindAppName();
|
||||
|
||||
const filterButton = await loader.getHarness(
|
||||
MatNavListItemHarness.with({ selector: `[data-automation-id="${fakeGlobalFilter[0].key}_filter"]` })
|
||||
);
|
||||
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]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('API agnostic', () => {
|
||||
beforeEach(async () => {
|
||||
await configureTestingModule('GET');
|
||||
await configureTestingModule();
|
||||
});
|
||||
|
||||
it('should emit an error with a bad response', (done) => {
|
||||
@@ -681,16 +546,16 @@ describe('TaskFiltersCloudComponent', () => {
|
||||
expect(component.updatedCountersSet.has(fakeFilterKey)).toBe(true);
|
||||
});
|
||||
|
||||
it('should resolve the counter only of the filters with a counter enabled', () => {
|
||||
it('should hold the counter only of the filters with a counter enabled', async () => {
|
||||
const filterWithCounter = new TaskFilterCloudModel({ ...defaultTaskFiltersMock[0], showCounter: true });
|
||||
const filterWithoutCounter = new TaskFilterCloudModel({ ...defaultTaskFiltersMock[1], showCounter: false });
|
||||
getTaskFilterCounterSpy.calls.reset();
|
||||
getTaskListFiltersSpy.and.returnValue(of([filterWithCounter, filterWithoutCounter]));
|
||||
getFilterCountersSpy.and.returnValue(of({ [filterWithCounter.key]: 7, [filterWithoutCounter.key]: 9 }));
|
||||
|
||||
component.filters = [filterWithCounter, filterWithoutCounter];
|
||||
component.updateFilterCounters();
|
||||
await bindAppName();
|
||||
|
||||
expect(getTaskFilterCounterSpy).toHaveBeenCalledTimes(1);
|
||||
expect(getTaskFilterCounterSpy).toHaveBeenCalledWith(filterWithCounter);
|
||||
expect(component.counters[filterWithCounter.key]).toBe(7);
|
||||
expect(component.counters[filterWithoutCounter.key]).toBe(0);
|
||||
});
|
||||
|
||||
describe('Batched counters', () => {
|
||||
@@ -705,7 +570,7 @@ describe('TaskFiltersCloudComponent', () => {
|
||||
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 }));
|
||||
getFilterCountersSpy.and.returnValue(of({ 'fake-involved-tasks': 9 }));
|
||||
|
||||
await bindAppName();
|
||||
expect(component.counters['fake-involved-tasks']).toBeUndefined();
|
||||
@@ -723,7 +588,7 @@ describe('TaskFiltersCloudComponent', () => {
|
||||
});
|
||||
|
||||
it('should hold the counters resolved by the batched count request', async () => {
|
||||
getFilterCountersSpy.and.returnValue(of({ counters: { 'fake-involved-tasks': 9 }, batched: true }));
|
||||
getFilterCountersSpy.and.returnValue(of({ 'fake-involved-tasks': 9 }));
|
||||
|
||||
await bindAppName();
|
||||
|
||||
@@ -731,7 +596,7 @@ describe('TaskFiltersCloudComponent', () => {
|
||||
});
|
||||
|
||||
it('should emit the filters whose counter changed', async () => {
|
||||
getFilterCountersSpy.and.returnValue(of({ counters: { 'fake-involved-tasks': 9 }, batched: true }));
|
||||
getFilterCountersSpy.and.returnValue(of({ 'fake-involved-tasks': 9 }));
|
||||
const updatedFilterSpy = spyOn(component.updatedFilter, 'emit');
|
||||
|
||||
await bindAppName();
|
||||
@@ -739,19 +604,20 @@ describe('TaskFiltersCloudComponent', () => {
|
||||
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 }));
|
||||
it('should keep the counter of a filter the request left out', async () => {
|
||||
getFilterCountersSpy.and.returnValue(of({ 'fake-involved-tasks': 9 }));
|
||||
await bindAppName();
|
||||
expect(component.counters['fake-involved-tasks']).toBe(9);
|
||||
|
||||
getFilterCountersSpy.and.returnValue(of({}));
|
||||
await bindAppName();
|
||||
|
||||
expect(getTaskFilterCounterSpy).toHaveBeenCalledWith(fakeGlobalFilter[0]);
|
||||
expect(component.counters['fake-involved-tasks']).toBe(11);
|
||||
expect(component.counters['fake-involved-tasks']).toBe(9);
|
||||
});
|
||||
|
||||
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');
|
||||
getFilterCountersSpy.and.returnValue(of({ 'fake-involved-tasks': 4 }));
|
||||
|
||||
await bindAppName();
|
||||
|
||||
@@ -759,15 +625,6 @@ describe('TaskFiltersCloudComponent', () => {
|
||||
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();
|
||||
|
||||
@@ -775,17 +632,6 @@ describe('TaskFiltersCloudComponent', () => {
|
||||
|
||||
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', () => {
|
||||
|
||||
+7
-75
@@ -15,8 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
|
||||
import { combineLatest, defer, EMPTY, Observable, of, Subscription } from 'rxjs';
|
||||
import { Component, EventEmitter, inject, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
|
||||
import { combineLatest, Observable, of, Subscription } from 'rxjs';
|
||||
import { TaskFilterCloudService } from '../../services/task-filter-cloud.service';
|
||||
import { FilterParamsModel, TaskFilterCloudModel } from '../../models/filter-cloud.model';
|
||||
import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core';
|
||||
@@ -24,10 +24,8 @@ import { catchError, map } from 'rxjs/operators';
|
||||
import { BaseTaskFiltersCloudComponent } from '../base-task-filters-cloud.component';
|
||||
import { TaskDetailsCloudModel } from '../../../models/task-details-cloud.model';
|
||||
import { TaskCloudEngineEvent } from '../../../../models/engine-event-cloud.model';
|
||||
import { TaskListCloudService } from '../../../task-list/services/task-list-cloud.service';
|
||||
import { TaskFilterCloudAdapter } from '../../../../models/filter-cloud-model';
|
||||
import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service';
|
||||
import { FilterCounterEntityType } from '../../../../models/filter-counters-cloud.model';
|
||||
import { FilterCounterEntityType, FilterCountersResult } from '../../../../models/filter-counters-cloud.model';
|
||||
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
@@ -44,15 +42,6 @@ import { AsyncPipe } from '@angular/common';
|
||||
export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent implements OnInit, OnChanges {
|
||||
protected readonly TASKS_ROUTE = '/task-list-cloud';
|
||||
|
||||
/**
|
||||
* (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the task count.
|
||||
*
|
||||
* @deprecated only used by the backends without `POST /query/v1/count`. It will be removed,
|
||||
* along with the 'GET' method, in ADF 10.0.0.
|
||||
*/
|
||||
@Input()
|
||||
searchApiMethod: 'GET' | 'POST' = 'GET';
|
||||
|
||||
/** Emitted when a filter is being selected based on the filterParam input. */
|
||||
@Output()
|
||||
filterSelected = new EventEmitter<TaskFilterCloudModel>();
|
||||
@@ -77,10 +66,8 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
||||
currentFiltersValues: { [key: string]: number } = {};
|
||||
private filtersLoadedFor?: string;
|
||||
private countersSubscription?: Subscription;
|
||||
private batchedCounters = true;
|
||||
|
||||
private readonly taskFilterCloudService = inject(TaskFilterCloudService);
|
||||
private readonly taskListCloudService = inject(TaskListCloudService);
|
||||
private readonly filterCountersCloudService = inject(FilterCountersCloudService);
|
||||
private readonly translationService = inject(TranslationService);
|
||||
private readonly appConfigService = inject(AppConfigService);
|
||||
@@ -142,40 +129,6 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
||||
this.filters.forEach((filter) => (this.counters[filter.key] = 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over filters and update counters
|
||||
*
|
||||
* @deprecated resolves the counters one filter at a time, for the backends without the batched
|
||||
* count endpoint. It will be 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 resolves the counter of one filter, for the backends without the batched count
|
||||
* endpoint. It will be removed in ADF 10.0.0.
|
||||
*/
|
||||
updateFilterCounter(filter: TaskFilterCloudModel): void {
|
||||
if (!filter?.showCounter) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Building the query throws for a malformed filter: `defer` turns that into a stream error to catch. */
|
||||
defer(() => this.fetchTaskFilterCounter(filter))
|
||||
.pipe(
|
||||
catchError(() => EMPTY),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
)
|
||||
.subscribe((counter) => {
|
||||
this.checkIfFilterValuesHasBeenUpdated(filter.key, counter);
|
||||
this.counters = { ...this.counters, [filter.key]: counter };
|
||||
});
|
||||
}
|
||||
|
||||
initFilterCounterNotifications(): void {
|
||||
if (this.appName && this.enableNotifications) {
|
||||
this.filterCountersCloudService
|
||||
@@ -244,7 +197,7 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
||||
onFilterClick(filter: FilterParamsModel) {
|
||||
if (filter) {
|
||||
this.selectFilter(filter);
|
||||
this.refreshFilterCounter(this.currentFilter);
|
||||
this.filterCountersCloudService.refreshFilterCounters(this.appName);
|
||||
this.filterClicked.emit(this.currentFilter);
|
||||
this.updatedCountersSet.delete(filter.key);
|
||||
} else {
|
||||
@@ -294,17 +247,10 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
||||
this.filterCountersCloudService.getFilterCounters(appName, FilterCounterEntityType.TASK)
|
||||
])
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(([, { counters, batched }]) => {
|
||||
this.batchedCounters = batched;
|
||||
if (batched) {
|
||||
this.applyFilterCounters(counters);
|
||||
} else {
|
||||
this.updateFilterCounters();
|
||||
}
|
||||
});
|
||||
.subscribe(([, counters]) => this.applyFilterCounters(counters));
|
||||
}
|
||||
|
||||
private applyFilterCounters(counters: { [filterKey: string]: number }): void {
|
||||
private applyFilterCounters(counters: FilterCountersResult): void {
|
||||
this.filters.forEach((filter) => {
|
||||
/* A filter without a key holds no request id. */
|
||||
const filterKey = filter?.showCounter ? filter.key : undefined;
|
||||
@@ -313,8 +259,8 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
||||
}
|
||||
|
||||
const counter = counters[filterKey];
|
||||
/* A filter the request left out keeps the counter it holds, rather than showing a wrong one. */
|
||||
if (counter === undefined) {
|
||||
this.updateFilterCounter(filter);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -323,12 +269,6 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
||||
});
|
||||
}
|
||||
|
||||
private fetchTaskFilterCounter(filter: TaskFilterCloudModel): Observable<number> {
|
||||
return this.searchApiMethod === 'POST'
|
||||
? this.taskListCloudService.getTaskListCount(new TaskFilterCloudAdapter(filter))
|
||||
: this.taskFilterCloudService.getTaskFilterCounter(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the filters properties
|
||||
*/
|
||||
@@ -336,12 +276,4 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
||||
this.filters = [];
|
||||
this.currentFilter = undefined;
|
||||
}
|
||||
|
||||
private refreshFilterCounter(filter: TaskFilterCloudModel): void {
|
||||
if (this.batchedCounters) {
|
||||
this.filterCountersCloudService.refreshFilterCounters(this.appName);
|
||||
} else {
|
||||
this.updateFilterCounter(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-39
@@ -16,13 +16,12 @@
|
||||
*/
|
||||
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable, of, BehaviorSubject, throwError, Subject } from 'rxjs';
|
||||
import { Observable, of, BehaviorSubject, Subject } from 'rxjs';
|
||||
import { TaskFilterCloudModel } from '../models/filter-cloud.model';
|
||||
import { switchMap, map } from 'rxjs/operators';
|
||||
import { BaseCloudService } from '../../../services/base-cloud.service';
|
||||
import { PreferenceCloudServiceInterface } from '../../../services/preference-cloud.interface';
|
||||
import { TASK_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.service';
|
||||
import { TaskCloudNodePaging } from '../../../models/task-cloud.model';
|
||||
import { NotificationCloudService } from '../../../services/notification-cloud.service';
|
||||
import { TaskCloudEngineEvent } from '../../../models/engine-event-cloud.model';
|
||||
import { IdentityUserService } from '../../../people/services/identity-user.service';
|
||||
@@ -247,43 +246,6 @@ export class TaskFilterCloudService extends BaseCloudService {
|
||||
return defaultFilters.findIndex((filter) => filterName === filter.name) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a task using an object with optional query properties.
|
||||
*
|
||||
* @returns Task information
|
||||
* @param taskFilter task filter model
|
||||
*/
|
||||
getTaskFilterCounter(taskFilter: TaskFilterCloudModel): Observable<number> {
|
||||
if (taskFilter.appName || taskFilter.appName === '') {
|
||||
const queryUrl = `${this.getBasePath(taskFilter.appName)}/query/v1/tasks`;
|
||||
const queryParams = {
|
||||
processInstanceId: taskFilter.processInstanceId,
|
||||
processDefinitionId: taskFilter.processDefinitionId,
|
||||
processDefinitionName: taskFilter.processDefinitionName,
|
||||
assignee: taskFilter.assignee,
|
||||
status: taskFilter.status,
|
||||
taskName: taskFilter.taskName,
|
||||
appName: taskFilter.appName,
|
||||
assignmentType: taskFilter.assignmentType,
|
||||
owner: taskFilter.owner,
|
||||
taskId: taskFilter.taskId,
|
||||
parentTaskId: taskFilter.parentTaskId,
|
||||
priority: taskFilter.priority,
|
||||
lastModifiedFrom: taskFilter.lastModifiedFrom,
|
||||
lastModifiedTo: taskFilter.lastModifiedTo,
|
||||
standalone: taskFilter.standalone,
|
||||
createdDate: taskFilter.createdDate,
|
||||
completedDate: taskFilter.completedDate,
|
||||
completedBy: taskFilter.completedBy,
|
||||
dueDate: taskFilter.dueDate,
|
||||
maxItems: 1
|
||||
};
|
||||
return this.get<TaskCloudNodePaging>(queryUrl, queryParams).pipe(map((tasks) => tasks.list.pagination.totalItems));
|
||||
} else {
|
||||
return throwError('Appname not configured');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls update preference api to update task filter
|
||||
*
|
||||
|
||||
-36
@@ -238,40 +238,4 @@ describe('TaskListCloudService', () => {
|
||||
expect(res).toBe('Appname not configured');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTaskListCount', () => {
|
||||
it('should concat the app name to the request url', async () => {
|
||||
const taskRequest = {
|
||||
appName: 'fakeName'
|
||||
} as TaskListRequestModel;
|
||||
requestSpy.and.callFake(returnCallUrl);
|
||||
|
||||
const res = await firstValueFrom(service.getTaskListCount(taskRequest));
|
||||
|
||||
expect(res).toBeDefined();
|
||||
expect(res).not.toBeNull();
|
||||
expect(res).toContain('fakeName/query/v1/tasks/count');
|
||||
});
|
||||
|
||||
it('should return 0 if response is falsy for getTaskListCount', async () => {
|
||||
const taskRequest = {
|
||||
appName: 'fakeName',
|
||||
pagination: { skipCount: 0, maxItems: 20 }
|
||||
} as TaskListRequestModel;
|
||||
requestSpy.and.callFake(() => Promise.resolve(null));
|
||||
|
||||
const res = await firstValueFrom(service.getTaskListCount(taskRequest));
|
||||
|
||||
expect(res).toBe(0);
|
||||
});
|
||||
|
||||
it('should throw error if appName is not configured in getTaskListCount', async () => {
|
||||
const taskRequest = { appName: null } as TaskListRequestModel;
|
||||
requestSpy.and.callFake(returnCallUrl);
|
||||
|
||||
const res = await firstValueFrom(service.getTaskListCount(taskRequest).pipe(catchError((error) => of(error.message))));
|
||||
|
||||
expect(res).toBe('Appname not configured');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,17 +124,6 @@ export class TaskListCloudService extends BaseCloudService implements TaskListCl
|
||||
return this.fetchTaskList(requestNode).pipe(map((tasks) => tasks.list.pagination.totalItems));
|
||||
}
|
||||
|
||||
getTaskListCount(requestNode: TaskListRequestModel): Observable<number> {
|
||||
if (!requestNode?.appName) {
|
||||
return throwError(() => new Error('Appname not configured'));
|
||||
}
|
||||
|
||||
const queryUrl = `${this.getBasePath(requestNode.appName)}/query/v1/tasks/count`;
|
||||
const queryData = this.buildQueryData(requestNode);
|
||||
|
||||
return this.post<object, number>(queryUrl, queryData).pipe(map((response) => response || 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the body of a task query, with the empty properties of the request stripped out.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user