AAE-25271 Refresh button for processes (#10211)

* [AAE-25271] display process notifications

* [AAE-25271] removed unnecessary conditional

* [AAE-25271] replaced observable with promise

* [AAE-25271] replaced observables with promises

* [AAE-25271] updated unit tests

* [AAE-25271] refactored tests to avoid circular dependency error

* [AAE-25271] replaced automatic imports
This commit is contained in:
tomasz hanaj
2024-09-19 15:51:59 +02:00
committed by GitHub
parent 797b800bd6
commit 1ff5e8f43c
15 changed files with 430 additions and 66 deletions

View File

@@ -15,16 +15,21 @@
* limitations under the License. * limitations under the License.
*/ */
import { NoopTranslateModule } from '@alfresco/adf-core';
import { AppListCloudModule } from './app-list-cloud.module'; import { AppListCloudModule } from './app-list-cloud.module';
import { TestBed } from '@angular/core/testing';
describe('AppListCloudModule', () => { describe('AppListCloudModule', () => {
let appListCloudModule: AppListCloudModule; let appListCloudModule: AppListCloudModule;
beforeEach(() => { beforeEach(() => {
appListCloudModule = new AppListCloudModule(); TestBed.configureTestingModule({
}); imports: [AppListCloudModule, NoopTranslateModule]
});
appListCloudModule = TestBed.inject(AppListCloudModule);
});
it('should create an instance', () => { it('should create an instance', () => {
expect(appListCloudModule).toBeTruthy(); expect(appListCloudModule).toBeTruthy();
}); });
}); });

View File

@@ -15,37 +15,53 @@
* limitations under the License. * limitations under the License.
*/ */
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SimpleChange } from '@angular/core';
import { AlfrescoApiService } from '@alfresco/adf-content-services'; import { AlfrescoApiService } from '@alfresco/adf-content-services';
import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module'; import { ADF_DATE_FORMATS, FullNamePipe, NoopTranslateModule, UserPreferencesService } from '@alfresco/adf-core';
import { MatDialog } from '@angular/material/dialog'; import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { SimpleChange } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { DateFnsAdapter } from '@angular/material-date-fns-adapter';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatChipsModule } from '@angular/material/chips';
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatExpansionPanelHarness } from '@angular/material/expansion/testing';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconTestingModule } from '@angular/material/icon/testing';
import { MatInputModule } from '@angular/material/input';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerHarness } from '@angular/material/progress-spinner/testing';
import { MatSelectModule } from '@angular/material/select';
import { MatSelectHarness } from '@angular/material/select/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { endOfDay, format, isValid, startOfDay, subYears } from 'date-fns';
import { enUS } from 'date-fns/locale';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { ProcessFilterDialogCloudComponent } from './process-filter-dialog-cloud.component'; import { AppsProcessCloudService } from '../../../app/services/apps-process-cloud.service';
import { DateRangeFilterComponent } from '../../../common/date-range-filter/date-range-filter.component';
import { fakeEnvironmentList } from '../../../common/mock/environment.mock';
import { DateCloudFilterType } from '../../../models/date-cloud-filter.model';
import { ProcessDefinitionCloud } from '../../../models/process-definition-cloud.model';
import { PeopleCloudComponent } from '../../../people/components/people-cloud.component';
import { IdentityUserServiceMock } from '../../../people/mock/people-cloud.mock';
import { IDENTITY_USER_SERVICE_TOKEN } from '../../../people/services/identity-user-service.token';
import { PROCESS_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.service';
import { LocalPreferenceCloudService } from '../../../services/local-preference-cloud.service';
import { NotificationCloudService } from '../../../services/notification-cloud.service';
import { ProcessCloudService } from '../../services/process-cloud.service';
import { mockAppVersions } from '../mock/process-filters-cloud.mock';
import { ProcessFilterCloudModel } from '../models/process-filter-cloud.model';
import { ProcessFilterCloudService } from '../services/process-filter-cloud.service';
import { fakeApplicationInstance, fakeApplicationInstanceWithEnvironment } from './../../../app/mock/app-model.mock';
import { import {
EditProcessFilterCloudComponent, EditProcessFilterCloudComponent,
PROCESS_FILTER_ACTION_RESTORE, PROCESS_FILTER_ACTION_RESTORE,
PROCESS_FILTER_ACTION_SAVE_DEFAULT PROCESS_FILTER_ACTION_SAVE_DEFAULT
} from './edit-process-filter-cloud.component'; } from './edit-process-filter-cloud.component';
import { ProcessFiltersCloudModule } from '../process-filters-cloud.module'; import { ProcessFilterDialogCloudComponent } from './process-filter-dialog-cloud.component';
import { ProcessFilterCloudModel } from '../models/process-filter-cloud.model';
import { ProcessFilterCloudService } from '../services/process-filter-cloud.service';
import { AppsProcessCloudService } from '../../../app/services/apps-process-cloud.service';
import { fakeApplicationInstance, fakeApplicationInstanceWithEnvironment } from './../../../app/mock/app-model.mock';
import { PROCESS_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.service';
import { LocalPreferenceCloudService } from '../../../services/local-preference-cloud.service';
import { ProcessCloudService } from '../../services/process-cloud.service';
import { DateCloudFilterType } from '../../../models/date-cloud-filter.model';
import { MatIconTestingModule } from '@angular/material/icon/testing';
import { ProcessDefinitionCloud } from '../../../models/process-definition-cloud.model';
import { mockAppVersions } from '../mock/process-filters-cloud.mock';
import { fakeEnvironmentList } from '../../../common/mock/environment.mock';
import { endOfDay, format, startOfDay, subYears, isValid } from 'date-fns';
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatSelectHarness } from '@angular/material/select/testing';
import { MatExpansionPanelHarness } from '@angular/material/expansion/testing';
import { MatProgressSpinnerHarness } from '@angular/material/progress-spinner/testing';
describe('EditProcessFilterCloudComponent', () => { describe('EditProcessFilterCloudComponent', () => {
let loader: HarnessLoader; let loader: HarnessLoader;
@@ -59,6 +75,7 @@ describe('EditProcessFilterCloudComponent', () => {
let getRunningApplicationsSpy: jasmine.Spy; let getRunningApplicationsSpy: jasmine.Spy;
let getProcessFilterByIdSpy: jasmine.Spy; let getProcessFilterByIdSpy: jasmine.Spy;
let alfrescoApiService: AlfrescoApiService; let alfrescoApiService: AlfrescoApiService;
let userPreferencesService: UserPreferencesService;
const fakeFilter = new ProcessFilterCloudModel({ const fakeFilter = new ProcessFilterCloudModel({
name: 'FakeRunningProcess', name: 'FakeRunningProcess',
@@ -83,8 +100,30 @@ describe('EditProcessFilterCloudComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ProcessFiltersCloudModule, ProcessServiceCloudTestingModule, MatIconTestingModule], imports: [
providers: [MatDialog, { provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }] MatIconTestingModule,
MatDialogModule,
NoopTranslateModule,
NoopAnimationsModule,
MatSelectModule,
MatDatepickerModule,
MatAutocompleteModule,
FullNamePipe,
MatFormFieldModule,
MatInputModule,
ReactiveFormsModule,
MatChipsModule,
MatProgressBarModule
],
providers: [
{ provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService },
{ provide: MAT_DATE_LOCALE, useValue: enUS },
{ provide: DateAdapter, useClass: DateFnsAdapter },
{ provide: NotificationCloudService, useValue: { makeGQLQuery: () => of([]) } },
{ provide: MAT_DATE_FORMATS, useValue: ADF_DATE_FORMATS },
{ provide: IDENTITY_USER_SERVICE_TOKEN, useExisting: IdentityUserServiceMock }
],
declarations: [PeopleCloudComponent, DateRangeFilterComponent]
}); });
fixture = TestBed.createComponent(EditProcessFilterCloudComponent); fixture = TestBed.createComponent(EditProcessFilterCloudComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
@@ -93,7 +132,9 @@ describe('EditProcessFilterCloudComponent', () => {
appsService = TestBed.inject(AppsProcessCloudService); appsService = TestBed.inject(AppsProcessCloudService);
processService = TestBed.inject(ProcessCloudService); processService = TestBed.inject(ProcessCloudService);
alfrescoApiService = TestBed.inject(AlfrescoApiService); alfrescoApiService = TestBed.inject(AlfrescoApiService);
userPreferencesService = TestBed.inject(UserPreferencesService);
dialog = TestBed.inject(MatDialog); dialog = TestBed.inject(MatDialog);
spyOn(dialog, 'open').and.returnValue({ spyOn(dialog, 'open').and.returnValue({
afterClosed: () => afterClosed: () =>
of({ of({
@@ -105,6 +146,7 @@ describe('EditProcessFilterCloudComponent', () => {
getProcessFilterByIdSpy = spyOn(service, 'getFilterById').and.returnValue(of(fakeFilter)); getProcessFilterByIdSpy = spyOn(service, 'getFilterById').and.returnValue(of(fakeFilter));
getRunningApplicationsSpy = spyOn(appsService, 'getDeployedApplicationsByStatus').and.returnValue(of(fakeApplicationInstance)); getRunningApplicationsSpy = spyOn(appsService, 'getDeployedApplicationsByStatus').and.returnValue(of(fakeApplicationInstance));
spyOn(alfrescoApiService, 'getInstance').and.returnValue(mock); spyOn(alfrescoApiService, 'getInstance').and.returnValue(mock);
spyOn(userPreferencesService, 'select').and.returnValue(of({ localize: 'en', formatLong: {} }));
fixture.detectChanges(); fixture.detectChanges();
loader = TestbedHarnessEnvironment.loader(fixture); loader = TestbedHarnessEnvironment.loader(fixture);
}); });

View File

@@ -9,15 +9,25 @@
[class.adf-active]="currentFilter === filter" [class.adf-active]="currentFilter === filter"
> >
<div class="adf-process-filters__entry"> <div class="adf-process-filters__entry">
<adf-icon <div>
data-automation-id="adf-filter-icon" <adf-icon
*ngIf="showIcons" data-automation-id="adf-filter-icon"
[value]="filter.icon"> *ngIf="showIcons"
</adf-icon> [value]="filter.icon">
</adf-icon>
<span
data-automation-id="adf-filter-label"
class="adf-filter-action-button__label">
{{ filter.name | translate }}
</span>
</div>
<span <span
data-automation-id="adf-filter-label" *ngIf="counters$[filter.key]"
class="adf-filter-action-button__label"> [attr.data-automation-id]="filter.key + '_filter-counter'"
{{ filter.name | translate }} class="adf-process-filters__entry-counter"
[class.adf-active]="isFilterUpdated(filter.key)"
>
{{ counters$[filter.key] | async }}
</span> </span>
</div> </div>
</button> </button>

View File

@@ -6,6 +6,7 @@
color: var(--adf-theme-foreground-text-color-054); color: var(--adf-theme-foreground-text-color-054);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
flex: 1; flex: 1;
height: 100%; height: 100%;
gap: var(--adf-theme-spacing); gap: var(--adf-theme-spacing);
@@ -13,6 +14,17 @@
&:hover { &:hover {
color: var(--theme-primary-color); color: var(--theme-primary-color);
} }
&-counter {
padding: 0 5px;
border-radius: 15px;
&.adf-active {
background-color: var(--theme-accent-color);
color: var(--theme-accent-color-default-contrast);
font-size: smaller;
}
}
} }
.adf-active { .adf-active {

View File

@@ -21,28 +21,46 @@ import { of, 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 { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module';
import { ProcessFiltersCloudModule } from '../process-filters-cloud.module';
import { PROCESS_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.service'; import { PROCESS_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, NoopTranslateModule } from '@alfresco/adf-core';
import { ProcessListCloudService } from '../../../process/process-list/services/process-list-cloud.service';
import { NotificationCloudService } from '../../../services/notification-cloud.service';
import { ApolloModule } from 'apollo-angular';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatListModule } from '@angular/material/list';
const ProcessFilterCloudServiceMock = {
getProcessFilters: () => of(mockProcessFilters),
getProcessNotificationSubscription: () => of([])
};
describe('ProcessFiltersCloudComponent', () => { describe('ProcessFiltersCloudComponent', () => {
let processFilterService: ProcessFilterCloudService; let processFilterService: ProcessFilterCloudService;
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;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ProcessServiceCloudTestingModule, ProcessFiltersCloudModule], imports: [NoopTranslateModule, NoopAnimationsModule, MatListModule],
providers: [{ provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }] providers: [
{ provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService },
{ provide: AppConfigService, useClass: AppConfigServiceMock },
{ provide: ProcessListCloudService, useValue: { getProcessCounter: () => of(10) } },
{ provide: ProcessFilterCloudService, useValue: ProcessFilterCloudServiceMock },
NotificationCloudService,
ApolloModule
]
}); });
fixture = TestBed.createComponent(ProcessFiltersCloudComponent); fixture = TestBed.createComponent(ProcessFiltersCloudComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
processFilterService = TestBed.inject(ProcessFilterCloudService); processFilterService = TestBed.inject(ProcessFilterCloudService);
getProcessFiltersSpy = spyOn(processFilterService, 'getProcessFilters').and.returnValue(of(mockProcessFilters)); getProcessFiltersSpy = spyOn(processFilterService, 'getProcessFilters').and.returnValue(of(mockProcessFilters));
getProcessNotificationSubscriptionSpy = spyOn(processFilterService, 'getProcessNotificationSubscription').and.returnValue(of([]));
}); });
afterEach(() => { afterEach(() => {
@@ -288,12 +306,14 @@ describe('ProcessFiltersCloudComponent', () => {
}; };
const clickOnFilter = async (filterKey: string) => { const clickOnFilter = async (filterKey: string) => {
fixture.debugElement.nativeElement.querySelector(`[data-automation-id="${filterKey}_filter"]`).click(); const button = fixture.debugElement.nativeElement.querySelector(`[data-automation-id="${filterKey}_filter"]`);
button.click();
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
}; };
it('should apply active CSS class on filter click', async () => { it('should apply active CSS class on filter click', async () => {
component.enableNotifications = true;
component.appName = 'mock-app-name'; component.appName = 'mock-app-name';
const appNameChange = new SimpleChange(null, 'mock-app-name', true); const appNameChange = new SimpleChange(null, 'mock-app-name', true);
component.ngOnChanges({ appName: appNameChange }); component.ngOnChanges({ appName: appNameChange });
@@ -301,18 +321,24 @@ describe('ProcessFiltersCloudComponent', () => {
await fixture.whenStable(); await fixture.whenStable();
await clickOnFilter(allProcessesFilterKey); await clickOnFilter(allProcessesFilterKey);
fixture.detectChanges();
await fixture.whenStable();
expect(getActiveFilterElement(allProcessesFilterKey)).toBeDefined(); expect(getActiveFilterElement(allProcessesFilterKey)).toBeDefined();
expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull(); expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull();
expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull(); expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull();
await clickOnFilter(runningProcessesFilterKey); await clickOnFilter(runningProcessesFilterKey);
fixture.detectChanges();
await fixture.whenStable();
expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull(); expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull();
expect(getActiveFilterElement(runningProcessesFilterKey)).toBeDefined(); expect(getActiveFilterElement(runningProcessesFilterKey)).toBeDefined();
expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull(); expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull();
await clickOnFilter(completedProcessesFilterKey); await clickOnFilter(completedProcessesFilterKey);
fixture.detectChanges();
await fixture.whenStable();
expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull(); expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull();
expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull(); expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull();
@@ -345,5 +371,77 @@ describe('ProcessFiltersCloudComponent', () => {
expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull(); expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull();
expect(getActiveFilterElement(completedProcessesFilterKey)).toBeDefined(); expect(getActiveFilterElement(completedProcessesFilterKey)).toBeDefined();
}); });
it('should made sbscription', () => {
component.enableNotifications = true;
component.appName = 'mock-app-name';
const appNameChange = new SimpleChange(null, 'mock-app-name', true);
component.ngOnChanges({ appName: appNameChange });
fixture.detectChanges();
expect(getProcessNotificationSubscriptionSpy).toHaveBeenCalled();
});
it('should not emit filter key when filter counter is set for first time', () => {
component.currentFiltersValues = {};
const fakeFilterKey = 'testKey';
const fakeFilterValue = 10;
const updatedFilterSpy = spyOn(component.updatedFilter, 'emit');
component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, fakeFilterValue);
fixture.detectChanges();
expect(component.currentFiltersValues).not.toEqual({});
expect(component.currentFiltersValues[fakeFilterKey]).toBe(fakeFilterValue);
expect(updatedFilterSpy).not.toHaveBeenCalled();
});
it('should not emit filter key when filter counter has not changd', () => {
component.currentFiltersValues = {};
const fakeFilterKey = 'testKey';
const fakeFilterValue = 10;
const updatedFilterSpy = spyOn(component.updatedFilter, 'emit');
component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, fakeFilterValue);
fixture.detectChanges();
expect(component.currentFiltersValues).not.toEqual({});
expect(component.currentFiltersValues[fakeFilterKey]).toBe(fakeFilterValue);
component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, fakeFilterValue);
expect(component.currentFiltersValues[fakeFilterKey]).toBe(fakeFilterValue);
expect(updatedFilterSpy).not.toHaveBeenCalled();
});
it('should emit filter key when filter counter is increased', () => {
component.currentFiltersValues = {};
const fakeFilterKey = 'testKey';
const updatedFilterSpy = spyOn(component.updatedFilter, 'emit');
component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 10);
fixture.detectChanges();
expect(updatedFilterSpy).not.toHaveBeenCalledWith(fakeFilterKey);
expect(component.currentFiltersValues[fakeFilterKey]).toBe(10);
component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 20);
fixture.detectChanges();
expect(updatedFilterSpy).toHaveBeenCalledWith(fakeFilterKey);
expect(component.currentFiltersValues[fakeFilterKey]).toBe(20);
});
it('should emit filter key when filter counter is decreased', () => {
component.currentFiltersValues = {};
const fakeFilterKey = 'testKey';
const updatedFilterSpy = spyOn(component.updatedFilter, 'emit');
component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 10);
fixture.detectChanges();
expect(updatedFilterSpy).not.toHaveBeenCalledWith(fakeFilterKey);
expect(component.currentFiltersValues[fakeFilterKey]).toBe(10);
component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 5);
fixture.detectChanges();
expect(updatedFilterSpy).toHaveBeenCalledWith(fakeFilterKey);
expect(component.currentFiltersValues[fakeFilterKey]).toBe(5);
});
}); });
}); });

View File

@@ -19,9 +19,10 @@ import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, OnDes
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } 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 { TranslationService } from '@alfresco/adf-core'; import { AppConfigService, 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 { takeUntil } from 'rxjs/operators'; import { debounceTime, takeUntil, tap } from 'rxjs/operators';
import { ProcessListCloudService } from '../../../process/process-list/services/process-list-cloud.service';
@Component({ @Component({
selector: 'adf-cloud-process-filters', selector: 'adf-cloud-process-filters',
@@ -58,19 +59,31 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges, OnDestro
@Output() @Output()
error = new EventEmitter<any>(); error = new EventEmitter<any>();
/** Emitted when filter is updated. */
@Output()
updatedFilter: EventEmitter<string> = new EventEmitter<string>();
filters$: Observable<ProcessFilterCloudModel[]>; filters$: Observable<ProcessFilterCloudModel[]>;
currentFilter?: ProcessFilterCloudModel; currentFilter?: ProcessFilterCloudModel;
filters: ProcessFilterCloudModel[] = []; filters: ProcessFilterCloudModel[] = [];
counters$: { [key: string]: Observable<number> } = {};
enableNotifications: boolean;
currentFiltersValues: { [key: string]: number } = {};
updatedFiltersSet = new Set<string>();
private onDestroy$ = new Subject<boolean>(); private onDestroy$ = new Subject<boolean>();
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 processListCloudService = inject(ProcessListCloudService);
ngOnInit() { ngOnInit() {
this.enableNotifications = this.appConfigService.get('notifications', true);
if (this.appName === '') { if (this.appName === '') {
this.getFilters(this.appName); this.getFilters(this.appName);
} }
this.initProcessNotification();
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
@@ -97,6 +110,7 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges, OnDestro
this.filters = res || []; this.filters = res || [];
this.selectFilterAndEmit(this.filterParam); this.selectFilterAndEmit(this.filterParam);
this.success.emit(res); this.success.emit(res);
this.updateFilterCounters();
}, },
error: (err: any) => { error: (err: any) => {
this.error.emit(err); this.error.emit(err);
@@ -172,6 +186,8 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges, OnDestro
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.updatedFiltersSet.delete(filter.key);
} else { } else {
this.currentFilter = undefined; this.currentFilter = undefined;
} }
@@ -220,4 +236,47 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges, OnDestro
isActiveFilter(filter: ProcessFilterCloudModel): boolean { isActiveFilter(filter: ProcessFilterCloudModel): boolean {
return this.currentFilter.name === filter.name; return this.currentFilter.name === filter.name;
} }
initProcessNotification(): void {
if (this.appName && this.enableNotifications) {
this.processFilterCloudService
.getProcessNotificationSubscription(this.appName)
.pipe(debounceTime(1000), takeUntil(this.onDestroy$))
.subscribe(() => {
this.updateFilterCounters();
});
}
}
updateFilterCounters(): void {
this.filters.forEach((filter: ProcessFilterCloudModel) => {
if (filter?.status) {
this.updateFilterCounter(filter);
}
});
}
updateFilterCounter(filter: ProcessFilterCloudModel): void {
this.counters$[filter.key] = this.processListCloudService.getProcessCounter(filter.appName, filter.status).pipe(
tap((filterCounter) => {
this.checkIfFilterValuesHasBeenUpdated(filter.key, filterCounter);
})
);
}
checkIfFilterValuesHasBeenUpdated(filterKey: string, filterValue: number): void {
if (!this.currentFiltersValues[filterKey]) {
this.currentFiltersValues[filterKey] = filterValue;
return;
}
if (this.currentFiltersValues[filterKey] !== filterValue) {
this.currentFiltersValues[filterKey] = filterValue;
this.updatedFilter.emit(filterKey);
this.updatedFiltersSet.add(filterKey);
}
}
isFilterUpdated(filterName: string): boolean {
return this.updatedFiltersSet.has(filterName);
}
} }

View File

@@ -186,3 +186,25 @@ const mockAppVersion2: ApplicationVersionModel = {
}; };
export const mockAppVersions = [mockAppVersion1, mockAppVersion2]; export const mockAppVersions = [mockAppVersion1, mockAppVersion2];
export const processNotifications = [
{
eventType: 'PROCESS_CREATED',
entity: {
appVersion: '1',
id: 'bccc1217-7036-11ef-86f2-bae4749e773e',
processDefinitionId: 'Process_XmWTFMqf:1:1b30709b-6ff3-11ef-86f2-bae4749e773e',
processDefinitionKey: 'Process_XmWTFMqf',
initiator: 'hruser',
status: 'CREATED',
processDefinitionVersion: 1,
processDefinitionName: 'processchild'
}
}
];
export const processCloudEngineEventsMock = {
data: {
engineEvents: processNotifications
}
};

View File

@@ -16,7 +16,7 @@
*/ */
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs'; import { firstValueFrom, of } from 'rxjs';
import { ProcessFilterCloudService } from './process-filter-cloud.service'; import { ProcessFilterCloudService } from './process-filter-cloud.service';
import { PROCESS_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.service'; import { PROCESS_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.service';
import { LocalPreferenceCloudService } from '../../../services/local-preference-cloud.service'; import { LocalPreferenceCloudService } from '../../../services/local-preference-cloud.service';
@@ -26,10 +26,12 @@ import {
fakeProcessCloudFilterEntries, fakeProcessCloudFilterEntries,
fakeProcessCloudFilters, fakeProcessCloudFilters,
fakeProcessCloudFilterWithDifferentEntries, fakeProcessCloudFilterWithDifferentEntries,
fakeProcessFilter fakeProcessFilter,
processCloudEngineEventsMock
} from '../mock/process-filters-cloud.mock'; } from '../mock/process-filters-cloud.mock';
import { ProcessFilterCloudModel } from '../models/process-filter-cloud.model'; import { ProcessFilterCloudModel } from '../models/process-filter-cloud.model';
import { IdentityUserService } from '../../../people/services/identity-user.service'; import { IdentityUserService } from '../../../people/services/identity-user.service';
import { NotificationCloudService } from '../../../services/notification-cloud.service';
describe('ProcessFilterCloudService', () => { describe('ProcessFilterCloudService', () => {
let service: ProcessFilterCloudService; let service: ProcessFilterCloudService;
@@ -38,6 +40,7 @@ describe('ProcessFilterCloudService', () => {
let updatePreferenceSpy: jasmine.Spy; let updatePreferenceSpy: jasmine.Spy;
let createPreferenceSpy: jasmine.Spy; let createPreferenceSpy: jasmine.Spy;
let getCurrentUserInfoSpy: jasmine.Spy; let getCurrentUserInfoSpy: jasmine.Spy;
let notificationCloudService: NotificationCloudService;
const identityUserMock = { const identityUserMock = {
username: 'mock-username', username: 'mock-username',
@@ -55,6 +58,7 @@ describe('ProcessFilterCloudService', () => {
const preferenceCloudService = TestBed.inject(PROCESS_FILTERS_SERVICE_TOKEN); const preferenceCloudService = TestBed.inject(PROCESS_FILTERS_SERVICE_TOKEN);
const identityUserService = TestBed.inject(IdentityUserService); const identityUserService = TestBed.inject(IdentityUserService);
notificationCloudService = TestBed.inject(NotificationCloudService);
createPreferenceSpy = spyOn(preferenceCloudService, 'createPreference').and.returnValue(of(fakeProcessCloudFilters)); createPreferenceSpy = spyOn(preferenceCloudService, 'createPreference').and.returnValue(of(fakeProcessCloudFilters));
updatePreferenceSpy = spyOn(preferenceCloudService, 'updatePreference').and.returnValue(of(fakeProcessCloudFilters)); updatePreferenceSpy = spyOn(preferenceCloudService, 'updatePreference').and.returnValue(of(fakeProcessCloudFilters));
@@ -236,4 +240,13 @@ describe('ProcessFilterCloudService', () => {
expect(updatePreferenceSpy).toHaveBeenCalledWith('mock-appName', 'process-filters-mock-appName-mock-username', fakeProcessCloudFilters); expect(updatePreferenceSpy).toHaveBeenCalledWith('mock-appName', 'process-filters-mock-appName-mock-username', fakeProcessCloudFilters);
}); });
it('should return engine event task subscription', async () => {
spyOn(notificationCloudService, 'makeGQLQuery').and.returnValue(of(processCloudEngineEventsMock));
const result = await firstValueFrom(service.getProcessNotificationSubscription('testApp'));
expect(result.length).toBe(1);
expect(result[0].eventType).toBe('PROCESS_CREATED');
expect(result[0].entity.status).toBe('CREATED');
});
}); });

View File

@@ -22,6 +22,25 @@ import { switchMap, map } from 'rxjs/operators';
import { PROCESS_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.service'; import { PROCESS_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.service';
import { PreferenceCloudServiceInterface } from '../../../services/preference-cloud.interface'; import { PreferenceCloudServiceInterface } from '../../../services/preference-cloud.interface';
import { IdentityUserService } from '../../../people/services/identity-user.service'; import { IdentityUserService } from '../../../people/services/identity-user.service';
import { NotificationCloudService } from '../../../services/notification-cloud.service';
import { TaskCloudEngineEvent } from '../../../models/engine-event-cloud.model';
const PROCESS_EVENT_SUBSCRIPTION_QUERY = `
subscription {
engineEvents(eventType: [
PROCESS_CANCELLED
PROCESS_COMPLETED
PROCESS_CREATED
PROCESS_RESUMED
PROCESS_SUSPENDED
PROCESS_STARTED
]) {
eventType
entity
}
}
`;
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
@@ -31,6 +50,7 @@ export class ProcessFilterCloudService {
private readonly preferenceService = inject<PreferenceCloudServiceInterface>(PROCESS_FILTERS_SERVICE_TOKEN); private readonly preferenceService = inject<PreferenceCloudServiceInterface>(PROCESS_FILTERS_SERVICE_TOKEN);
private readonly identityUserService = inject(IdentityUserService); private readonly identityUserService = inject(IdentityUserService);
private readonly notificationCloudService = inject(NotificationCloudService);
constructor() { constructor() {
this.filtersSubject = new BehaviorSubject([]); this.filtersSubject = new BehaviorSubject([]);
@@ -377,4 +397,10 @@ export class ProcessFilterCloudService {
}) })
]; ];
} }
getProcessNotificationSubscription(appName: string): Observable<TaskCloudEngineEvent[]> {
return this.notificationCloudService
.makeGQLQuery(appName, PROCESS_EVENT_SUBSCRIPTION_QUERY)
.pipe(map((events: any) => events?.data?.engineEvents));
}
} }

View File

@@ -39,7 +39,7 @@ import { PROCESS_LISTS_PREFERENCES_SERVICE_TOKEN } from '../../../services/cloud
import { ProcessListCloudPreferences } from '../models/process-cloud-preferences'; import { ProcessListCloudPreferences } from '../models/process-cloud-preferences';
import { PROCESS_LIST_CUSTOM_VARIABLE_COLUMN } from '../../../models/data-column-custom-data'; import { PROCESS_LIST_CUSTOM_VARIABLE_COLUMN } from '../../../models/data-column-custom-data';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { PreferenceCloudServiceInterface } from '@alfresco/adf-process-services-cloud'; import { PreferenceCloudServiceInterface } from '../../../services/preference-cloud.interface';
import { HarnessLoader } from '@angular/cdk/testing'; import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatProgressSpinnerHarness } from '@angular/material/progress-spinner/testing'; import { MatProgressSpinnerHarness } from '@angular/material/progress-spinner/testing';

View File

@@ -20,6 +20,7 @@ import { ProcessListCloudService } from './process-list-cloud.service';
import { ProcessQueryCloudRequestModel } from '../models/process-cloud-query-request.model'; import { ProcessQueryCloudRequestModel } from '../models/process-cloud-query-request.model';
import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module'; import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module';
import { AdfHttpClient } from '@alfresco/adf-core/api'; import { AdfHttpClient } from '@alfresco/adf-core/api';
import { firstValueFrom } from 'rxjs';
describe('ProcessListCloudService', () => { describe('ProcessListCloudService', () => {
let service: ProcessListCloudService; let service: ProcessListCloudService;
@@ -36,9 +37,7 @@ describe('ProcessListCloudService', () => {
beforeEach(fakeAsync(() => { beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [ProcessServiceCloudTestingModule]
ProcessServiceCloudTestingModule
]
}); });
adfHttpClient = TestBed.inject(AdfHttpClient); adfHttpClient = TestBed.inject(AdfHttpClient);
service = TestBed.inject(ProcessListCloudService); service = TestBed.inject(ProcessListCloudService);
@@ -71,8 +70,14 @@ describe('ProcessListCloudService', () => {
it('should concat the sorting to append as parameters', (done) => { it('should concat the sorting to append as parameters', (done) => {
const processRequest = { const processRequest = {
appName: 'fakeName', skipCount: 0, maxItems: 20, service: 'fake-service', appName: 'fakeName',
sorting: [{ orderBy: 'NAME', direction: 'DESC' }, { orderBy: 'TITLE', direction: 'ASC' }] skipCount: 0,
maxItems: 20,
service: 'fake-service',
sorting: [
{ orderBy: 'NAME', direction: 'DESC' },
{ orderBy: 'TITLE', direction: 'ASC' }
]
} as ProcessQueryCloudRequestModel; } as ProcessQueryCloudRequestModel;
requestSpy.and.callFake(returnCallQueryParameters); requestSpy.and.callFake(returnCallQueryParameters);
service.getProcessByRequest(processRequest).subscribe((res) => { service.getProcessByRequest(processRequest).subscribe((res) => {
@@ -87,7 +92,7 @@ describe('ProcessListCloudService', () => {
const processRequest = { appName: null } as ProcessQueryCloudRequestModel; const processRequest = { appName: null } as ProcessQueryCloudRequestModel;
requestSpy.and.callFake(returnCallUrl); requestSpy.and.callFake(returnCallUrl);
service.getProcessByRequest(processRequest).subscribe( service.getProcessByRequest(processRequest).subscribe(
() => { }, () => {},
(error) => { (error) => {
expect(error).toBe('Appname not configured'); expect(error).toBe('Appname not configured');
done(); done();
@@ -95,8 +100,19 @@ describe('ProcessListCloudService', () => {
); );
}); });
describe('getAdminProcessRequest', () => { it('should return number of total items of processes ', async () => {
const processRequest = { appName: 'fakeName', skipCount: 0, maxItems: 1, service: 'fake-service' } as ProcessQueryCloudRequestModel;
requestSpy.and.callFake(returnCallQueryParameters);
const result = await firstValueFrom(service.getProcessByRequest(processRequest));
expect(result).toBeDefined();
expect(result).not.toBeNull();
expect(result.skipCount).toBe(0);
expect(result.maxItems).toBe(1);
expect(result.service).toBe('fake-service');
});
describe('getAdminProcessRequest', () => {
it('should append to the call all the parameters', async () => { it('should append to the call all the parameters', async () => {
const processRequest = { appName: 'fakeName', skipCount: 0, maxItems: 20, service: 'fake-service' } as ProcessQueryCloudRequestModel; const processRequest = { appName: 'fakeName', skipCount: 0, maxItems: 20, service: 'fake-service' } as ProcessQueryCloudRequestModel;
requestSpy.and.callFake(returnCallQueryParameters); requestSpy.and.callFake(returnCallQueryParameters);
@@ -121,8 +137,14 @@ describe('ProcessListCloudService', () => {
it('should concat the sorting to append as parameters', async () => { it('should concat the sorting to append as parameters', async () => {
const processRequest = { const processRequest = {
appName: 'fakeName', skipCount: 0, maxItems: 20, service: 'fake-service', appName: 'fakeName',
sorting: [{ orderBy: 'NAME', direction: 'DESC' }, { orderBy: 'TITLE', direction: 'ASC' }] skipCount: 0,
maxItems: 20,
service: 'fake-service',
sorting: [
{ orderBy: 'NAME', direction: 'DESC' },
{ orderBy: 'TITLE', direction: 'ASC' }
]
} as ProcessQueryCloudRequestModel; } as ProcessQueryCloudRequestModel;
requestSpy.and.callFake(returnCallQueryParameters); requestSpy.and.callFake(returnCallQueryParameters);
const request = await service.getAdminProcessByRequest(processRequest).toPromise(); const request = await service.getAdminProcessByRequest(processRequest).toPromise();
@@ -140,7 +162,7 @@ describe('ProcessListCloudService', () => {
await service.getAdminProcessByRequest(processRequest).toPromise(); await service.getAdminProcessByRequest(processRequest).toPromise();
fail('Should have thrown error'); fail('Should have thrown error');
} catch(error) { } catch (error) {
expect(error).toBe('Appname not configured'); expect(error).toBe('Appname not configured');
} }
}); });
@@ -155,7 +177,13 @@ describe('ProcessListCloudService', () => {
}); });
it('should not have variable keys as part of query parameters', async () => { it('should not have variable keys as part of query parameters', async () => {
const processRequest = { appName: 'fakeName', skipCount: 0, maxItems: 20, service: 'fake-service', variableKeys: ['test-one', 'test-two'] } as ProcessQueryCloudRequestModel; const processRequest = {
appName: 'fakeName',
skipCount: 0,
maxItems: 20,
service: 'fake-service',
variableKeys: ['test-one', 'test-two']
} as ProcessQueryCloudRequestModel;
requestSpy.and.callFake(returnCallQueryParameters); requestSpy.and.callFake(returnCallQueryParameters);
const requestParams = await service.getAdminProcessByRequest(processRequest).toPromise(); const requestParams = await service.getAdminProcessByRequest(processRequest).toPromise();
@@ -165,7 +193,13 @@ describe('ProcessListCloudService', () => {
}); });
it('should send right variable keys as post body', async () => { it('should send right variable keys as post body', async () => {
const processRequest = { appName: 'fakeName', skipCount: 0, maxItems: 20, service: 'fake-service', variableKeys: ['test-one', 'test-two'] } as ProcessQueryCloudRequestModel; const processRequest = {
appName: 'fakeName',
skipCount: 0,
maxItems: 20,
service: 'fake-service',
variableKeys: ['test-one', 'test-two']
} as ProcessQueryCloudRequestModel;
requestSpy.and.callFake(returnCallBody); requestSpy.and.callFake(returnCallBody);
const requestBodyParams = await service.getAdminProcessByRequest(processRequest).toPromise(); const requestBodyParams = await service.getAdminProcessByRequest(processRequest).toPromise();

View File

@@ -67,6 +67,48 @@ export class ProcessListCloudService extends BaseCloudService {
return this.getProcess(callback, defaultQueryUrl, requestNode, queryUrl); return this.getProcess(callback, defaultQueryUrl, requestNode, queryUrl);
} }
/**
* 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. * Finds a process using an object with optional query properties in admin app.
* *

View File

@@ -49,7 +49,8 @@ import { ProcessServiceCloudTestingModule } from '../../../testing/process-servi
import { ProcessNameCloudPipe } from '../../../pipes/process-name-cloud.pipe'; import { ProcessNameCloudPipe } from '../../../pipes/process-name-cloud.pipe';
import { ProcessInstanceCloud } from '../models/process-instance-cloud.model'; import { ProcessInstanceCloud } from '../models/process-instance-cloud.model';
import { ESCAPE } from '@angular/cdk/keycodes'; import { ESCAPE } from '@angular/cdk/keycodes';
import { ProcessDefinitionCloud, TaskVariableCloud } from '@alfresco/adf-process-services-cloud'; import { ProcessDefinitionCloud } from '../../../models/process-definition-cloud.model';
import { TaskVariableCloud } from '../../../form/models/task-variable-cloud.model';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { HarnessLoader } from '@angular/cdk/testing'; import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';

View File

@@ -52,7 +52,7 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
filters: TaskFilterCloudModel[] = []; filters: TaskFilterCloudModel[] = [];
currentFilter: TaskFilterCloudModel; currentFilter: TaskFilterCloudModel;
enableNotifications: boolean; enableNotifications: boolean;
currentFiltersValues = {}; currentFiltersValues: { [key: string]: number } = {};
private readonly taskFilterCloudService = inject(TaskFilterCloudService); private readonly taskFilterCloudService = inject(TaskFilterCloudService);
private readonly translationService = inject(TranslationService); private readonly translationService = inject(TranslationService);

View File

@@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { TaskDetailsCloudModel } from '@alfresco/adf-process-services-cloud'; import { TaskDetailsCloudModel } from '../../../task/start-task/models/task-details-cloud.model';
import { assignedTaskDetailsCloudMock } from '../../task-header/mocks/task-details-cloud.mock'; import { assignedTaskDetailsCloudMock } from '../../task-header/mocks/task-details-cloud.mock';
import { TaskFilterCloudModel, ServiceTaskFilterCloudModel, AssignmentType, TaskStatusFilter } from '../models/filter-cloud.model'; import { TaskFilterCloudModel, ServiceTaskFilterCloudModel, AssignmentType, TaskStatusFilter } from '../models/filter-cloud.model';