RED-10422 - improved number of requests with help of new changes endpoint

This commit is contained in:
Timo Bejan 2024-11-07 15:41:33 +02:00
parent 3d26a73b05
commit cdc5d2ee6c
6 changed files with 99 additions and 43 deletions

View File

@ -41,7 +41,7 @@ import { Roles } from '@users/roles';
import { UserPreferenceService } from '@users/user-preference.service';
import { convertFiles, Files, handleFileDrop } from '@utils/index';
import { merge, Observable } from 'rxjs';
import { filter, skip, switchMap, tap } from 'rxjs/operators';
import { filter, map, skip, switchMap, tap } from 'rxjs/operators';
import { ConfigService } from '../config.service';
import { BulkActionsService } from '../services/bulk-actions.service';
import { DossiersCacheService } from '@services/dossiers/dossiers-cache.service';
@ -145,8 +145,9 @@ export default class DossierOverviewScreenComponent extends ListingComponent<Fil
get #dossierFilesChange$() {
return this._dossiersService.dossierFileChanges$.pipe(
filter(dossierId => dossierId === this.dossierId && !!this._dossiersCacheService.get(dossierId)),
switchMap(dossierId => this._filesService.loadAll(dossierId)),
map(changes => changes[this.dossierId]),
filter(changes => !!changes && !!this._dossiersCacheService.get(this.dossierId)),
switchMap(changes => this._filesService.loadByIds({ [this.dossierId]: changes }).pipe(map(files => files[this.dossierId]))),
);
}

View File

@ -128,7 +128,8 @@ export class FilePreviewStateService {
get #dossierFilesChange$() {
return this._dossiersService.dossierFileChanges$.pipe(
filter(dossierId => dossierId === this.dossierId),
map(changes => changes[this.dossierId]),
filter(fileIds => fileIds && fileIds.length > 0),
map(() => true),
);
}

View File

