From e50b5bd56768fdbdf027753b48bebedebe143cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adina=20=C8=9Aeudan?= Date: Tue, 22 Feb 2022 23:31:47 +0200 Subject: [PATCH] Trash dossiers service --- .../src/app/guards/trash-dossiers.guard.ts | 14 +++ .../notifications/notifications.module.ts | 2 +- .../user-profile/user-profile.module.ts | 2 +- .../app/modules/admin/admin-routing.module.ts | 9 +- .../screens/trash/trash-screen.component.ts | 109 +++++------------- .../modules/archive/archive-routing.module.ts | 2 +- .../edit-dossier-general-info.component.ts | 4 +- .../active-dossiers.service.ts | 37 +----- .../entity-services/trash-dossiers.service.ts | 83 +++++++++++++ libs/red-domain/src/index.ts | 1 + .../red-domain/src/lib/trash-dossier/index.ts | 1 + .../lib/trash-dossier/trash-dossier.model.ts | 69 +++++++++++ 12 files changed, 207 insertions(+), 126 deletions(-) create mode 100644 apps/red-ui/src/app/guards/trash-dossiers.guard.ts create mode 100644 apps/red-ui/src/app/services/entity-services/trash-dossiers.service.ts create mode 100644 libs/red-domain/src/lib/trash-dossier/index.ts create mode 100644 libs/red-domain/src/lib/trash-dossier/trash-dossier.model.ts diff --git a/apps/red-ui/src/app/guards/trash-dossiers.guard.ts b/apps/red-ui/src/app/guards/trash-dossiers.guard.ts new file mode 100644 index 000000000..70587e75f --- /dev/null +++ b/apps/red-ui/src/app/guards/trash-dossiers.guard.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@angular/core'; +import { CanActivate } from '@angular/router'; +import { firstValueFrom } from 'rxjs'; +import { TrashDossiersService } from '../services/entity-services/trash-dossiers.service'; + +@Injectable({ providedIn: 'root' }) +export class TrashDossiersGuard implements CanActivate { + constructor(private readonly _trashDossiersService: TrashDossiersService) {} + + async canActivate(): Promise { + await firstValueFrom(this._trashDossiersService.loadAll()); + return true; + } +} diff --git a/apps/red-ui/src/app/modules/account/screens/notifications/notifications.module.ts b/apps/red-ui/src/app/modules/account/screens/notifications/notifications.module.ts index ef7874558..5f9b8c77b 100644 --- a/apps/red-ui/src/app/modules/account/screens/notifications/notifications.module.ts +++ b/apps/red-ui/src/app/modules/account/screens/notifications/notifications.module.ts @@ -3,7 +3,7 @@ import { RouterModule } from '@angular/router'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { NotificationsScreenComponent } from './notifications-screen/notifications-screen.component'; -import { PendingChangesGuard } from '../../../../guards/can-deactivate.guard'; +import { PendingChangesGuard } from '@guards/can-deactivate.guard'; const routes = [{ path: '', component: NotificationsScreenComponent, canDeactivate: [PendingChangesGuard] }]; diff --git a/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile.module.ts b/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile.module.ts index 775a99569..78c3ec932 100644 --- a/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile.module.ts +++ b/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile.module.ts @@ -3,7 +3,7 @@ import { RouterModule } from '@angular/router'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { UserProfileScreenComponent } from './user-profile-screen/user-profile-screen.component'; -import { PendingChangesGuard } from '../../../../guards/can-deactivate.guard'; +import { PendingChangesGuard } from '@guards/can-deactivate.guard'; const routes = [{ path: '', component: UserProfileScreenComponent, canDeactivate: [PendingChangesGuard] }]; diff --git a/apps/red-ui/src/app/modules/admin/admin-routing.module.ts b/apps/red-ui/src/app/modules/admin/admin-routing.module.ts index bb0a2a0e8..180801557 100644 --- a/apps/red-ui/src/app/modules/admin/admin-routing.module.ts +++ b/apps/red-ui/src/app/modules/admin/admin-routing.module.ts @@ -17,11 +17,12 @@ import { TrashScreenComponent } from './screens/trash/trash-screen.component'; import { GeneralConfigScreenComponent } from './screens/general-config/general-config-screen.component'; import { BaseAdminScreenComponent } from './base-admin-screen/base-admin-screen.component'; import { BaseDossierTemplateScreenComponent } from './base-dossier-templates-screen/base-dossier-template-screen.component'; -import { DossierTemplatesGuard } from '../../guards/dossier-templates.guard'; +import { DossierTemplatesGuard } from '@guards/dossier-templates.guard'; import { DICTIONARY_TYPE, DOSSIER_TEMPLATE_ID } from '@utils/constants'; -import { DossierTemplateExistsGuard } from '../../guards/dossier-template-exists.guard'; -import { DictionaryExistsGuard } from '../../guards/dictionary-exists.guard'; +import { DossierTemplateExistsGuard } from '@guards/dossier-template-exists.guard'; +import { DictionaryExistsGuard } from '@guards/dictionary-exists.guard'; import { DossierStatesListingScreenComponent } from './screens/dossier-states-listing/dossier-states-listing-screen.component'; +import { TrashDossiersGuard } from '@guards/trash-dossiers.guard'; const routes: Routes = [ { path: '', redirectTo: 'dossier-templates', pathMatch: 'full' }, @@ -200,7 +201,7 @@ const routes: Routes = [ component: TrashScreenComponent, canActivate: [CompositeRouteGuard], data: { - routeGuards: [AuthGuard, RedRoleGuard], + routeGuards: [AuthGuard, RedRoleGuard, TrashDossiersGuard], requiredRoles: ['RED_MANAGER'], }, }, diff --git a/apps/red-ui/src/app/modules/admin/screens/trash/trash-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/trash/trash-screen.component.ts index ddb82b7d0..188ad23c7 100644 --- a/apps/red-ui/src/app/modules/admin/screens/trash/trash-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/trash/trash-screen.component.ts @@ -1,45 +1,42 @@ -import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, forwardRef, Injector } from '@angular/core'; import { CircleButtonTypes, ConfirmationDialogInput, - DefaultListingServices, - IListable, + DefaultListingServicesTmp, + EntitiesService, ListingComponent, LoadingService, SortingOrders, TableColumnConfig, TitleColors, } from '@iqser/common-ui'; -import { ConfigService } from '@services/config.service'; -import * as moment from 'moment'; -import { ActiveDossiersService } from '@services/entity-services/active-dossiers.service'; import { AdminDialogService } from '../../services/admin-dialog.service'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { Observable } from 'rxjs'; import { distinctUntilChanged, map } from 'rxjs/operators'; -import { getLeftDateTime } from '@utils/functions'; import { RouterHistoryService } from '@services/router-history.service'; -import { IDossier } from '@red/domain'; -import { PermissionsService } from '@services/permissions.service'; - -interface DossierListItem extends IDossier, IListable { - readonly canRestore: boolean; - readonly canHardDelete: boolean; - readonly restoreDate: string; -} +import { TrashDossier } from '@red/domain'; +import { TrashDossiersService } from '@services/entity-services/trash-dossiers.service'; @Component({ templateUrl: './trash-screen.component.html', styleUrls: ['./trash-screen.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => TrashScreenComponent) }], + providers: [ + ...DefaultListingServicesTmp, + { + provide: EntitiesService, + useExisting: TrashDossiersService, + }, + { provide: ListingComponent, useExisting: forwardRef(() => TrashScreenComponent) }, + ], }) -export class TrashScreenComponent extends ListingComponent implements OnInit { +export class TrashScreenComponent extends ListingComponent { readonly circleButtonTypes = CircleButtonTypes; readonly tableHeaderLabel = _('trash.table-header.title'); readonly canRestoreSelected$ = this._canRestoreSelected$; readonly canHardDeleteSelected$ = this._canHardDeleteSelected$; - readonly tableColumnConfigs: TableColumnConfig[] = [ + readonly tableColumnConfigs: TableColumnConfig[] = [ { label: _('trash.table-col-names.name'), sortByKey: 'searchKey' }, { label: _('trash.table-col-names.owner'), class: 'user-column' }, { label: _('trash.table-col-names.deleted-on'), sortByKey: 'softDeletedTime' }, @@ -49,13 +46,16 @@ export class TrashScreenComponent extends ListingComponent impl constructor( protected readonly _injector: Injector, private readonly _loadingService: LoadingService, - private readonly _permissionsService: PermissionsService, - private readonly _activeDossiersService: ActiveDossiersService, + private readonly _trashDossiersService: TrashDossiersService, readonly routerHistoryService: RouterHistoryService, - private readonly _configService: ConfigService, private readonly _adminDialogService: AdminDialogService, ) { super(_injector); + + this.sortingService.setSortingOption({ + column: 'softDeletedTime', + order: SortingOrders.desc, + }); } private get _canRestoreSelected$(): Observable { @@ -72,17 +72,7 @@ export class TrashScreenComponent extends ListingComponent impl ); } - disabledFn = (dossier: DossierListItem) => !dossier.canRestore; - - async ngOnInit(): Promise { - this._loadingService.start(); - await this._loadDossiersData(); - this.sortingService.setSortingOption({ - column: 'softDeletedTime', - order: SortingOrders.desc, - }); - this._loadingService.stop(); - } + disabledFn = (dossier: TrashDossier) => !dossier.canRestore; hardDelete(dossiers = this.listingService.selected): void { const data = new ConfirmationDialogInput({ @@ -95,60 +85,13 @@ export class TrashScreenComponent extends ListingComponent impl }, }); this._adminDialogService.openDialog('confirm', null, data, () => { - this._loadingService.loadWhile(this._hardDelete(dossiers)); + const dossierIds: string[] = dossiers.map(d => d.id); + this._loadingService.loadWhile(this._trashDossiersService.hardDelete(dossierIds)); }); } restore(dossiers = this.listingService.selected): void { - this._loadingService.loadWhile(this._restore(dossiers)); - } - - private _getRestoreDate(softDeletedTime: string): string { - return moment(softDeletedTime).add(this._configService.values.DELETE_RETENTION_HOURS, 'hours').toISOString(); - } - - private async _loadDossiersData(): Promise { - this.entitiesService.setEntities(this._toListItems(await this._activeDossiersService.getDeleted()).filter(d => d.canRestore)); - } - - private _canRestoreDossier(restoreDate: string): boolean { - const { daysLeft, hoursLeft, minutesLeft } = getLeftDateTime(restoreDate); - - return daysLeft >= 0 && hoursLeft >= 0 && minutesLeft > 0; - } - - private _toListItems(dossiers: IDossier[]): DossierListItem[] { - return dossiers.map(dossier => this._toListItem(dossier)); - } - - private _toListItem(dossier: IDossier): DossierListItem { - const restoreDate = this._getRestoreDate(dossier.softDeletedTime); - return { - id: dossier.dossierId, - ...dossier, - searchKey: dossier.dossierName, - restoreDate, - canRestore: this._canRestoreDossier(restoreDate), - canHardDelete: this._permissionsService.canDeleteDossier(dossier), - // Because of migrations, for some this is not set - softDeletedTime: dossier.softDeletedTime || '-', - }; - } - - private async _restore(dossiers: DossierListItem[]): Promise { - const dossierIds = dossiers.map(d => d.id); - await this._activeDossiersService.restore(dossierIds); - this._removeFromList(dossierIds); - } - - private async _hardDelete(dossiers: DossierListItem[]) { - const dossierIds = dossiers.map(d => d.id); - await this._activeDossiersService.hardDelete(dossierIds); - this._removeFromList(dossierIds); - } - - private _removeFromList(ids: string[]): void { - const entities = this.entitiesService.all.filter(e => !ids.includes(e.id)); - this.entitiesService.setEntities(entities); + const dossierIds: string[] = dossiers.map(d => d.id); + this._loadingService.loadWhile(this._trashDossiersService.restore(dossierIds)); } } diff --git a/apps/red-ui/src/app/modules/archive/archive-routing.module.ts b/apps/red-ui/src/app/modules/archive/archive-routing.module.ts index 36e88ed79..905da9c09 100644 --- a/apps/red-ui/src/app/modules/archive/archive-routing.module.ts +++ b/apps/red-ui/src/app/modules/archive/archive-routing.module.ts @@ -5,7 +5,7 @@ import { ArchivedDossiersScreenComponent } from './screens/archived-dossiers-scr import { DOSSIER_ID } from '@utils/constants'; import { CompositeRouteGuard } from '@iqser/common-ui'; import { ARCHIVED_DOSSIERS_SERVICE } from '../../tokens'; -import { DossierFilesGuard } from '../../guards/dossier-files-guard'; +import { DossierFilesGuard } from '@guards/dossier-files-guard'; const routes: Routes = [ { diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.ts index 7bf60d961..5062d6720 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.ts @@ -17,6 +17,7 @@ import { DossierStateService } from '@services/entity-services/dossier-state.ser import { DOSSIER_TEMPLATE_ID } from '@utils/constants'; import { TranslateService } from '@ngx-translate/core'; import { DossiersService } from '@services/entity-services/dossiers.service'; +import { TrashDossiersService } from '@services/entity-services/trash-dossiers.service'; @Component({ selector: 'redaction-edit-dossier-general-info', @@ -39,6 +40,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti private readonly _dossierStateService: DossierStateService, private readonly _dossierTemplatesService: DossierTemplatesService, private readonly _dossiersService: DossiersService, + private readonly _trashDossiersService: TrashDossiersService, private readonly _dossierStatsService: DossierStatsService, private readonly _formBuilder: FormBuilder, private readonly _dialogService: DossiersDialogService, @@ -139,7 +141,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti }, }); this._dialogService.openDialog('confirm', null, data, async () => { - await firstValueFrom(this._dossiersService.delete(this.dossier)); + await firstValueFrom(this._trashDossiersService.delete(this.dossier)); this._editDossierDialogRef.close(); this._router.navigate(['main', 'dossiers']).then(() => this.#notifyDossierDeleted()); }); diff --git a/apps/red-ui/src/app/services/entity-services/active-dossiers.service.ts b/apps/red-ui/src/app/services/entity-services/active-dossiers.service.ts index 7f842c2c5..b252b43d2 100644 --- a/apps/red-ui/src/app/services/entity-services/active-dossiers.service.ts +++ b/apps/red-ui/src/app/services/entity-services/active-dossiers.service.ts @@ -1,9 +1,6 @@ import { Injectable, Injector } from '@angular/core'; -import { List, QueryParam, RequiredParam, Validate } from '@iqser/common-ui'; -import { Dossier, IDossier } from '@red/domain'; -import { catchError, switchMap, tap } from 'rxjs/operators'; -import { firstValueFrom, Observable, of, timer } from 'rxjs'; -import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; +import { switchMap, tap } from 'rxjs/operators'; +import { timer } from 'rxjs'; import { CHANGED_CHECK_INTERVAL } from '@utils/constants'; import { DossiersService } from '@services/entity-services/dossiers.service'; @@ -27,37 +24,7 @@ export class ActiveDossiersService extends DossiersService { .subscribe(); } - getDeleted(): Promise { - return firstValueFrom(this.getAll('deleted-dossiers')); - } - - delete(dossier: Dossier): Observable { - const showToast = () => { - this._toaster.error(_('dossier-listing.delete.delete-failed'), { params: dossier }); - return of({}); - }; - return super.delete(dossier.dossierId).pipe( - tap(() => this.#removeDossiers([dossier])), - catchError(showToast), - ); - } - - @Validate() - restore(@RequiredParam() dossierIds: List): Promise { - return firstValueFrom(this._post(dossierIds, 'deleted-dossiers/restore').pipe(switchMap(() => this.loadAll()))); - } - - @Validate() - hardDelete(@RequiredParam() dossierIds: List): Promise { - const body = dossierIds.map(id => ({ key: 'dossierId', value: id })); - return firstValueFrom(super.delete(body, 'deleted-dossiers/hard-delete', body)); - } - getCountWithState(dossierStatusId: string): number { return this.all.filter(dossier => dossier.dossierStatusId === dossierStatusId).length; } - - #removeDossiers(dossiers: Dossier[]): void { - this.setEntities(this.all.filter(dossier => !dossiers.find(d => dossier.id === d.id))); - } } diff --git a/apps/red-ui/src/app/services/entity-services/trash-dossiers.service.ts b/apps/red-ui/src/app/services/entity-services/trash-dossiers.service.ts new file mode 100644 index 000000000..a1ebc5a88 --- /dev/null +++ b/apps/red-ui/src/app/services/entity-services/trash-dossiers.service.ts @@ -0,0 +1,83 @@ +import { Injectable, Injector } from '@angular/core'; +import { EntitiesService, mapEach, QueryParam, RequiredParam, Toaster, Validate } from '@iqser/common-ui'; +import { Dossier, IDossier, TrashDossier } from '@red/domain'; +import { catchError, switchMap, tap } from 'rxjs/operators'; +import { firstValueFrom, Observable, of } from 'rxjs'; +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; +import * as moment from 'moment'; +import { ConfigService } from '../config.service'; +import { PermissionsService } from '../permissions.service'; +import { ActiveDossiersService } from '@services/entity-services/active-dossiers.service'; + +export interface IDossiersStats { + totalPeople: number; + totalAnalyzedPages: number; +} + +@Injectable({ + providedIn: 'root', +}) +export class TrashDossiersService extends EntitiesService { + constructor( + protected readonly _injector: Injector, + private readonly _toaster: Toaster, + private readonly _configService: ConfigService, + private readonly _permissionsService: PermissionsService, + private readonly _activeDossiersService: ActiveDossiersService, + ) { + super(_injector, TrashDossier, 'dossier'); + } + + loadAll(): Observable { + return this.#getDeleted().pipe( + mapEach( + dossier => new TrashDossier(dossier, this.#getRestoreDate(dossier), this._permissionsService.canDeleteDossier(dossier)), + ), + tap(dossiers => this.setEntities(dossiers)), + ); + } + + delete(dossier: Dossier): Observable { + const showToast = () => { + this._toaster.error(_('dossier-listing.delete.delete-failed'), { params: dossier }); + return of({}); + }; + return super.delete(dossier.dossierId).pipe( + switchMap(() => this._activeDossiersService.loadAll()), + catchError(showToast), + ); + } + + @Validate() + restore(@RequiredParam() dossierIds: string[]): Promise { + return firstValueFrom( + this._post(dossierIds, 'deleted-dossiers/restore').pipe( + switchMap(() => this._activeDossiersService.loadAll()), + tap(() => this.#removeDossiers(dossierIds)), + ), + ); + } + + @Validate() + hardDelete(@RequiredParam() dossierIds: string[]): Promise { + const body = dossierIds.map(id => ({ key: 'dossierId', value: id })); + return firstValueFrom( + super.delete(body, 'deleted-dossiers/hard-delete', body).pipe( + switchMap(() => this._activeDossiersService.loadAll()), + tap(() => this.#removeDossiers(dossierIds)), + ), + ); + } + + #getRestoreDate(dossier: IDossier): string { + return moment(dossier.softDeletedTime).add(this._configService.values.DELETE_RETENTION_HOURS, 'hours').toISOString(); + } + + #getDeleted(): Observable { + return this.getAll('deleted-dossiers'); + } + + #removeDossiers(dossierIds: string[]): void { + this.setEntities(this.all.filter(dossier => !dossierIds.includes(dossier.id))); + } +} diff --git a/libs/red-domain/src/index.ts b/libs/red-domain/src/index.ts index e2d4bf0e4..7349bef8b 100644 --- a/libs/red-domain/src/index.ts +++ b/libs/red-domain/src/index.ts @@ -20,3 +20,4 @@ export * from './lib/signature'; export * from './lib/legal-basis'; export * from './lib/dossier-stats'; export * from './lib/dossier-state'; +export * from './lib/trash-dossier'; diff --git a/libs/red-domain/src/lib/trash-dossier/index.ts b/libs/red-domain/src/lib/trash-dossier/index.ts new file mode 100644 index 000000000..a90f3227f --- /dev/null +++ b/libs/red-domain/src/lib/trash-dossier/index.ts @@ -0,0 +1 @@ +export * from './trash-dossier.model'; diff --git a/libs/red-domain/src/lib/trash-dossier/trash-dossier.model.ts b/libs/red-domain/src/lib/trash-dossier/trash-dossier.model.ts new file mode 100644 index 000000000..0988653f1 --- /dev/null +++ b/libs/red-domain/src/lib/trash-dossier/trash-dossier.model.ts @@ -0,0 +1,69 @@ +import { List } from '@iqser/common-ui'; +import { DownloadFileType } from '../shared'; +import { DossierStatus, IDossier } from '@red/domain'; +import { getLeftDateTime } from '@utils/functions'; + +export class TrashDossier implements IDossier { + readonly dossierId: string; + readonly dossierTemplateId: string; + readonly ownerId: string; + readonly memberIds: List; + readonly approverIds: List; + readonly reportTemplateIds: List; + readonly dossierName: string; + readonly dossierStatusId: string; + readonly date: string; + readonly dueDate?: string; + readonly description?: string; + readonly downloadFileTypes?: List; + readonly hardDeletedTime?: string; + readonly softDeletedTime?: string; + readonly startDate?: string; + readonly status: DossierStatus; + readonly watermarkEnabled: boolean; + readonly watermarkPreviewEnabled: boolean; + readonly archivedTime: string; + readonly hasReviewers: boolean; + readonly canRestore: boolean; + + constructor(dossier: IDossier, readonly restoreDate: string, readonly canHardDelete: boolean) { + this.dossierId = dossier.dossierId; + this.approverIds = dossier.approverIds; + this.date = dossier.date; + this.description = dossier.description; + this.dossierName = dossier.dossierName; + this.dossierStatusId = dossier.dossierStatusId; + this.dossierTemplateId = dossier.dossierTemplateId; + this.downloadFileTypes = dossier.downloadFileTypes; + this.dueDate = dossier.dueDate; + this.hardDeletedTime = dossier.hardDeletedTime; + this.memberIds = dossier.memberIds; + this.ownerId = dossier.ownerId; + this.reportTemplateIds = dossier.reportTemplateIds; + this.softDeletedTime = dossier.softDeletedTime; + this.startDate = dossier.startDate; + this.status = dossier.status; + this.watermarkEnabled = dossier.watermarkEnabled; + this.watermarkPreviewEnabled = dossier.watermarkPreviewEnabled; + this.archivedTime = dossier.archivedTime; + this.hasReviewers = !!this.memberIds && this.memberIds.length > 1; + + this.canRestore = this.#canRestoreDossier(restoreDate); + // Because of migrations, for some this is not set + this.softDeletedTime = dossier.softDeletedTime || '-'; + } + + get id(): string { + return this.dossierId; + } + + get searchKey(): string { + return this.dossierName; + } + + #canRestoreDossier(restoreDate: string): boolean { + const { daysLeft, hoursLeft, minutesLeft } = getLeftDateTime(restoreDate); + + return daysLeft >= 0 && hoursLeft >= 0 && minutesLeft > 0; + } +}