AAE-38626 Reload columns on appname change (#11560)

* [AAE-38626] Updating columns when appName changes

[AAE-38626] Updating columns when appName changes

* [AAE-38626] Added tests

* Added indicator to handle app change

* [ci:force] fixes

* Checking enableAppChange in ngOnchange

* Fixed tests

---------

Co-authored-by: Fabian Kindgen <39992669+fkindgen@users.noreply.github.com>
This commit is contained in:
Andreas Philippi
2026-01-27 20:18:14 +01:00
committed by GitHub
parent 15806d7c9f
commit 6131bf3890
4 changed files with 131 additions and 65 deletions

View File

@@ -45,17 +45,19 @@ export abstract class DataTableSchema<T = unknown> {
protected columnsSchemaSubject$ = new BehaviorSubject<boolean>(false);
isColumnSchemaCreated$ = this.columnsSchemaSubject$.asObservable();
constructor(private appConfigService: AppConfigService, protected presetKey: string, protected presetsModel: any) {}
constructor(
private appConfigService: AppConfigService,
protected presetKey: string,
protected presetsModel: any
) {}
public createDatatableSchema(): void {
this.loadLayoutPresets();
if (!this.columns || this.columns.length === 0) {
this.createColumns();
this.columnsSchemaSubject$.next(true);
} else {
this.columnsSchemaSubject$.next(true);
}
this.columnsSchemaSubject$.next(true);
}
public createColumns(): void {

View File

@@ -3,7 +3,7 @@
[columns]="columns"
[data]="dataAdapter"
[stickyHeader]="stickyHeader"
[loading]="isLoading"
[loading]="isLoading$ | async"
[sorting]="formattedSorting"
[selectionMode]="selectionMode"
[multiselect]="multiselect"

View File

@@ -34,7 +34,7 @@ import {
} from '@alfresco/adf-core';
import { ProcessListCloudService } from '../services/process-list-cloud.service';
import { ProcessListCloudComponent } from './process-list-cloud.component';
import { of, throwError } from 'rxjs';
import { of, throwError, firstValueFrom } from 'rxjs';
import { shareReplay, skip } from 'rxjs/operators';
import { ProcessListCloudSortingModel } from '../models/process-list-sorting.model';
import { PROCESS_LISTS_PREFERENCES_SERVICE_TOKEN } from '../../../services/cloud-token.service';
@@ -338,15 +338,16 @@ describe('ProcessListCloudComponent', () => {
spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList));
const appName = new SimpleChange(null, 'FAKE-APP-NAME', true);
fixture.detectChanges();
expect(component.isLoading).toBe(true);
expect(await firstValueFrom(component.isLoading$)).toBe(true);
expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(true);
fixture.detectChanges();
await fixture.whenStable();
component.ngOnChanges({ appName });
fixture.detectChanges();
await fixture.whenStable();
expect(component.isLoading).toBe(false);
expect(await firstValueFrom(component.isLoading$)).toBe(false);
expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(false);
const emptyContent = fixture.debugElement.query(By.css('.adf-empty-content'));
@@ -424,9 +425,10 @@ describe('ProcessListCloudComponent', () => {
done();
});
component.ngAfterContentInit();
component.appName = appName.currentValue;
component.ngOnChanges({ appName });
fixture.detectChanges();
component.reload();
});
it('should shown columns selector', () => {
@@ -633,15 +635,13 @@ describe('ProcessListCloudComponent', () => {
spyOn(processListCloudService, 'fetchProcessList').and.returnValue(of(fakeProcessCloudList));
const appName = new SimpleChange(null, 'FAKE-APP-NAME', true);
fixture.detectChanges();
expect(component.isLoading).toBe(true);
expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(true);
expect(await firstValueFrom(component.isLoading$)).toBe(true);
component.ngOnChanges({ appName });
fixture.detectChanges();
await fixture.whenStable();
expect(component.isLoading).toBe(false);
expect(await firstValueFrom(component.isLoading$)).toBe(false);
expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(false);
const emptyContent = fixture.debugElement.query(By.css('.adf-empty-content'));
@@ -758,9 +758,9 @@ describe('ProcessListCloudComponent', () => {
done();
});
component.ngAfterContentInit();
component.appName = appName.currentValue;
component.ngOnChanges({ appName });
fixture.detectChanges();
component.reload();
});
it('should shown columns selector', () => {
@@ -954,6 +954,34 @@ describe('ProcessListCloudComponent', () => {
component.resetPagination();
});
});
it('should call loadPreferencesAndInitialize when appName changes', () => {
spyOn(preferencesService, 'getPreferences').and.returnValue(of({}));
spyOn(processListCloudService, 'fetchProcessList').and.returnValue(of(fakeProcessCloudList));
spyOn(component as any, 'createDatatableSchema');
spyOn(component as any, 'createColumns');
component.enableAppChange = true;
component.appName = 'old-app-name';
const appName = new SimpleChange('old-app-name', 'new-app-name', false);
component.appName = 'new-app-name';
component.ngOnChanges({ appName });
expect(preferencesService.getPreferences).toHaveBeenCalledWith('new-app-name');
expect((component as any).createDatatableSchema).toHaveBeenCalled();
expect((component as any).createColumns).toHaveBeenCalled();
});
it('should not call loadPreferencesAndInitialize when appName does not change', () => {
spyOn(preferencesService, 'getPreferences');
const status = new SimpleChange('old-status', 'new-status', false);
component.ngOnChanges({ status });
expect(preferencesService.getPreferences).not.toHaveBeenCalled();
});
});
});
@@ -974,15 +1002,17 @@ describe('ProcessListCloudComponent', () => {
const emptyList = { list: { entries: [] } };
spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(emptyList));
fixture.detectChanges();
expect(component.isLoading).toBe(true);
expect(await firstValueFrom(component.isLoading$)).toBe(true);
expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(true);
fixture.detectChanges();
await fixture.whenStable();
const appName = new SimpleChange(null, 'FAKE-APP-NAME', true);
component.ngOnChanges({ appName });
fixture.detectChanges();
await fixture.whenStable();
expect(await firstValueFrom(component.isLoading$)).toBe(false);
expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(false);
const emptyContent = fixture.debugElement.query(By.css('.adf-empty-content'));
@@ -1223,7 +1253,7 @@ describe('ProcessListCloudComponent: Creating an empty custom template - EmptyTe
});
it('should render the custom template', () => {
fixtureEmpty.componentInstance.processListCloud.isLoading = false;
fixtureEmpty.componentInstance.processListCloud['isLoadingPreferences$'].next(false);
fixtureEmpty.detectChanges();

View File

@@ -64,7 +64,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ProcessVariableFilterModel } from '../../../models/process-variable-filter.model';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { TranslatePipe } from '@ngx-translate/core';
import { NgIf } from '@angular/common';
import { AsyncPipe, NgIf } from '@angular/common';
const PRESET_KEY = 'adf-cloud-process-list.presets';
@@ -81,7 +81,8 @@ const PRESET_KEY = 'adf-cloud-process-list.presets';
EmptyContentComponent,
NoContentTemplateDirective,
LoadingContentTemplateDirective,
NgIf
NgIf,
AsyncPipe
],
templateUrl: './process-list-cloud.component.html',
styleUrls: ['./process-list-cloud.component.scss'],
@@ -290,6 +291,12 @@ export class ProcessListCloudComponent
@Input()
processVariables: ProcessVariableFilterModel[];
/**
* Enables reloading of preferences and process list when appName changes.
*/
@Input()
enableAppChange: boolean = false;
/** Include subprocesses in the process list. */
@Input()
includeSubprocesses: boolean | null = null;
@@ -331,7 +338,6 @@ export class ProcessListCloudComponent
skipCount: number = 0;
currentInstanceId: string;
selectedInstances: any[];
isLoading = true;
rows: any[] = [];
formattedSorting: any[];
@@ -341,6 +347,13 @@ export class ProcessListCloudComponent
private defaultSorting = { key: 'startDate', direction: 'desc' };
protected isLoadingPreferences$ = new BehaviorSubject<boolean>(true);
private readonly isReloadingSubject$ = new BehaviorSubject<boolean>(false);
isLoading$ = combineLatest([this.isLoadingPreferences$, this.isReloadingSubject$]).pipe(
map(([isLoadingPreferences, isReloading]) => isLoadingPreferences || isReloading)
);
private fetchProcessesTrigger$ = new Subject<void>();
constructor(
@@ -361,10 +374,10 @@ export class ProcessListCloudComponent
totalItems: 0
});
combineLatest([this.isColumnSchemaCreated$, this.fetchProcessesTrigger$])
combineLatest([this.isLoadingPreferences$, this.isColumnSchemaCreated$, this.fetchProcessesTrigger$])
.pipe(
tap(() => (this.isLoading = true)),
filter(([isColumnSchemaCreated]) => isColumnSchemaCreated),
tap(() => this.isReloadingSubject$.next(true)),
filter(([isLoadingPreferences, isColumnSchemaCreated]) => !isLoadingPreferences && !!isColumnSchemaCreated),
switchMap(() => {
if (this.searchApiMethod === 'POST') {
const requestNode = this.createProcessListRequestNode();
@@ -373,7 +386,7 @@ export class ProcessListCloudComponent
} else {
const requestNode = this.createRequestNode();
this.requestNode = requestNode;
return this.processListCloudService.getProcessByRequest(requestNode).pipe(take(1));
return this.processListCloudService.getProcessByRequest(requestNode).pipe();
}
}),
takeUntilDestroyed()
@@ -385,18 +398,46 @@ export class ProcessListCloudComponent
this.dataAdapter = new ProcessListDatatableAdapter(this.rows, this.columns);
this.success.emit(processes);
this.isLoading = false;
this.isReloadingSubject$.next(false);
this.pagination.next(processes.list.pagination);
},
error: (error) => {
console.error(error);
this.error.emit(error);
this.isLoading = false;
this.isReloadingSubject$.next(false);
}
});
}
reload() {
if (this.appName || this.appName === '') {
this.isReloadingSubject$.next(true);
this.fetchProcessesTrigger$.next();
} else {
this.rows = [];
}
}
ngAfterContentInit() {
this.retrieveProcessPreferences();
}
ngOnChanges(changes: SimpleChanges) {
if (this.isPropertyChanged(changes, 'sorting')) {
this.formatSorting(changes['sorting'].currentValue);
}
if (changes['appName'] && this.enableAppChange) {
this.retrieveProcessPreferences();
}
if (this.isAnyPropertyChanged(changes)) {
this.reload();
}
}
private retrieveProcessPreferences(): void {
this.isLoadingPreferences$.next(true);
this.cloudPreferenceService
.getPreferences(this.appName)
.pipe(
@@ -411,7 +452,7 @@ export class ProcessListCloudComponent
return {
columnsOrder: columnsOrder ? JSON.parse(columnsOrder.entry.value) : undefined,
columnsVisibility: columnsVisibility ? JSON.parse(columnsVisibility.entry.value) : this.columnsVisibility,
columnsVisibility: columnsVisibility ? JSON.parse(columnsVisibility.entry.value) : undefined,
columnsWidths: columnsWidths ? JSON.parse(columnsWidths.entry.value) : undefined
};
}),
@@ -419,7 +460,7 @@ export class ProcessListCloudComponent
if (error.status === 404) {
return of({
columnsOrder: undefined,
columnsVisibility: this.columnsVisibility,
columnsVisibility: undefined,
columnsWidths: undefined
});
} else {
@@ -427,44 +468,37 @@ export class ProcessListCloudComponent
}
})
)
.subscribe(({ columnsOrder, columnsVisibility, columnsWidths }) => {
if (columnsVisibility) {
this.columnsVisibility = columnsVisibility;
.subscribe(
({ columnsOrder, columnsVisibility, columnsWidths }) => {
if (columnsVisibility) {
this.columnsVisibility = columnsVisibility;
}
if (columnsOrder) {
this.columnsOrder = columnsOrder;
}
if (columnsWidths) {
this.columnsWidths = columnsWidths;
}
this.createDatatableSchema();
if (this.enableAppChange) {
this.createColumns();
}
this.isLoadingPreferences$.next(false);
},
(error) => {
this.error.emit(error);
this.isLoadingPreferences$.next(false);
}
if (columnsOrder) {
this.columnsOrder = columnsOrder;
}
if (columnsWidths) {
this.columnsWidths = columnsWidths;
}
this.createDatatableSchema();
});
}
ngOnChanges(changes: SimpleChanges) {
if (this.isPropertyChanged(changes, 'sorting')) {
this.formatSorting(changes['sorting'].currentValue);
}
if (this.isAnyPropertyChanged(changes)) {
this.reload();
}
);
}
getCurrentId(): string {
return this.currentInstanceId;
}
reload() {
if (this.appName || this.appName === '') {
this.fetchProcessesTrigger$.next();
} else {
this.rows = [];
}
}
private isAnyPropertyChanged(changes: SimpleChanges): boolean {
for (const property in changes) {
if (this.isPropertyChanged(changes, property)) {