@ -1,17 +1,16 @@
import { GenericService, QueryParam, ROOT_CHANGES_KEY } from '@iqser/common-ui';
import { Dossier, DossierStats, IDossierChanges } from '@red/domain';
import { forkJoin, Observable, of, Subscription, throwError, timer } from 'rxjs';
import { catchError, filter, map, switchMap, take, tap } from 'rxjs/operators';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { GenericService, ROOT_CHANGES_KEY } from '@iqser/common-ui';
import { Dossier, DossierStats, IChangesDetails } from '@red/domain';
import { forkJoin, Observable, Subscription, timer } from 'rxjs';
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
import { NGXLogger } from 'ngx-logger';
import { ActiveDossiersService } from './active-dossiers.service';
import { ArchivedDossiersService } from './archived-dossiers.service';
import { inject, Injectable, OnDestroy } from '@angular/core';
import { DashboardStatsService } from '../dossier-templates/dashboard-stats.service';
import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
import { List } from '@iqser/common-ui/lib/utils';
import { Router } from '@angular/router';
import { filterEventsOnPages } from '@utils/operators';
import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
@Injectable({ providedIn: 'root' })
export class DossiersChangesService extends GenericService<Dossier> implements OnDestroy {
@ -19,40 +18,39 @@ export class DossiersChangesService extends GenericService<Dossier> implements O
readonly #activeDossiersService = inject(ActiveDossiersService);
readonly #archivedDossiersService = inject(ArchivedDossiersService);
readonly #dashboardStatsService = inject(DashboardStatsService);
readonly #dossierStatsService = inject(DossierStatsService);
readonly #logger = inject(NGXLogger);
readonly #router = inject(Router);
protected readonly _defaultModelPath = 'dossier';
loadOnlyChanged(): Observable<IDossierChanges> {
const removeIfNotFound = (id: string) =>
catchError((error: unknown) => {
if (error instanceof HttpErrorResponse && error.status === HttpStatusCode.NotFound) {
this.#activeDossiersService.remove(id);
this.#archivedDossiersService.remove(id);
return of([]);
}
return throwError(() => error);
});
loadOnlyChanged(): Observable<IChangesDetails> {
const load = (changes: IChangesDetails) => this.#load(changes.dossierChanges.map(d => d.dossierId));
const load = (changes: IDossierChanges) =>
changes.map(change => this.#load(change.dossierId).pipe(removeIfNotFound(change.dossierId)));
const loadStats = (change: IChangesDetails) => {
const dossierStatsToLoad = new Set<string>();
change.dossierChanges.forEach(dossierChange => dossierStatsToLoad.add(dossierChange.dossierId));
change.fileChanges.forEach(fileChange => dossierStatsToLoad.add(fileChange.dossierId));
return this.#dossierStatsService.getFor(Array.from(dossierStatsToLoad));
};
return this.hasChangesDetails$().pipe(
tap(changes => this.#logger.info('[DOSSIERS_CHANGES] Found changes', changes)),
switchMap(dossierChanges =>
forkJoin([...load(dossierChanges), this.#dashboardStatsService.loadAll().pipe(take(1))]).pipe(map(() => dossierChanges)),
forkJoin([load(dossierChanges), loadStats(dossierChanges), this.#dashboardStatsService.loadAll().pipe(take(1))]).pipe(
map(() => dossierChanges),
),
),
);
}
hasChangesDetails$(): Observable<IDossierChanges> {
hasChangesDetails$(): Observable<IChangesDetails> {
const body = { value: this._lastCheckedForChanges.get(ROOT_CHANGES_KEY) };
const dateBeforeRequest = new Date().toISOString();
this.#logger.info('[DOSSIERS_CHANGES] Check with Last Checked Date', body.value);
return this._post<IDossierChanges>(body, `${this._defaultModelPath}/changes/details`).pipe(
filter(changes => changes.length > 0),
return this._post<IChangesDetails>(body, `${this._defaultModelPath}/changes/details/v2`).pipe(
filter(changes => changes.dossierChanges.length > 0 || changes.fileChanges.length > 0),
tap(() => this._lastCheckedForChanges.set(ROOT_CHANGES_KEY, dateBeforeRequest)),
tap(() => this.#logger.info('[DOSSIERS_CHANGES] Save Last Checked Date value', dateBeforeRequest)),
);
@ -75,17 +73,27 @@ export class DossiersChangesService extends GenericService<Dossier> implements O
this.#subscription.unsubscribe();
}
#load(id: string): Observable<DossierStats[]> {
const queryParams: List<QueryParam> = [{ key: 'includeArchived', value: true }];
return super._getOne([id], this._defaultModelPath, queryParams).pipe(
map(entity => new Dossier(entity)),
switchMap((dossier: Dossier) => {
if (dossier.isArchived) {
this.#activeDossiersService.remove(dossier.id);
return this.#archivedDossiersService.updateDossier(dossier);
}
this.#archivedDossiersService.remove(dossier.id);
return this.#activeDossiersService.updateDossier(dossier);
getByIds(ids: string[]) {
return super._post<Record<string, Dossier>>({ value: ids }, `${this._defaultModelPath}/by-id`);
}
#load(ids: string[]): Observable<Dossier[]> {
return this.getByIds(ids).pipe(
map(entity => {
return Object.values(entity).map(dossier => new Dossier(dossier));
}),
map((dossiers: Dossier[]) => {
const archivedDossiers = dossiers.filter(dossier => dossier.isArchived);
const deletedDossiers = dossiers.filter(dossier => dossier.isSoftDeleted);
const activeDossiers = dossiers.filter(dossier => !dossier.isArchived && !dossier.isSoftDeleted);
archivedDossiers.forEach(dossier => this.#activeDossiersService.remove(dossier.id));
activeDossiers.forEach(dossier => this.#archivedDossiersService.remove(dossier.id));
deletedDossiers.forEach(dossier => this.#activeDossiersService.remove(dossier.id));
this.#activeDossiersService.updateDossiers(activeDossiers);
this.#archivedDossiersService.updateDossiers(archivedDossiers);
return dossiers;
}),
);
}

View File

@ -1,5 +1,5 @@
import { EntitiesService, Toaster } from '@iqser/common-ui';
import { Dossier, DossierStats, IDossier, IDossierChanges, IDossierRequest } from '@red/domain';
import { Dossier, DossierFileChanges, DossierStats, IChangesDetails, IDossier, IDossierRequest } from '@red/domain';
import { Observable, of, Subject } from 'rxjs';
import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { inject } from '@angular/core';
@ -17,7 +17,7 @@ export abstract class DossiersService extends EntitiesService<IDossier, Dossier>
protected readonly _toaster = inject(Toaster);
protected readonly _entityClass = Dossier;
protected abstract readonly _defaultModelPath: string;
readonly dossierFileChanges$ = new Subject<string>();
readonly dossierFileChanges$ = new Subject<DossierFileChanges>();
abstract readonly routerPath: string;
createOrUpdate(dossier: IDossierRequest): Observable<Dossier> {
@ -52,7 +52,18 @@ export abstract class DossiersService extends EntitiesService<IDossier, Dossier>
return this._dossierStatsService.getFor([dossier.id]);
}
emitFileChanges(dossierChanges: IDossierChanges): void {
dossierChanges.filter(change => change.fileChanges).forEach(change => this.dossierFileChanges$.next(change.dossierId));
updateDossiers(dossier: Dossier[]): void {
dossier.forEach(d => this.replace(d));
}
emitFileChanges(changes: IChangesDetails): void {
const changeModel: DossierFileChanges = {};
changes.fileChanges.forEach(change => {
if (!changeModel[change.dossierId]) {
changeModel[change.dossierId] = [];
}
changeModel[change.dossierId].push(change.fileId);
});
this.dossierFileChanges$.next(changeModel);
}
}

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { EntitiesService, isArray, QueryParam } from '@iqser/common-ui';
import { List, mapEach } from '@iqser/common-ui/lib/utils';
import { ApproveResponse, File, IFile } from '@red/domain';
import { ApproveResponse, Dossier, DossierFileChanges, File, IFile } from '@red/domain';
import { UserService } from '@users/user.service';
import { NGXLogger } from 'ngx-logger';
import { firstValueFrom } from 'rxjs';
@ -27,8 +27,35 @@ export class FilesService extends EntitiesService<IFile, File> {
super();
}
loadByIds(dossierFileChanges: DossierFileChanges) {
const filesByDossier$ = super
._post<{ value: Record<string, IFile[]> }>({ value: dossierFileChanges }, `${this._defaultModelPath}/by-id`)
.pipe(
map(response => {
const filesByDossier = response.value;
const result: Record<string, File[]> = {};
for (const key of Object.keys(filesByDossier)) {
result[key] = filesByDossier[key].map(file => new File(file, this._userService.getName(file.assignee)));
result[key].forEach(file => this._logger.info('[FILE] Loaded', file));
}
return result;
}),
);
return filesByDossier$.pipe(
tap(files => {
for (const key of Object.keys(files)) {
const notDeletedFiles = files[key].filter(file => !file.deleted);
const deletedFiles = files[key].filter(file => file.deleted);
this._filesMapService.replace(key, notDeletedFiles);
deletedFiles.map(file => file.id).forEach(id => this._filesMapService.delete(key, id));
}
}),
);
}
/** Reload dossier files + stats. */
loadAll(dossierId: string) {
console.log('loadAll');
const files$ = this.getFor(dossierId).pipe(
mapEach(file => new File(file, this._userService.getName(file.assignee))),
tap(file => this._logger.info('[FILE] Loaded', file)),

View File

@ -1,11 +1,19 @@
export interface IDossierChange {
readonly dossierChanges: boolean;
readonly dossierId: string;
readonly fileChanges: boolean;
readonly lastUpdated: string;
}
export interface IFileChange extends IDossierChange {
readonly fileId: string;
}
export type IDossierChanges = readonly IDossierChange[];
export type IFileChanges = readonly IFileChange[];
export interface IChangesDetails {
readonly dossierChanges: IDossierChanges;
readonly fileChanges: IFileChanges;
}
export type DossierFileChanges = Record<string, string[]>;