diff --git a/apps/red-ui/src/app/components/toast/toast.component.html b/apps/red-ui/src/app/components/toast/toast.component.html index 93c4004fa..cfffe69b4 100644 --- a/apps/red-ui/src/app/components/toast/toast.component.html +++ b/apps/red-ui/src/app/components/toast/toast.component.html @@ -7,7 +7,7 @@ {{ message }} -
+
{{ action.title }} diff --git a/apps/red-ui/src/app/components/toast/toast.component.ts b/apps/red-ui/src/app/components/toast/toast.component.ts index e434030cb..e61e2b967 100644 --- a/apps/red-ui/src/app/components/toast/toast.component.ts +++ b/apps/red-ui/src/app/components/toast/toast.component.ts @@ -1,8 +1,8 @@ import { Component } from '@angular/core'; import { Toast, ToastPackage, ToastrService } from 'ngx-toastr'; +import { ToasterOptions } from '@services/toaster.service'; @Component({ - selector: 'redaction-toast', templateUrl: './toast.component.html', styleUrls: ['./toast.component.scss'] }) @@ -12,12 +12,10 @@ export class ToastComponent extends Toast { } get actions() { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return this.options.actions; + return (this.options as ToasterOptions)?.actions; } - callAction($event: MouseEvent, action: Function) { + callAction($event: MouseEvent, action: () => void) { $event.stopPropagation(); if (action) { action(); diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component.ts index f77311212..eb80f32d9 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component.ts @@ -3,7 +3,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { DictionaryControllerService, TypeValue } from '@redaction/red-ui-http'; import { Observable } from 'rxjs'; -import { NotificationService, NotificationType } from '@services/notification.service'; +import { Toaster } from '@services/toaster.service'; import { TranslateService } from '@ngx-translate/core'; import { TypeValueWrapper } from '@models/file/type-value.wrapper'; import { humanize } from '../../../../utils/functions'; @@ -21,7 +21,7 @@ export class AddEditDictionaryDialogComponent { constructor( private readonly _dictionaryControllerService: DictionaryControllerService, private readonly _formBuilder: FormBuilder, - private readonly _notificationService: NotificationService, + private readonly _toaster: Toaster, private readonly _translateService: TranslateService, private readonly _dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) @@ -89,20 +89,16 @@ export class AddEditDictionaryDialogComponent { () => this._dialogRef.close(true), error => { if (error.status === 409) { - this._notifyError('add-edit-dictionary.error.dictionary-already-exists'); + this._toaster.error('add-edit-dictionary.error.dictionary-already-exists'); } else if (error.status === 400) { - this._notifyError('add-edit-dictionary.error.invalid-color-or-rank'); + this._toaster.error('add-edit-dictionary.error.invalid-color-or-rank'); } else { - this._notifyError('add-edit-dictionary.error.generic'); + this._toaster.error('add-edit-dictionary.error.generic'); } } ); } - private _notifyError(message: string) { - this._notificationService.showToastNotification(this._translateService.instant(message), null, NotificationType.ERROR); - } - private _formToObject(): TypeValue { return { caseInsensitive: !this.dictionaryForm.get('caseSensitive').value, diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-attribute-dialog/add-edit-dossier-attribute-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-attribute-dialog/add-edit-dossier-attribute-dialog.component.ts index 498af6fba..5a27f48ea 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-attribute-dialog/add-edit-dossier-attribute-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-attribute-dialog/add-edit-dossier-attribute-dialog.component.ts @@ -1,12 +1,12 @@ -import { Component, Inject } from '@angular/core'; +import { Component, Inject, OnDestroy } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { DossierAttributeConfig, FileAttributeConfig } from '@redaction/red-ui-http'; import { AppStateService } from '@state/app-state.service'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { LoadingService } from '@services/loading.service'; -import { ErrorMessageService } from '@services/error-message.service'; -import { NotificationService, NotificationType } from '@services/notification.service'; +import { LoadingService } from '../../../../services/loading.service'; import { HttpErrorResponse } from '@angular/common/http'; +import { Toaster } from '../../../../services/toaster.service'; +import { AutoUnsubscribeComponent } from '../../../shared/base/auto-unsubscribe.component'; import { DossierAttributesService } from '@shared/services/controller-wrappers/dossier-attributes.service'; @Component({ @@ -14,7 +14,7 @@ import { DossierAttributesService } from '@shared/services/controller-wrappers/d templateUrl: './add-edit-dossier-attribute-dialog.component.html', styleUrls: ['./add-edit-dossier-attribute-dialog.component.scss'] }) -export class AddEditDossierAttributeDialogComponent { +export class AddEditDossierAttributeDialogComponent extends AutoUnsubscribeComponent implements OnDestroy { dossierAttributeForm: FormGroup; dossierAttribute: DossierAttributeConfig; dossierTemplateId: string; @@ -30,12 +30,12 @@ export class AddEditDossierAttributeDialogComponent { private readonly _formBuilder: FormBuilder, private readonly _loadingService: LoadingService, private readonly _dossierAttributesService: DossierAttributesService, - private readonly _errorMessageService: ErrorMessageService, - private readonly _notificationService: NotificationService, + private readonly _toaster: Toaster, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: { dossierAttribute: DossierAttributeConfig; dossierTemplateId: string } ) { + super(); this.dossierAttribute = data.dossierAttribute; this.dossierTemplateId = data.dossierTemplateId; @@ -76,13 +76,9 @@ export class AddEditDossierAttributeDialogComponent { () => { this.dialogRef.close(true); }, - (err: HttpErrorResponse) => { + (error: HttpErrorResponse) => { this._loadingService.stop(); - this._notificationService.showToastNotification( - this._errorMessageService.getMessage(err, 'add-edit-dossier-attribute.error.generic'), - null, - NotificationType.ERROR - ); + this._toaster.error('add-edit-dossier-attribute.error.generic', { error: error }); } ); } diff --git a/apps/red-ui/src/app/modules/admin/dialogs/edit-color-dialog/edit-color-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/edit-color-dialog/edit-color-dialog.component.ts index 67df7c56d..c56609d0b 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/edit-color-dialog/edit-color-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/edit-color-dialog/edit-color-dialog.component.ts @@ -1,7 +1,7 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Colors, DictionaryControllerService } from '@redaction/red-ui-http'; -import { NotificationService, NotificationType } from '@services/notification.service'; +import { Toaster } from '../../../../services/toaster.service'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { DefaultColorType } from '@models/default-color-key.model'; @@ -21,7 +21,7 @@ export class EditColorDialogComponent { constructor( private readonly _formBuilder: FormBuilder, private readonly _dictionaryControllerService: DictionaryControllerService, - private readonly _notificationService: NotificationService, + private readonly _toaster: Toaster, private readonly _translateService: TranslateService, private readonly _dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) @@ -50,17 +50,10 @@ export class EditColorDialogComponent { try { await this._dictionaryControllerService.setColors(colors, this._dossierTemplateId).toPromise(); this._dialogRef.close(true); - this._notificationService.showToastNotification( - this._translateService.instant('edit-color-dialog.success', { - color: this._translateService.instant('default-colors-screen.types.' + this.colorKey) - }) - ); + const color = this._translateService.instant(`default-colors-screen.types.${this.colorKey}`); + this._toaster.info('edit-color-dialog.success', { params: { color: color } }); } catch (e) { - this._notificationService.showToastNotification( - this._translateService.instant('edit-color-dialog.error'), - null, - NotificationType.ERROR - ); + this._toaster.error('edit-color-dialog.error'); } } } diff --git a/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/active-fields-listing/active-fields-listing.component.html b/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/active-fields-listing/active-fields-listing.component.html index 8a6de6ad6..f03412ce2 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/active-fields-listing/active-fields-listing.component.html +++ b/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/active-fields-listing/active-fields-listing.component.html @@ -2,15 +2,15 @@
- {{ 'file-attributes-csv-import.table-header.title' | translate: { length: allEntities.length } }} + {{ 'file-attributes-csv-import.table-header.title' | translate: { length: (screenStateService.allEntitiesLength$ | async) } }} - + @@ -91,7 +91,7 @@
diff --git a/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/active-fields-listing/active-fields-listing.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/active-fields-listing/active-fields-listing.component.ts index abd4dbd85..5068b98b5 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/active-fields-listing/active-fields-listing.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/active-fields-listing/active-fields-listing.component.ts @@ -20,41 +20,39 @@ export class ActiveFieldsListingComponent extends BaseListingComponent im @Output() toggleFieldActive = new EventEmitter(); readonly typeOptions = [FileAttributeConfig.TypeEnum.TEXT, FileAttributeConfig.TypeEnum.NUMBER, FileAttributeConfig.TypeEnum.DATE]; + protected readonly _primaryKey = 'id'; constructor(protected readonly _injector: Injector) { super(_injector); - this._screenStateService.setIdKey('csvColumn'); } ngOnChanges(changes: SimpleChanges): void { if (changes.entities) { - this._screenStateService.setEntities(this.entities); - this._screenStateService.setDisplayedEntities(this.entities); - this._screenStateService.updateSelection(); + this.screenStateService.setEntities(this.entities); + this.screenStateService.updateSelection(); } } deactivateSelection() { this.allEntities.filter(field => this.isSelected(field)).forEach(field => (field.primaryAttribute = false)); - this._screenStateService.setEntities([...this.allEntities.filter(field => !this.isSelected(field))]); + this.screenStateService.setEntities(this.allEntities.filter(field => !this.isSelected(field))); this.entitiesChange.emit(this.allEntities); - this._screenStateService.setSelectedEntitiesIds([]); + this.screenStateService.setSelectedEntities([]); } setAttributeForSelection(attribute: string, value: any) { - for (const csvColumn of this._screenStateService.selectedEntitiesIds) { - this.allEntities.find(f => f.csvColumn === csvColumn)[attribute] = value; + for (const item of this.screenStateService.selectedEntities) { + this.allEntities.find(f => f.csvColumn === item.csvColumn)[attribute] = value; } } togglePrimary(field: Field) { if (field.primaryAttribute) { field.primaryAttribute = false; - } else { - for (const f of this.allEntities) { - f.primaryAttribute = false; - } - field.primaryAttribute = true; + return; } + + for (const f of this.allEntities) f.primaryAttribute = false; + field.primaryAttribute = true; } } diff --git a/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component.html b/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component.html index 505d781f2..23f2e2e5f 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component.html +++ b/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component.html @@ -90,7 +90,7 @@
diff --git a/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component.ts index e1ea2765e..44cac87d1 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component.ts @@ -6,7 +6,7 @@ import * as Papa from 'papaparse'; import { FileAttributeConfig, FileAttributesConfig, FileAttributesControllerService } from '@redaction/red-ui-http'; import { Observable } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; -import { NotificationService, NotificationType } from '@services/notification.service'; +import { Toaster } from '../../../../services/toaster.service'; import { TranslateService } from '@ngx-translate/core'; import { FilterService } from '@shared/services/filter.service'; import { SearchService } from '@shared/services/search.service'; @@ -26,12 +26,13 @@ export interface Field { } @Component({ - selector: 'redaction-file-attributes-csv-import-dialog', templateUrl: './file-attributes-csv-import-dialog.component.html', styleUrls: ['./file-attributes-csv-import-dialog.component.scss'], providers: [FilterService, SearchService, ScreenStateService, SortingService] }) export class FileAttributesCsvImportDialogComponent extends BaseListingComponent { + protected readonly _primaryKey = 'id'; + csvFile: File; dossierTemplateId: string; parseResult: { data: any[]; errors: any[]; meta: any; fields: Field[] }; @@ -44,18 +45,17 @@ export class FileAttributesCsvImportDialogComponent extends BaseListingComponent keepPreview = false; columnSample = []; initialParseConfig: { delimiter?: string; encoding?: string } = {}; - protected readonly _searchKey = 'csvColumn'; constructor( private readonly _appStateService: AppStateService, private readonly _fileAttributesControllerService: FileAttributesControllerService, private readonly _translateService: TranslateService, - private readonly _notificationService: NotificationService, + private readonly _toaster: Toaster, private readonly _formBuilder: FormBuilder, - public dialogRef: MatDialogRef, + readonly dialogRef: MatDialogRef, protected readonly _injector: Injector, @Inject(MAT_DIALOG_DATA) - public data: { + readonly data: { csv: File; dossierTemplateId: string; existingConfiguration: FileAttributesConfig; @@ -96,8 +96,7 @@ export class FileAttributesCsvImportDialogComponent extends BaseListingComponent this.parseResult.meta.fields = Object.keys(this.parseResult.data[0]); } - this._screenStateService.setEntities(this.parseResult.meta.fields.map(field => this._buildAttribute(field))); - this._screenStateService.setDisplayedEntities(this.allEntities); + this.screenStateService.setEntities(this.parseResult.meta.fields.map(field => this._buildAttribute(field))); this.activeFields = []; for (const entity of this.allEntities) { @@ -204,19 +203,9 @@ export class FileAttributesCsvImportDialogComponent extends BaseListingComponent try { await this._fileAttributesControllerService.setFileAttributesConfig(fileAttributes, this.dossierTemplateId).toPromise(); - this._notificationService.showToastNotification( - this._translateService.instant('file-attributes-csv-import.save.success', { - count: this.activeFields.length - }), - null, - NotificationType.SUCCESS - ); + this._toaster.success('file-attributes-csv-import.save.success', { params: { count: this.activeFields.length } }); } catch (e) { - this._notificationService.showToastNotification( - this._translateService.instant('file-attributes-csv-import.save.error'), - null, - NotificationType.ERROR - ); + this._toaster.error('file-attributes-csv-import.save.error'); } this.dialogRef.close(true); diff --git a/apps/red-ui/src/app/modules/admin/screens/audit/audit-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/audit/audit-screen.component.html index a58289d6e..5f839294a 100644 --- a/apps/red-ui/src/app/modules/admin/screens/audit/audit-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/audit/audit-screen.component.html @@ -132,5 +132,3 @@
- - diff --git a/apps/red-ui/src/app/modules/admin/screens/audit/audit-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/audit/audit-screen.component.ts index ec2d30fd6..645c50334 100644 --- a/apps/red-ui/src/app/modules/admin/screens/audit/audit-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/audit/audit-screen.component.ts @@ -1,10 +1,11 @@ -import { Component } from '@angular/core'; +import { Component, OnDestroy } from '@angular/core'; import { PermissionsService } from '@services/permissions.service'; import { FormBuilder, FormGroup } from '@angular/forms'; import { AuditControllerService, AuditResponse, AuditSearchRequest } from '@redaction/red-ui-http'; -import { TranslateService } from '@ngx-translate/core'; import { Moment } from 'moment'; import { applyIntervalConstraints } from '@utils/date-inputs-utils'; +import { LoadingService } from '../../../../services/loading.service'; +import { AutoUnsubscribeComponent } from '../../../shared/base/auto-unsubscribe.component'; const PAGE_SIZE = 50; @@ -13,16 +14,14 @@ const PAGE_SIZE = 50; templateUrl: './audit-screen.component.html', styleUrls: ['./audit-screen.component.scss'] }) -export class AuditScreenComponent { +export class AuditScreenComponent extends AutoUnsubscribeComponent implements OnDestroy { readonly ALL_CATEGORIES = 'all-categories'; readonly ALL_USERS = 'audit-screen.all-users'; filterForm: FormGroup; - viewReady = false; categories: string[] = []; userIds: Set; logs: AuditResponse; - currentPage = 1; private _previousFrom: Moment; private _previousTo: Moment; @@ -31,8 +30,9 @@ export class AuditScreenComponent { readonly permissionsService: PermissionsService, private readonly _formBuilder: FormBuilder, private readonly _auditControllerService: AuditControllerService, - private readonly _translateService: TranslateService + private readonly _loadingService: LoadingService ) { + super(); this.filterForm = this._formBuilder.group({ category: [this.ALL_CATEGORIES], userId: [this.ALL_USERS], @@ -40,7 +40,7 @@ export class AuditScreenComponent { to: [] }); - this.filterForm.valueChanges.subscribe(value => { + this.addSubscription = this.filterForm.valueChanges.subscribe(value => { if (!this._updateDateFilters(value)) { this._fetchData(); } @@ -71,7 +71,7 @@ export class AuditScreenComponent { } private _fetchData(page?: number) { - this.viewReady = false; + this._loadingService.start(); const promises = []; const category = this.filterForm.get('category').value; const userId = this.filterForm.get('userId').value; @@ -101,7 +101,7 @@ export class AuditScreenComponent { for (const id of this.logs.data.map(log => log.userId).filter(uid => !!uid)) { this.userIds.add(id); } - this.viewReady = true; + this._loadingService.stop(); }); } } diff --git a/apps/red-ui/src/app/modules/admin/screens/default-colors/default-colors-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/default-colors/default-colors-screen.component.html index 5ea4f8ae5..60375ce9a 100644 --- a/apps/red-ui/src/app/modules/admin/screens/default-colors/default-colors-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/default-colors/default-colors-screen.component.html @@ -22,14 +22,12 @@
- {{ 'default-colors-screen.table-header.title' | translate: { length: (allEntities$ | async).length } }} + {{ 'default-colors-screen.table-header.title' | translate: { length: screenStateService.allEntitiesLength$ | async } }}
-
+
diff --git a/apps/red-ui/src/app/modules/admin/screens/default-colors/default-colors-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/default-colors/default-colors-screen.component.ts index eb2ba49ce..a76513de9 100644 --- a/apps/red-ui/src/app/modules/admin/screens/default-colors/default-colors-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/default-colors/default-colors-screen.component.ts @@ -1,4 +1,4 @@ -import { Component, Injector, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Injector, OnInit } from '@angular/core'; import { AppStateService } from '@state/app-state.service'; import { Colors, DictionaryControllerService } from '@redaction/red-ui-http'; import { ActivatedRoute } from '@angular/router'; @@ -8,13 +8,14 @@ import { LoadingService } from '@services/loading.service'; import { FilterService } from '@shared/services/filter.service'; import { SearchService } from '@shared/services/search.service'; import { ScreenStateService } from '@shared/services/screen-state.service'; -import { ScreenNames, SortingService } from '@services/sorting.service'; import { BaseListingComponent } from '@shared/base/base-listing.component'; import { DefaultColorType } from '@models/default-color-key.model'; +import { SortingService } from '../../../../services/sorting.service'; @Component({ templateUrl: './default-colors-screen.component.html', styleUrls: ['./default-colors-screen.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, providers: [FilterService, SearchService, ScreenStateService, SortingService] }) export class DefaultColorsScreenComponent @@ -25,6 +26,7 @@ export class DefaultColorsScreenComponent implements OnInit { private _colorsObj: Colors; + protected readonly _primaryKey = 'key'; constructor( private readonly _appStateService: AppStateService, @@ -36,7 +38,6 @@ export class DefaultColorsScreenComponent protected readonly _injector: Injector ) { super(_injector); - this._sortingService.setScreenName(ScreenNames.DEFAULT_COLORS); _appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId); } @@ -53,9 +54,7 @@ export class DefaultColorsScreenComponent colorKey: color.key, dossierTemplateId: this._appStateService.activeDossierTemplateId }, - async () => { - await this._loadColors(); - } + async () => await this._loadColors() ); } @@ -63,12 +62,11 @@ export class DefaultColorsScreenComponent this._loadingService.start(); const data = await this._dictionaryControllerService.getColors(this._appStateService.activeDossierTemplateId).toPromise(); this._colorsObj = data; - this._screenStateService.setEntities( - Object.keys(data).map(key => ({ - key, - value: data[key] - })) - ); + const entities = Object.keys(data).map(key => ({ + key, + value: data[key] + })); + this.screenStateService.setEntities(entities); this._loadingService.stop(); } } diff --git a/apps/red-ui/src/app/modules/admin/screens/dictionary-listing/dictionary-listing-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/dictionary-listing/dictionary-listing-screen.component.html index 713fce6cc..b33a0b768 100644 --- a/apps/red-ui/src/app/modules/admin/screens/dictionary-listing/dictionary-listing-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/dictionary-listing/dictionary-listing-screen.component.html @@ -24,18 +24,18 @@
- {{ 'dictionary-listing.table-header.title' | translate: { length: (displayedEntities$ | async)?.length } }} + {{ 'dictionary-listing.table-header.title' | translate: { length: (screenStateService.displayedLength$ | async) } }} @@ -59,20 +59,16 @@
-
+
@@ -141,7 +137,7 @@
({ - value: dict.entries ? dict.entries.length : 0, + value: dict.entries?.length ?? 0, color: dict.hexColor, label: dict.label, key: dict.type @@ -31,6 +31,8 @@ const toChartConfig = (dict: TypeValueWrapper): DoughnutChartConfig => ({ export class DictionaryListingScreenComponent extends BaseListingComponent implements OnInit { chartData: DoughnutChartConfig[] = []; + protected readonly _primaryKey = 'label'; + constructor( private readonly _dialogService: AdminDialogService, private readonly _dictionaryControllerService: DictionaryControllerService, @@ -43,9 +45,6 @@ export class DictionaryListingScreenComponent extends BaseListingComponent { this._loadingService.start(); - await this._dictionaryControllerService.deleteTypes(types, this._appStateService.activeDossierTemplateId).toPromise(); - this._screenStateService.setSelectedEntitiesIds([]); + await this._dictionaryControllerService + .deleteTypes( + types.map(t => t.type), + this._appStateService.activeDossierTemplateId + ) + .toPromise(); + this.screenStateService.setSelectedEntities([]); await this._appStateService.loadDictionaryData(); this._loadDictionaryData(false); this._calculateData(); @@ -88,15 +92,13 @@ export class DictionaryListingScreenComponent extends BaseListingComponent !d.virtual); if (!loadEntries) - this._screenStateService.setEntities( + this.screenStateService.setEntities( entities.map(dict => { dict.entries = this.allEntities.find(d => d.type === dict.type)?.entries || []; return dict; }) ); - else this._screenStateService.setEntities(entities); - - this._screenStateService.setDisplayedEntities(this.allEntities); + else this.screenStateService.setEntities(entities); if (!loadEntries) return; diff --git a/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.html index de42692ed..fd0ba0385 100644 --- a/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.html @@ -4,25 +4,15 @@
- +
-
+
- - diff --git a/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.ts index 0ebbfb000..0a2777a7a 100644 --- a/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.ts @@ -1,30 +1,31 @@ -import { Component } from '@angular/core'; +import { Component, OnDestroy } from '@angular/core'; import { DigitalSignature, DigitalSignatureControllerService } from '@redaction/red-ui-http'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { NotificationService, NotificationType } from '@services/notification.service'; -import { TranslateService } from '@ngx-translate/core'; +import { Toaster } from '../../../../services/toaster.service'; import { PermissionsService } from '@services/permissions.service'; import { lastIndexOfEnd } from '@utils/functions'; +import { AutoUnsubscribeComponent } from '../../../shared/base/auto-unsubscribe.component'; +import { LoadingService } from '../../../../services/loading.service'; @Component({ selector: 'redaction-digital-signature-screen', templateUrl: './digital-signature-screen.component.html', styleUrls: ['./digital-signature-screen.component.scss'] }) -export class DigitalSignatureScreenComponent { +export class DigitalSignatureScreenComponent extends AutoUnsubscribeComponent implements OnDestroy { digitalSignature: DigitalSignature; digitalSignatureForm: FormGroup; - viewReady = false; digitalSignatureExists = false; constructor( private readonly _digitalSignatureControllerService: DigitalSignatureControllerService, - private readonly _notificationService: NotificationService, + private readonly _toaster: Toaster, private readonly _formBuilder: FormBuilder, - private readonly _translateService: TranslateService, + private readonly _loadingService: LoadingService, readonly permissionsService: PermissionsService ) { + super(); this.loadDigitalSignatureAndInitializeForm(); } @@ -43,50 +44,28 @@ export class DigitalSignatureScreenComponent { ? this._digitalSignatureControllerService.updateDigitalSignature(digitalSignature) : this._digitalSignatureControllerService.saveDigitalSignature(digitalSignature); - observable.subscribe( + this.addSubscription = observable.subscribe( () => { this.loadDigitalSignatureAndInitializeForm(); - this._notificationService.showToastNotification( - this._translateService.instant('digital-signature-screen.action.save-success'), - null, - NotificationType.SUCCESS - ); + this._toaster.success('digital-signature-screen.action.save-success'); }, error => { if (error.status === 400) { - this._notificationService.showToastNotification( - this._translateService.instant('digital-signature-screen.action.certificate-not-valid-error'), - null, - NotificationType.ERROR - ); + this._toaster.error('digital-signature-screen.action.certificate-not-valid-error'); } else { - this._notificationService.showToastNotification( - this._translateService.instant('digital-signature-screen.action.save-error'), - null, - NotificationType.ERROR - ); + this._toaster.error('digital-signature-screen.action.save-error'); } } ); } removeDigitalSignature() { - this._digitalSignatureControllerService.deleteDigitalSignature().subscribe( + this.addSubscription = this._digitalSignatureControllerService.deleteDigitalSignature().subscribe( () => { this.loadDigitalSignatureAndInitializeForm(); - this._notificationService.showToastNotification( - this._translateService.instant('digital-signature-screen.action.delete-success'), - null, - NotificationType.SUCCESS - ); + this._toaster.success('digital-signature-screen.action.delete-success'); }, - () => { - this._notificationService.showToastNotification( - this._translateService.instant('digital-signature-screen.action.delete-error'), - null, - NotificationType.ERROR - ); - } + () => this._toaster.error('digital-signature-screen.action.delete-error') ); } @@ -104,8 +83,8 @@ export class DigitalSignatureScreenComponent { } loadDigitalSignatureAndInitializeForm() { - this.viewReady = false; - this._digitalSignatureControllerService + this._loadingService.start(); + this.addSubscription = this._digitalSignatureControllerService .getDigitalSignature() .subscribe( digitalSignature => { @@ -119,12 +98,10 @@ export class DigitalSignatureScreenComponent { ) .add(() => { this._initForm(); - this.viewReady = true; + this._loadingService.stop(); }); } - formChanged() {} - private _initForm() { this.digitalSignatureForm = this._formBuilder.group({ certificateName: [this.digitalSignature.certificateName, Validators.required], diff --git a/apps/red-ui/src/app/modules/admin/screens/dossier-attributes-listing/dossier-attributes-listing-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/dossier-attributes-listing/dossier-attributes-listing-screen.component.html index 5475afb04..df5e1bf6e 100644 --- a/apps/red-ui/src/app/modules/admin/screens/dossier-attributes-listing/dossier-attributes-listing-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/dossier-attributes-listing/dossier-attributes-listing-screen.component.html @@ -20,22 +20,25 @@
-
+
- {{ 'dossier-attributes-listing.table-header.title' | translate: { length: (displayedEntities$ | async)?.length } }} + {{ + 'dossier-attributes-listing.table-header.title' + | translate: { length: (screenStateService.displayedLength$ | async) } + }} @@ -58,42 +61,46 @@
-
+
- + + + +
- +
@@ -118,15 +125,14 @@ [tooltip]="'dossier-attributes-listing.action.edit' | translate" icon="red:edit" type="dark-bg" - > - + > + - + >
diff --git a/apps/red-ui/src/app/modules/admin/screens/dossier-attributes-listing/dossier-attributes-listing-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/dossier-attributes-listing/dossier-attributes-listing-screen.component.ts index 63dbde699..fd9d9f10f 100644 --- a/apps/red-ui/src/app/modules/admin/screens/dossier-attributes-listing/dossier-attributes-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/dossier-attributes-listing/dossier-attributes-listing-screen.component.ts @@ -5,7 +5,7 @@ import { AppStateService } from '@state/app-state.service'; import { ActivatedRoute } from '@angular/router'; import { AdminDialogService } from '../../services/admin-dialog.service'; import { LoadingService } from '@services/loading.service'; -import { ScreenNames, SortingService } from '@services/sorting.service'; +import { SortingService } from '../../../../services/sorting.service'; import { FilterService } from '@shared/services/filter.service'; import { SearchService } from '@shared/services/search.service'; import { ScreenStateService } from '@shared/services/screen-state.service'; @@ -18,6 +18,8 @@ import { DossierAttributesService } from '@shared/services/controller-wrappers/d providers: [FilterService, SearchService, ScreenStateService, SortingService] }) export class DossierAttributesListingScreenComponent extends BaseListingComponent implements OnInit { + protected readonly _primaryKey = 'label'; + constructor( protected readonly _injector: Injector, private readonly _appStateService: AppStateService, @@ -28,9 +30,6 @@ export class DossierAttributesListingScreenComponent extends BaseListingComponen readonly permissionsService: PermissionsService ) { super(_injector); - this._searchService.setSearchKey('label'); - this._screenStateService.setIdKey('id'); - this._sortingService.setScreenName(ScreenNames.DOSSIER_ATTRIBUTES_LISTING); _appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId); } @@ -41,7 +40,7 @@ export class DossierAttributesListingScreenComponent extends BaseListingComponen openConfirmDeleteAttributeDialog($event: MouseEvent, dossierAttribute?: DossierAttributeConfig) { this._dialogService.openDialog('confirm', $event, null, async () => { this._loadingService.start(); - const ids = dossierAttribute ? [dossierAttribute.id] : this._screenStateService.selectedEntitiesIds; + const ids = dossierAttribute ? [dossierAttribute.id] : this.screenStateService.selectedEntities.map(item => item.id); await this._dossierAttributesService.deleteConfigs(ids); await this._loadData(); }); @@ -61,8 +60,7 @@ export class DossierAttributesListingScreenComponent extends BaseListingComponen private async _loadData() { this._loadingService.start(); const attributes = await this._dossierAttributesService.getConfig(); - this._screenStateService.setEntities(attributes); - this.filterService.filterEntities(); + this.screenStateService.setEntities(attributes); this._loadingService.stop(); } } diff --git a/apps/red-ui/src/app/modules/admin/screens/dossier-template-listing/dossier-templates-listing-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/dossier-template-listing/dossier-templates-listing-screen.component.html index 1983c630a..06e453cb1 100644 --- a/apps/red-ui/src/app/modules/admin/screens/dossier-template-listing/dossier-templates-listing-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/dossier-template-listing/dossier-templates-listing-screen.component.html @@ -4,17 +4,10 @@
- +
@@ -22,28 +15,29 @@
- {{ 'dossier-templates-listing.table-header.title' | translate: { length: (displayedEntities$ | async).length } }} + {{ + 'dossier-templates-listing.table-header.title' + | translate: { length: (screenStateService.displayedLength$ | async) } + }} - - - +
@@ -58,12 +52,10 @@
-
+
+
diff --git a/apps/red-ui/src/app/modules/admin/screens/dossier-template-listing/dossier-templates-listing-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/dossier-template-listing/dossier-templates-listing-screen.component.ts index b31a54a5a..769f22100 100644 --- a/apps/red-ui/src/app/modules/admin/screens/dossier-template-listing/dossier-templates-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/dossier-template-listing/dossier-templates-listing-screen.component.ts @@ -9,8 +9,8 @@ import { DossierTemplateControllerService } from '@redaction/red-ui-http'; import { FilterService } from '@shared/services/filter.service'; import { SearchService } from '@shared/services/search.service'; import { ScreenStateService } from '@shared/services/screen-state.service'; -import { ScreenNames, SortingService } from '@services/sorting.service'; import { BaseListingComponent } from '@shared/base/base-listing.component'; +import { SortingService } from '../../../../services/sorting.service'; @Component({ templateUrl: './dossier-templates-listing-screen.component.html', @@ -19,6 +19,8 @@ import { BaseListingComponent } from '@shared/base/base-listing.component'; providers: [FilterService, SearchService, ScreenStateService, SortingService] }) export class DossierTemplatesListingScreenComponent extends BaseListingComponent implements OnInit { + protected _primaryKey = 'name'; + constructor( private readonly _dialogService: AdminDialogService, private readonly _appStateService: AppStateService, @@ -29,9 +31,6 @@ export class DossierTemplatesListingScreenComponent extends BaseListingComponent readonly userPreferenceService: UserPreferenceService ) { super(_injector); - this._sortingService.setScreenName(ScreenNames.DOSSIER_TEMPLATES_LISTING); - this._searchService.setSearchKey('name'); - this._screenStateService.setIdKey('dossierTemplateId'); } ngOnInit(): void { @@ -41,8 +40,10 @@ export class DossierTemplatesListingScreenComponent extends BaseListingComponent openDeleteTemplatesDialog($event?: MouseEvent) { return this._dialogService.openDialog('confirm', $event, null, async () => { this._loadingService.start(); - await this._dossierTemplateControllerService.deleteDossierTemplates(this._screenStateService.selectedEntitiesIds).toPromise(); - this._screenStateService.setSelectedEntitiesIds([]); + await this._dossierTemplateControllerService + .deleteDossierTemplates(this.screenStateService.selectedEntities.map(d => d.dossierTemplateId)) + .toPromise(); + this.screenStateService.setSelectedEntities([]); await this._appStateService.loadAllDossierTemplates(); await this._appStateService.loadDictionaryData(); this.loadDossierTemplatesData(); @@ -52,8 +53,7 @@ export class DossierTemplatesListingScreenComponent extends BaseListingComponent loadDossierTemplatesData() { this._loadingService.start(); this._appStateService.reset(); - this._screenStateService.setEntities(this._appStateService.dossierTemplates); - this.filterService.filterEntities(); + this.screenStateService.setEntities(this._appStateService.dossierTemplates); this._loadDossierTemplateStats(); this._loadingService.stop(); } @@ -67,7 +67,7 @@ export class DossierTemplatesListingScreenComponent extends BaseListingComponent } private _loadDossierTemplateStats() { - this._screenStateService.entities.forEach(rs => { + this.screenStateService.allEntities.forEach(rs => { const dictionaries = this._appStateService.dictionaryData[rs.dossierTemplateId]; if (dictionaries) { rs.dictionariesCount = Object.keys(dictionaries) diff --git a/apps/red-ui/src/app/modules/admin/screens/file-attributes-listing/file-attributes-listing-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/file-attributes-listing/file-attributes-listing-screen.component.html index da001f604..afdfafadc 100644 --- a/apps/red-ui/src/app/modules/admin/screens/file-attributes-listing/file-attributes-listing-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/file-attributes-listing/file-attributes-listing-screen.component.html @@ -24,27 +24,28 @@
- {{ 'file-attributes-listing.table-header.title' | translate: { length: (displayedEntities$ | async)?.length } }} + {{ + 'file-attributes-listing.table-header.title' | translate: { length: (screenStateService.displayedLength$ | async) } + }} - + >
@@ -69,28 +70,22 @@
-
+
- + -
+
diff --git a/apps/red-ui/src/app/modules/admin/screens/file-attributes-listing/file-attributes-listing-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/file-attributes-listing/file-attributes-listing-screen.component.ts index f66cf50a0..c17f588f1 100644 --- a/apps/red-ui/src/app/modules/admin/screens/file-attributes-listing/file-attributes-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/file-attributes-listing/file-attributes-listing-screen.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, ElementRef, Injector, OnInit, ViewChild } from '@angular/core'; +import { ChangeDetectionStrategy, Component, ElementRef, Injector, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { PermissionsService } from '@services/permissions.service'; import { FileAttributeConfig, FileAttributesConfig, FileAttributesControllerService } from '@redaction/red-ui-http'; import { AppStateService } from '@state/app-state.service'; @@ -8,7 +8,7 @@ import { LoadingService } from '@services/loading.service'; import { FilterService } from '@shared/services/filter.service'; import { SearchService } from '@shared/services/search.service'; import { ScreenStateService } from '@shared/services/screen-state.service'; -import { ScreenNames, SortingService } from '@services/sorting.service'; +import { SortingService } from '../../../../services/sorting.service'; import { BaseListingComponent } from '@shared/base/base-listing.component'; @Component({ @@ -17,7 +17,9 @@ import { BaseListingComponent } from '@shared/base/base-listing.component'; changeDetection: ChangeDetectionStrategy.OnPush, providers: [FilterService, SearchService, ScreenStateService, SortingService] }) -export class FileAttributesListingScreenComponent extends BaseListingComponent implements OnInit { +export class FileAttributesListingScreenComponent extends BaseListingComponent implements OnInit, OnDestroy { + protected readonly _primaryKey = 'label'; + private _existingConfiguration: FileAttributesConfig; @ViewChild('fileInput') private _fileInput: ElementRef; @@ -31,9 +33,6 @@ export class FileAttributesListingScreenComponent extends BaseListingComponent f.id), + this._appStateService.activeDossierTemplateId + ) .toPromise(); } await this._loadData(); @@ -89,17 +91,16 @@ export class FileAttributesListingScreenComponent extends BaseListingComponent -
- +
+
@@ -129,5 +119,3 @@
- - diff --git a/apps/red-ui/src/app/modules/admin/screens/license-information/license-information-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/license-information/license-information-screen.component.ts index 1f5b2ba38..398f76664 100644 --- a/apps/red-ui/src/app/modules/admin/screens/license-information/license-information-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/license-information/license-information-screen.component.ts @@ -4,6 +4,9 @@ import { LicenseReport, LicenseReportControllerService } from '@redaction/red-ui import { AppConfigService } from '@app-config/app-config.service'; import * as moment from 'moment'; import { TranslateService } from '@ngx-translate/core'; +import { LoadingService } from '../../../../services/loading.service'; +import { ButtonConfig } from '../../../shared/components/page-header/models/button-config.model'; +import { IconButtonTypes } from '../../../shared/components/buttons/icon-button/icon-button.component'; @Component({ selector: 'redaction-license-information-screen', @@ -16,7 +19,6 @@ export class LicenseInformationScreenComponent implements OnInit { unlicensedInfo: LicenseReport = {}; totalLicensedNumberOfPages = 0; analysisPercentageOfLicense = 100; - viewReady = false; barChart: any[] = []; lineChartSeries: any[] = []; yAxisLabel = this._translateService.instant('license-info-screen.chart.pages-per-month'); @@ -31,13 +33,23 @@ export class LicenseInformationScreenComponent implements OnInit { group: 'Ordinal', domain: ['#0389ec'] }; + buttonConfigs: ButtonConfig[] = [ + { + label: this._translateService.instant('license-info-screen.email-report'), + action: () => this.sendMail(), + type: IconButtonTypes.PRIMARY + } + ]; constructor( readonly permissionsService: PermissionsService, readonly appConfigService: AppConfigService, private readonly _licenseReportController: LicenseReportControllerService, - private readonly _translateService: TranslateService - ) {} + private readonly _translateService: TranslateService, + private readonly _loadingService: LoadingService + ) { + _loadingService.start(); + } get currentYear(): number { return new Date().getFullYear(); @@ -68,7 +80,7 @@ export class LicenseInformationScreenComponent implements OnInit { Promise.all(promises).then(reports => { [this.currentInfo, this.totalInfo, this.unlicensedInfo] = reports; - this.viewReady = true; + this._loadingService.stop(); this.analysisPercentageOfLicense = this.totalLicensedNumberOfPages > 0 ? (this.currentInfo.numberOfAnalyzedPages / this.totalLicensedNumberOfPages) * 100 diff --git a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen.component.ts index 8f266eebd..d8a6552a7 100644 --- a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen.component.ts @@ -1,7 +1,7 @@ -import { Component, ElementRef, ViewChild } from '@angular/core'; +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { PermissionsService } from '@services/permissions.service'; import { RulesControllerService } from '@redaction/red-ui-http'; -import { NotificationService, NotificationType } from '@services/notification.service'; +import { Toaster } from '../../../../services/toaster.service'; import { TranslateService } from '@ngx-translate/core'; import { saveAs } from 'file-saver'; import { ComponentHasChanges } from '@guards/can-deactivate.guard'; @@ -17,7 +17,7 @@ import IStandaloneEditorConstructionOptions = monaco.editor.IStandaloneEditorCon templateUrl: './rules-screen.component.html', styleUrls: ['./rules-screen.component.scss'] }) -export class RulesScreenComponent extends ComponentHasChanges { +export class RulesScreenComponent extends ComponentHasChanges implements OnInit { editorOptions: IStandaloneEditorConstructionOptions = { theme: 'vs', language: 'java', @@ -39,13 +39,16 @@ export class RulesScreenComponent extends ComponentHasChanges { readonly permissionsService: PermissionsService, private readonly _rulesControllerService: RulesControllerService, private readonly _appStateService: AppStateService, - private readonly _notificationService: NotificationService, + private readonly _toaster: Toaster, protected readonly _translateService: TranslateService, private readonly _activatedRoute: ActivatedRoute ) { super(_translateService); - this._appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId); - this._initialize(); + _appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId); + } + + async ngOnInit() { + await this._initialize(); } get hasChanges(): boolean { @@ -83,27 +86,20 @@ export class RulesScreenComponent extends ComponentHasChanges { async save(): Promise { this.processing = true; - this._rulesControllerService + await this._rulesControllerService .uploadRules({ rules: this._codeEditor.getModel().getValue(), dossierTemplateId: this._appStateService.activeDossierTemplateId }) - .subscribe( - () => { - this._initialize(); - this._notificationService.showToastNotification( - this._translateService.instant('rules-screen.success.generic'), - null, - NotificationType.SUCCESS - ); + .toPromise() + .then( + async () => { + await this._initialize(); + this._toaster.success('rules-screen.success.generic'); }, () => { this.processing = false; - this._notificationService.showToastNotification( - this._translateService.instant('rules-screen.error.generic'), - null, - NotificationType.ERROR - ); + this._toaster.error('rules-screen.error.generic'); } ); } @@ -148,13 +144,16 @@ export class RulesScreenComponent extends ComponentHasChanges { } as IModelDeltaDecoration; } - private _initialize() { - this._rulesControllerService.downloadRules(this._appStateService.activeDossierTemplateId).subscribe( - rules => { - this.currentLines = this.initialLines = rules.rules.split('\n'); - this.revert(); - }, - () => (this.processing = false) - ); + private async _initialize() { + await this._rulesControllerService + .downloadRules(this._appStateService.activeDossierTemplateId) + .toPromise() + .then( + rules => { + this.currentLines = this.initialLines = rules.rules.split('\n'); + this.revert(); + }, + () => (this.processing = false) + ); } } diff --git a/apps/red-ui/src/app/modules/admin/screens/smtp-config/smtp-config-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/smtp-config/smtp-config-screen.component.ts index b36206694..33cef2580 100644 --- a/apps/red-ui/src/app/modules/admin/screens/smtp-config/smtp-config-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/smtp-config/smtp-config-screen.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { PermissionsService } from '@services/permissions.service'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AdminDialogService } from '../../services/admin-dialog.service'; @@ -8,15 +8,15 @@ import { SmtpConfigurationControllerService, SMTPConfigurationModel } from '@redaction/red-ui-http'; -import { NotificationService, NotificationType } from '@services/notification.service'; -import { TranslateService } from '@ngx-translate/core'; +import { Toaster } from '../../../../services/toaster.service'; +import { AutoUnsubscribeComponent } from '../../../shared/base/auto-unsubscribe.component'; @Component({ selector: 'redaction-smtp-config-screen', templateUrl: './smtp-config-screen.component.html', styleUrls: ['./smtp-config-screen.component.scss'] }) -export class SmtpConfigScreenComponent implements OnInit { +export class SmtpConfigScreenComponent extends AutoUnsubscribeComponent implements OnInit, OnDestroy { viewReady = false; configForm: FormGroup; generalSettings: GeneralConfigurationModel = { @@ -31,11 +31,11 @@ export class SmtpConfigScreenComponent implements OnInit { private readonly _smtpConfigService: SmtpConfigurationControllerService, private readonly _formBuilder: FormBuilder, private readonly _dialogService: AdminDialogService, - private readonly _notificationService: NotificationService, - private readonly _translateService: TranslateService, + private readonly _toaster: Toaster, private readonly _generalSettingsControllerService: GeneralSettingsControllerService ) { - this.configForm = this._formBuilder.group({ + super(); + this.configForm = _formBuilder.group({ host: [undefined, Validators.required], port: [25], from: [undefined, [Validators.required, Validators.email]], @@ -50,7 +50,7 @@ export class SmtpConfigScreenComponent implements OnInit { password: [undefined] }); - this.configForm.controls.auth.valueChanges.subscribe(auth => { + this.addSubscription = this.configForm.controls.auth.valueChanges.subscribe(auth => { if (auth) { this.openAuthConfigDialog(); } @@ -110,17 +110,9 @@ export class SmtpConfigScreenComponent implements OnInit { this.viewReady = false; try { await this._smtpConfigService.testSMTPConfiguration(this.configForm.getRawValue()).toPromise(); - this._notificationService.showToastNotification( - this._translateService.instant('smtp-config-screen.test.success'), - undefined, - NotificationType.SUCCESS - ); + this._toaster.success('smtp-config-screen.test.success'); } catch (e) { - this._notificationService.showToastNotification( - this._translateService.instant('smtp-config-screen.test.error'), - undefined, - NotificationType.ERROR - ); + this._toaster.error('smtp-config-screen.test.error'); } finally { this.viewReady = true; } diff --git a/apps/red-ui/src/app/modules/admin/screens/trash/trash-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/trash/trash-screen.component.html index 61385b36b..e82bc8d48 100644 --- a/apps/red-ui/src/app/modules/admin/screens/trash/trash-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/trash/trash-screen.component.html @@ -9,18 +9,18 @@
- {{ 'trash.table-header.title' | translate: { length: (displayedEntities$ | async)?.length } }} + {{ 'trash.table-header.title' | translate: { length: (screenStateService.displayedLength$ | async) } }}
-
+
- + - + -
+
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 259e0f8b6..8c362ec5d 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 @@ -8,7 +8,7 @@ import * as moment from 'moment'; import { FilterService } from '@shared/services/filter.service'; import { SearchService } from '@shared/services/search.service'; import { ScreenStateService } from '@shared/services/screen-state.service'; -import { ScreenNames, SortingService } from '@services/sorting.service'; +import { SortingService } from '../../../../services/sorting.service'; import { BaseListingComponent } from '@shared/base/base-listing.component'; import { DossiersService } from '../../../dossier/services/dossiers.service'; @@ -21,6 +21,7 @@ import { DossiersService } from '../../../dossier/services/dossiers.service'; export class TrashScreenComponent extends BaseListingComponent implements OnInit { readonly itemSize = 85; private readonly _deleteRetentionHours = this._appConfigService.getConfig(AppConfigKey.DELETE_RETENTION_HOURS); + protected readonly _primaryKey = 'dossierName'; constructor( private readonly _appStateService: AppStateService, @@ -32,36 +33,29 @@ export class TrashScreenComponent extends BaseListingComponent implemen private readonly _appConfigService: AppConfigService ) { super(_injector); - this._sortingService.setScreenName(ScreenNames.DOSSIER_LISTING); - this._screenStateService.setIdKey('dossierId'); } async ngOnInit(): Promise { this._loadingService.start(); await this.loadDossierTemplatesData(); - this.filterService.filterEntities(); this._loadingService.stop(); } async loadDossierTemplatesData(): Promise { - this._screenStateService.setEntities(await this._dossiersService.getDeletedDossiers()); + this.screenStateService.setEntities(await this._dossiersService.getDeleted()); } getRestoreDate(softDeletedTime: string): string { return moment(softDeletedTime).add(this._deleteRetentionHours, 'hours').toISOString(); } - trackById(index: number, dossier: Dossier): string { - return dossier.dossierId; - } - - bulkDelete(dossierIds = this._screenStateService.selectedEntitiesIds) { + bulkDelete(dossierIds = this.screenStateService.selectedEntities.map(d => d.dossierId)) { this._loadingService.loadWhile(this._hardDelete(dossierIds)); } - bulkRestore(dossierIds = this._screenStateService.selectedEntitiesIds) { + bulkRestore(dossierIds = this.screenStateService.selectedEntities.map(d => d.dossierId)) { this._loadingService.loadWhile(this._restore(dossierIds)); } @@ -76,9 +70,8 @@ export class TrashScreenComponent extends BaseListingComponent implemen } private _removeFromList(ids: string[]): void { - const entities = this._screenStateService.entities.filter(e => !ids.includes(e.dossierId)); - this._screenStateService.setEntities(entities); - this._screenStateService.setSelectedEntitiesIds([]); - this.filterService.filterEntities(); + const entities = this.screenStateService.allEntities.filter(e => !ids.includes(e.dossierId)); + this.screenStateService.setEntities(entities); + this.screenStateService.setSelectedEntities([]); } } diff --git a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html index df3f6ec46..66eefc267 100644 --- a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html @@ -9,7 +9,7 @@
@@ -37,18 +37,18 @@
- {{ 'user-listing.table-header.title' | translate: { length: (displayedEntities$ | async)?.length } }} + {{ 'user-listing.table-header.title' | translate: { length: (screenStateService.displayedLength$ | async) } }}
- + -
+
diff --git a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts index 8b7edbee3..4a6fdd703 100644 --- a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Injector, OnInit, QueryList, ViewChildren } from '@angular/core'; +import { Component, Injector, OnInit, QueryList, ViewChildren } from '@angular/core'; import { PermissionsService } from '@services/permissions.service'; import { UserService } from '@services/user.service'; import { User, UserControllerService } from '@redaction/red-ui-http'; @@ -19,10 +19,12 @@ import { map } from 'rxjs/operators'; @Component({ templateUrl: './user-listing-screen.component.html', styleUrls: ['./user-listing-screen.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, providers: [FilterService, SearchService, ScreenStateService, SortingService] }) export class UserListingScreenComponent extends BaseListingComponent implements OnInit { + protected readonly _primaryKey = 'userId'; + readonly canDeleteSelected$ = this._canDeleteSelected$; + collapsedDetails = false; chartData: DoughnutChartConfig[] = []; @ViewChildren(InitialsAvatarComponent) @@ -39,12 +41,11 @@ export class UserListingScreenComponent extends BaseListingComponent imple protected readonly _injector: Injector ) { super(_injector); - this._screenStateService.setIdKey('userId'); } - get canDeleteSelected$(): Observable { - const entities$ = this._screenStateService.selectedEntitiesIds$; - return entities$.pipe(map(all => all.indexOf(this.userService.userId) === -1)); + get _canDeleteSelected$(): Observable { + const entities$ = this.screenStateService.selectedEntities$; + return entities$.pipe(map(all => all.indexOf(this.userService.user) === -1)); } async ngOnInit() { @@ -79,17 +80,12 @@ export class UserListingScreenComponent extends BaseListingComponent imple } bulkDelete() { - this.openDeleteUsersDialog(this._screenStateService.entities.filter(u => this.isSelected(u))); - } - - trackById(index: number, user: User) { - return user.userId; + this.openDeleteUsersDialog(this.screenStateService.allEntities.filter(u => this.isSelected(u))); } private async _loadData() { - this._screenStateService.setEntities(await this._userControllerService.getAllUsers().toPromise()); + this.screenStateService.setEntities(await this._userControllerService.getAllUsers().toPromise()); await this.userService.loadAllUsers(); - this.filterService.filterEntities(); this._computeStats(); this._loadingService.stop(); } diff --git a/apps/red-ui/src/app/modules/admin/screens/watermark/watermark-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/watermark/watermark-screen.component.ts index 3bc64a4da..627166a6e 100644 --- a/apps/red-ui/src/app/modules/admin/screens/watermark/watermark-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/watermark/watermark-screen.component.ts @@ -7,8 +7,7 @@ import { HttpClient } from '@angular/common/http'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { debounce } from '@utils/debounce'; import { WatermarkControllerService, WatermarkModelRes } from '@redaction/red-ui-http'; -import { NotificationService, NotificationType } from '@services/notification.service'; -import { TranslateService } from '@ngx-translate/core'; +import { Toaster } from '../../../../services/toaster.service'; import { ActivatedRoute } from '@angular/router'; import { BASE_HREF } from '../../../../tokens'; import { stampPDFPage } from '../../../../utils/page-stamper'; @@ -39,15 +38,14 @@ export class WatermarkScreenComponent implements OnInit { readonly permissionsService: PermissionsService, readonly appStateService: AppStateService, @Inject(BASE_HREF) private readonly _baseHref: string, - private readonly _translateService: TranslateService, private readonly _watermarkControllerService: WatermarkControllerService, - private readonly _notificationService: NotificationService, + private readonly _toaster: Toaster, private readonly _http: HttpClient, private readonly _changeDetectorRef: ChangeDetectorRef, private readonly _formBuilder: FormBuilder, private readonly _activatedRoute: ActivatedRoute ) { - this.appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId); + appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId); this._initForm(); } @@ -81,24 +79,12 @@ export class WatermarkScreenComponent implements OnInit { ? this._watermarkControllerService.saveWatermark(watermark, this.appStateService.activeDossierTemplateId) : this._watermarkControllerService.deleteWatermark(this.appStateService.activeDossierTemplateId); - observable.subscribe( + observable.toPromise().then( () => { this._loadWatermark(); - this._notificationService.showToastNotification( - this._translateService.instant( - watermark.text ? 'watermark-screen.action.change-success' : 'watermark-screen.action.delete-success' - ), - null, - NotificationType.SUCCESS - ); + this._toaster.success(watermark.text ? 'watermark-screen.action.change-success' : 'watermark-screen.action.delete-success'); }, - () => { - this._notificationService.showToastNotification( - this._translateService.instant('watermark-screen.action.error'), - null, - NotificationType.ERROR - ); - } + () => this._toaster.error('watermark-screen.action.error') ); } @@ -188,11 +174,41 @@ export class WatermarkScreenComponent implements OnInit { private _initForm() { this.configForm = this._formBuilder.group({ text: [{ value: null, disabled: !this.permissionsService.isAdmin() }], - hexColor: [{ value: null, disabled: !this.permissionsService.isAdmin() }, Validators.required], - opacity: [{ value: null, disabled: !this.permissionsService.isAdmin() }, Validators.required], - fontSize: [{ value: null, disabled: !this.permissionsService.isAdmin() }, Validators.required], - fontType: [{ value: null, disabled: !this.permissionsService.isAdmin() }, Validators.required], - orientation: [{ value: null, disabled: !this.permissionsService.isAdmin() }, Validators.required] + hexColor: [ + { + value: null, + disabled: !this.permissionsService.isAdmin() + }, + Validators.required + ], + opacity: [ + { + value: null, + disabled: !this.permissionsService.isAdmin() + }, + Validators.required + ], + fontSize: [ + { + value: null, + disabled: !this.permissionsService.isAdmin() + }, + Validators.required + ], + fontType: [ + { + value: null, + disabled: !this.permissionsService.isAdmin() + }, + Validators.required + ], + orientation: [ + { + value: null, + disabled: !this.permissionsService.isAdmin() + }, + Validators.required + ] }); } } diff --git a/apps/red-ui/src/app/modules/dossier/components/bulk-actions/dossier-overview-bulk-actions.component.html b/apps/red-ui/src/app/modules/dossier/components/bulk-actions/dossier-overview-bulk-actions.component.html index 9c41e2e5d..28809a440 100644 --- a/apps/red-ui/src/app/modules/dossier/components/bulk-actions/dossier-overview-bulk-actions.component.html +++ b/apps/red-ui/src/app/modules/dossier/components/bulk-actions/dossier-overview-bulk-actions.component.html @@ -21,8 +21,7 @@ [tooltip]="'dossier-overview.assign-me' | translate" icon="red:assign-me" type="dark-bg" - > - + > - + > - + > @@ -52,8 +49,7 @@ [tooltip]="canApprove ? ('dossier-overview.approve' | translate) : ('dossier-overview.approve-disabled' | translate)" icon="red:approved" type="dark-bg" - > - + > - + > ) {} get dossier() { @@ -43,30 +38,27 @@ export class DossierOverviewBulkActionsComponent { } get selectedFiles(): FileStatusWrapper[] { - return this.selectedFileIds.map(fileId => - this._appStateService.getFileById(this._appStateService.activeDossier.dossier.dossierId, fileId) - ); + return this._screenStateService.selectedEntities; } get areAllFilesSelected() { return ( this._appStateService.activeDossier.files.length !== 0 && - this.selectedFileIds.length === this._appStateService.activeDossier.files.length + this.selectedFiles.length === this._appStateService.activeDossier.files.length ); } get areSomeFilesSelected() { - return this.selectedFileIds.length > 0; + return this.selectedFiles.length > 0; } get allSelectedFilesCanBeAssignedIntoSameState() { if (this.areSomeFilesSelected) { - const selectedFiles = this.selectedFiles; - const allFilesAreUnderReviewOrUnassigned = selectedFiles.reduce( + const allFilesAreUnderReviewOrUnassigned = this.selectedFiles.reduce( (acc, file) => acc && (file.isUnderReview || file.isUnassigned), true ); - const allFilesAreUnderApproval = selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true); + const allFilesAreUnderApproval = this.selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true); return allFilesAreUnderReviewOrUnassigned || allFilesAreUnderApproval; } return false; @@ -144,11 +136,14 @@ export class DossierOverviewBulkActionsComponent { async () => { this._loadingService.start(); await this._fileManagementControllerService - .deleteFiles(this.selectedFileIds, this._appStateService.activeDossierId) + .deleteFiles( + this.selectedFiles.map(item => item.fileId), + this._appStateService.activeDossierId + ) .toPromise(); await this._appStateService.reloadActiveDossierFiles(); this.reload.emit(); - this.selectedFileIds.splice(0, this.selectedFileIds.length); + this._screenStateService.setSelectedEntities([]); this._loadingService.stop(); } ); @@ -158,12 +153,9 @@ export class DossierOverviewBulkActionsComponent { // If more than 1 approver - show dialog and ask who to assign if (this._appStateService.activeDossier.approverIds.length > 1) { this._loadingService.start(); - const files = this.selectedFileIds.map(fileId => - this._appStateService.getFileById(this._appStateService.activeDossierId, fileId) - ); this._dialogService.openAssignFileToUserDialog( - files, + this.selectedFiles, 'approver', () => { this.reload.emit(); @@ -203,11 +195,10 @@ export class DossierOverviewBulkActionsComponent { assign() { this._loadingService.start(); - const files = this.selectedFileIds.map(fileId => this._appStateService.getFileById(this._appStateService.activeDossierId, fileId)); - const mode = files[0].isUnderApproval ? 'approver' : 'reviewer'; + const mode = this.selectedFiles[0].isUnderApproval ? 'approver' : 'reviewer'; - this._dialogService.openAssignFileToUserDialog(files, mode, () => { + this._dialogService.openAssignFileToUserDialog(this.selectedFiles, mode, () => { this.reload.emit(); this._loadingService.stop(); }); diff --git a/apps/red-ui/src/app/modules/dossier/components/dossier-details/dossier-details.component.html b/apps/red-ui/src/app/modules/dossier/components/dossier-details/dossier-details.component.html index 62a7c0101..89394475d 100644 --- a/apps/red-ui/src/app/modules/dossier/components/dossier-details/dossier-details.component.html +++ b/apps/red-ui/src/app/modules/dossier/components/dossier-details/dossier-details.component.html @@ -42,9 +42,7 @@
@@ -69,9 +67,9 @@ >
-
+
-
{{ appStateService.activeDossier.dossier.description }}
+
{{ description }}
diff --git a/apps/red-ui/src/app/modules/dossier/components/dossier-details/dossier-details.component.ts b/apps/red-ui/src/app/modules/dossier/components/dossier-details/dossier-details.component.ts index 3eba24b9d..5ad0f88f5 100644 --- a/apps/red-ui/src/app/modules/dossier/components/dossier-details/dossier-details.component.ts +++ b/apps/red-ui/src/app/modules/dossier/components/dossier-details/dossier-details.component.ts @@ -7,9 +7,8 @@ import { TranslateChartService } from '@services/translate-chart.service'; import { StatusSorter } from '@utils/sorters/status-sorter'; import { UserService } from '@services/user.service'; import { User } from '@redaction/red-ui-http'; -import { NotificationService } from '@services/notification.service'; -import { FilterService } from '@shared/services/filter.service'; -import { FileStatusWrapper } from '@models/file/file-status.wrapper'; +import { Toaster } from '../../../../services/toaster.service'; +import { FilterService } from '../../../shared/services/filter.service'; import { DossierAttributeWithValue } from '@models/dossier-attributes.model'; @Component({ @@ -26,14 +25,16 @@ export class DossierDetailsComponent implements OnInit { @Output() openDossierDictionaryDialog = new EventEmitter(); @Output() toggleCollapse = new EventEmitter(); + readonly needsWorkFilters$ = this.filterService.getFilterModels$('needsWorkFilters'); + constructor( readonly appStateService: AppStateService, readonly translateChartService: TranslateChartService, readonly permissionsService: PermissionsService, - readonly filterService: FilterService, + readonly filterService: FilterService, private readonly _changeDetectorRef: ChangeDetectorRef, private readonly _userService: UserService, - private readonly _notificationService: NotificationService + private readonly _toaster: Toaster ) {} get memberIds(): string[] { @@ -71,7 +72,7 @@ export class DossierDetailsComponent implements OnInit { key: key }); } - this.documentsChartData.sort((a, b) => StatusSorter[a.key] - StatusSorter[b.key]); + this.documentsChartData.sort(StatusSorter.byStatus); this.documentsChartData = this.translateChartService.translateStatus(this.documentsChartData); this._changeDetectorRef.detectChanges(); } @@ -80,11 +81,11 @@ export class DossierDetailsComponent implements OnInit { this.owner = typeof user === 'string' ? this._userService.getRedUserById(user) : user; const dw = Object.assign({}, this.appStateService.activeDossier); dw.dossier.ownerId = this.owner.userId; - await this.appStateService.addOrUpdateDossier(dw.dossier); + await this.appStateService.createOrUpdateDossier(dw.dossier); const ownerName = this._userService.getNameForId(this.owner.userId); const dossierName = this.appStateService.activeDossier.name; const msg = 'Successfully assigned ' + ownerName + ' to dossier: ' + dossierName; - this._notificationService.showToastNotification(msg); + this._toaster.info(msg); } } diff --git a/apps/red-ui/src/app/modules/dossier/components/dossier-listing-actions/dossier-listing-actions.component.ts b/apps/red-ui/src/app/modules/dossier/components/dossier-listing-actions/dossier-listing-actions.component.ts index ae79b6868..e8f6d2469 100644 --- a/apps/red-ui/src/app/modules/dossier/components/dossier-listing-actions/dossier-listing-actions.component.ts +++ b/apps/red-ui/src/app/modules/dossier/components/dossier-listing-actions/dossier-listing-actions.component.ts @@ -2,7 +2,6 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { PermissionsService } from '@services/permissions.service'; import { DossierWrapper } from '@state/model/dossier.wrapper'; import { StatusSorter } from '@utils/sorters/status-sorter'; -import { FileManagementControllerService } from '@redaction/red-ui-http'; import { AppStateService } from '@state/app-state.service'; import { DossiersDialogService } from '../../services/dossiers-dialog.service'; @@ -19,16 +18,13 @@ export class DossierListingActionsComponent { constructor( readonly permissionsService: PermissionsService, readonly appStateService: AppStateService, - private readonly _dialogService: DossiersDialogService, - private readonly _fileManagementControllerService: FileManagementControllerService + private readonly _dialogService: DossiersDialogService ) {} openEditDossierDialog($event: MouseEvent, dossierWrapper: DossierWrapper) { this._dialogService.openDialog('editDossier', $event, { dossierWrapper, - afterSave: () => { - this.actionPerformed.emit(); - } + afterSave: () => this.actionPerformed.emit() }); } @@ -51,7 +47,7 @@ export class DossierListingActionsComponent { }, {}); return Object.keys(obj) - .sort((a, b) => StatusSorter[a] - StatusSorter[b]) + .sort(StatusSorter.byStatus) .map(status => ({ length: obj[status], color: status })); } } diff --git a/apps/red-ui/src/app/modules/dossier/components/dossier-listing-details/dossier-listing-details.component.html b/apps/red-ui/src/app/modules/dossier/components/dossier-listing-details/dossier-listing-details.component.html index 1bb665327..6609a95ad 100644 --- a/apps/red-ui/src/app/modules/dossier/components/dossier-listing-details/dossier-listing-details.component.html +++ b/apps/red-ui/src/app/modules/dossier/components/dossier-listing-details/dossier-listing-details.component.html @@ -27,7 +27,6 @@
{ +export class DossierListingDetailsComponent { @Input() dossiersChartData: DoughnutChartConfig[]; @Input() documentsChartData: DoughnutChartConfig[]; - constructor(readonly appStateService: AppStateService, readonly filterService: FilterService) {} + constructor(readonly appStateService: AppStateService, readonly filterService: FilterService) {} } diff --git a/apps/red-ui/src/app/modules/dossier/components/page-exclusion/page-exclusion.component.ts b/apps/red-ui/src/app/modules/dossier/components/page-exclusion/page-exclusion.component.ts index 7245e5ccc..0dd8eb2bb 100644 --- a/apps/red-ui/src/app/modules/dossier/components/page-exclusion/page-exclusion.component.ts +++ b/apps/red-ui/src/app/modules/dossier/components/page-exclusion/page-exclusion.component.ts @@ -2,9 +2,9 @@ import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from import { PermissionsService } from '@services/permissions.service'; import { FormBuilder, FormGroup } from '@angular/forms'; import { PageRange, ReanalysisControllerService } from '@redaction/red-ui-http'; -import { FileDataModel } from '@models/file/file-data.model'; -import { NotificationService, NotificationType } from '@services/notification.service'; -import { LoadingService } from '@services/loading.service'; +import { FileDataModel } from '../../../../models/file/file-data.model'; +import { Toaster } from '../../../../services/toaster.service'; +import { LoadingService } from '../../../../services/loading.service'; import { TranslateService } from '@ngx-translate/core'; @Component({ @@ -23,9 +23,8 @@ export class PageExclusionComponent implements OnChanges { readonly permissionsService: PermissionsService, private readonly _formBuilder: FormBuilder, private readonly _reanalysisControllerService: ReanalysisControllerService, - private readonly _notificationService: NotificationService, - private readonly _loadingService: LoadingService, - private readonly _translateService: TranslateService + private readonly _toaster: Toaster, + private readonly _loadingService: LoadingService ) { this.excludePagesForm = this._formBuilder.group({ value: [''] @@ -80,11 +79,7 @@ export class PageExclusionComponent implements OnChanges { this.excludePagesForm.reset(); this.actionPerformed.emit('exclude-pages'); } catch (e) { - this._notificationService.showToastNotification( - this._translateService.instant('file-preview.tabs.exclude-pages.error'), - null, - NotificationType.ERROR - ); + this._toaster.error('file-preview.tabs.exclude-pages.error'); this._loadingService.stop(); } } diff --git a/apps/red-ui/src/app/modules/dossier/components/team-members-manager/team-members-manager.component.ts b/apps/red-ui/src/app/modules/dossier/components/team-members-manager/team-members-manager.component.ts index d8d4d6b16..665fa19c8 100644 --- a/apps/red-ui/src/app/modules/dossier/components/team-members-manager/team-members-manager.component.ts +++ b/apps/red-ui/src/app/modules/dossier/components/team-members-manager/team-members-manager.component.ts @@ -1,8 +1,8 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { Dossier, DossierControllerService, StatusControllerService } from '@redaction/red-ui-http'; +import { Dossier } from '@redaction/red-ui-http'; import { AppStateService } from '@state/app-state.service'; import { UserService } from '@services/user.service'; -import { NotificationService, NotificationType } from '@services/notification.service'; +import { Toaster } from '../../../../services/toaster.service'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { DossierWrapper } from '@state/model/dossier.wrapper'; @@ -21,10 +21,8 @@ export class TeamMembersManagerComponent implements OnInit { constructor( readonly userService: UserService, - private readonly _dossierControllerService: DossierControllerService, - private readonly _notificationService: NotificationService, + private readonly _toaster: Toaster, private readonly _formBuilder: FormBuilder, - private readonly _statusControllerService: StatusControllerService, private readonly _appStateService: AppStateService ) {} @@ -88,14 +86,10 @@ export class TeamMembersManagerComponent implements OnInit { dw.dossier.memberIds = memberIds; dw.dossier.approverIds = approverIds; dw.dossier.ownerId = ownerId; - result = await this._appStateService.addOrUpdateDossier(dw.dossier); + result = await this._appStateService.createOrUpdateDossier(dw.dossier); this.save.emit(result); } catch (error) { - this._notificationService.showToastNotification( - 'Failed: ' + error.error ? error.error.message : error, - null, - NotificationType.ERROR - ); + this._toaster.error('Failed: ' + error.error ? error.error.message : error); } } diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.ts index 1bcf7568c..a0cf0b415 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.ts @@ -64,7 +64,7 @@ export class AddDossierDialogComponent { dossier.memberIds = foundDossier.memberIds; } - const savedDossier = await this._appStateService.addOrUpdateDossier(dossier); + const savedDossier = await this._appStateService.createOrUpdateDossier(dossier); if (savedDossier) { this.dialogRef.close({ dossier: savedDossier }); } @@ -72,7 +72,7 @@ export class AddDossierDialogComponent { async saveDossierAndAddMembers() { const dossier: Dossier = this._formToObject(); - const savedDossier = await this._appStateService.addOrUpdateDossier(dossier); + const savedDossier = await this._appStateService.createOrUpdateDossier(dossier); if (savedDossier) { this.dialogRef.close({ addMembers: true, dossier: savedDossier }); } diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component.ts index 0c7390409..b82a86f1d 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component.ts @@ -1,9 +1,9 @@ import { Component, Inject } from '@angular/core'; -import { DossierControllerService, StatusControllerService } from '@redaction/red-ui-http'; +import { StatusControllerService } from '@redaction/red-ui-http'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { AppStateService } from '@state/app-state.service'; import { UserService } from '@services/user.service'; -import { NotificationService, NotificationType } from '@services/notification.service'; +import { Toaster } from '../../../../services/toaster.service'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FileStatusWrapper } from '@models/file/file-status.wrapper'; import { DossierWrapper } from '@state/model/dossier.wrapper'; @@ -16,7 +16,6 @@ class DialogData { } @Component({ - selector: 'redaction-dossier-details-dialog', templateUrl: './assign-reviewer-approver-dialog.component.html', styleUrls: ['./assign-reviewer-approver-dialog.component.scss'] }) @@ -26,13 +25,12 @@ export class AssignReviewerApproverDialogComponent { constructor( readonly userService: UserService, - private readonly _dossierControllerService: DossierControllerService, - private readonly _notificationService: NotificationService, + private readonly _toaster: Toaster, private readonly _formBuilder: FormBuilder, private readonly _statusControllerService: StatusControllerService, private readonly _appStateService: AppStateService, - public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: DialogData + private readonly _dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) readonly data: DialogData ) { this._loadData(); } @@ -52,10 +50,8 @@ export class AssignReviewerApproverDialogComponent { return true; } - const reviewerId = this.selectedSingleUser; - for (const file of this.data.files) { - if (file.currentReviewer !== reviewerId) { + if (file.currentReviewer !== this.selectedSingleUser) { return true; } } @@ -94,14 +90,10 @@ export class AssignReviewerApproverDialogComponent { file.reviewerName = this.userService.getNameForId(selectedUser); } } catch (error) { - this._notificationService.showToastNotification( - 'Failed: ' + error.error ? error.error.message : error, - null, - NotificationType.ERROR - ); + this._toaster.error('Failed: ' + error.error ? error.error.message : error); } - this.dialogRef.close(); + this._dialogRef.close(); } private _loadData() { diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/download-package/edit-dossier-download-package.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/download-package/edit-dossier-download-package.component.ts index c00e79dab..b336ede21 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/download-package/edit-dossier-download-package.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/download-package/edit-dossier-download-package.component.ts @@ -71,7 +71,7 @@ export class EditDossierDownloadPackageComponent implements OnInit, EditDossierS downloadFileTypes: this.dossierForm.get('downloadFileTypes').value, reportTypes: this.dossierForm.get('reportTypes').value }; - const updatedDossier = await this._appStateService.addOrUpdateDossier(dossier); + const updatedDossier = await this._appStateService.createOrUpdateDossier(dossier); this.updateDossier.emit(updatedDossier); } diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.ts index 46f7b73d7..3a2624f57 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.ts @@ -6,8 +6,7 @@ import { DossierWrapper } from '@state/model/dossier.wrapper'; import { EditDossierGeneralInfoComponent } from './general-info/edit-dossier-general-info.component'; import { EditDossierDownloadPackageComponent } from './download-package/edit-dossier-download-package.component'; import { EditDossierSectionInterface } from './edit-dossier-section.interface'; -import { NotificationService, NotificationType } from '@services/notification.service'; -import { TranslateService } from '@ngx-translate/core'; +import { Toaster } from '../../../../services/toaster.service'; import { EditDossierDictionaryComponent } from './dictionary/edit-dossier-dictionary.component'; import { EditDossierTeamMembersComponent } from './team-members/edit-dossier-team-members.component'; import { EditDossierAttributesComponent } from './attributes/edit-dossier-attributes.component'; @@ -32,12 +31,8 @@ export class EditDossierDialogComponent { @ViewChild(EditDossierAttributesComponent) attributesComponent: EditDossierAttributesComponent; constructor( - private readonly _appStateService: AppStateService, - private readonly _formBuilder: FormBuilder, - private readonly _notificationService: NotificationService, - private readonly _translateService: TranslateService, + private readonly _toaster: Toaster, private readonly _changeRef: ChangeDetectorRef, - private readonly _dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) private readonly _data: { dossierWrapper: DossierWrapper; @@ -102,11 +97,7 @@ export class EditDossierDialogComponent { } updatedDossier(updatedDossier: DossierWrapper) { - this._notificationService.showToastNotification( - this._translateService.instant('edit-dossier-dialog.change-successful'), - null, - NotificationType.SUCCESS - ); + this._toaster.success('edit-dossier-dialog.change-successful'); if (updatedDossier) { this.dossierWrapper = updatedDossier; @@ -131,11 +122,7 @@ export class EditDossierDialogComponent { changeTab(key: Section) { if (this.activeComponent.changed) { - this._notificationService.showToastNotification( - this._translateService.instant('edit-dossier-dialog.unsaved-changes'), - null, - NotificationType.ERROR - ); + this._toaster.error('edit-dossier-dialog.unsaved-changes'); return; } this.activeNav = key; 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 3435b47be..dc52079f7 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 @@ -10,8 +10,7 @@ import { PermissionsService } from '@services/permissions.service'; import { Router } from '@angular/router'; import { MatDialogRef } from '@angular/material/dialog'; import { EditDossierDialogComponent } from '../edit-dossier-dialog.component'; -import { NotificationService, NotificationType } from '@services/notification.service'; -import { TranslateService } from '@ngx-translate/core'; +import { Toaster } from '../../../../../services/toaster.service'; @Component({ selector: 'redaction-edit-dossier-general-info', @@ -34,8 +33,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti private readonly _dialogService: DossiersDialogService, private readonly _router: Router, private readonly _editDossierDialogRef: MatDialogRef, - private readonly _notificationService: NotificationService, - private readonly _translateService: TranslateService + private readonly _toaster: Toaster ) {} get changed() { @@ -100,7 +98,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti dueDate: this.hasDueDate ? this.dossierForm.get('dueDate').value : undefined, dossierTemplateId: this.dossierForm.get('dossierTemplateId').value }; - const updatedDossier = await this._appStateService.addOrUpdateDossier(dossier); + const updatedDossier = await this._appStateService.createOrUpdateDossier(dossier); if (updatedDossier) this.updateDossier.emit(updatedDossier); } @@ -113,11 +111,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti } private _notifyDossierDeleted() { - this._notificationService.showToastNotification( - this._translateService.instant('edit-dossier-dialog.delete-successful'), - null, - NotificationType.SUCCESS - ); + this._toaster.success('edit-dossier-dialog.delete-successful'); } private _filterInvalidDossierTemplates() { diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/force-redaction-dialog/force-redaction-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/force-redaction-dialog/force-redaction-dialog.component.ts index 8b905a9a1..c3de99226 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/force-redaction-dialog/force-redaction-dialog.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/force-redaction-dialog/force-redaction-dialog.component.ts @@ -3,7 +3,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AppStateService } from '@state/app-state.service'; import { MatDialogRef } from '@angular/material/dialog'; import { ForceRedactionRequest, LegalBasisMappingControllerService } from '@redaction/red-ui-http'; -import { NotificationService } from '@services/notification.service'; +import { Toaster } from '../../../../services/toaster.service'; import { TranslateService } from '@ngx-translate/core'; import { UserService } from '@services/user.service'; import { ManualAnnotationService } from '../../services/manual-annotation.service'; @@ -29,7 +29,7 @@ export class ForceRedactionDialogComponent implements OnInit { private readonly _appStateService: AppStateService, private readonly _userService: UserService, private readonly _formBuilder: FormBuilder, - private readonly _notificationService: NotificationService, + private readonly _notificationService: Toaster, private readonly _translateService: TranslateService, private readonly _legalBasisMappingControllerService: LegalBasisMappingControllerService, private readonly _manualAnnotationService: ManualAnnotationService, diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/manual-redaction-dialog/manual-annotation-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/manual-redaction-dialog/manual-annotation-dialog.component.ts index 85fe5c5e1..b5c5d24df 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/manual-redaction-dialog/manual-annotation-dialog.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/manual-redaction-dialog/manual-annotation-dialog.component.ts @@ -3,7 +3,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AppStateService } from '@state/app-state.service'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { AddRedactionRequest, LegalBasisMappingControllerService } from '@redaction/red-ui-http'; -import { NotificationService } from '@services/notification.service'; +import { Toaster } from '../../../../services/toaster.service'; import { TranslateService } from '@ngx-translate/core'; import { UserService } from '@services/user.service'; import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper'; @@ -37,7 +37,7 @@ export class ManualAnnotationDialogComponent implements OnInit { private readonly _appStateService: AppStateService, private readonly _userService: UserService, private readonly _formBuilder: FormBuilder, - private readonly _notificationService: NotificationService, + private readonly _notificationService: Toaster, private readonly _translateService: TranslateService, private readonly _legalBasisMappingControllerService: LegalBasisMappingControllerService, private readonly _manualAnnotationService: ManualAnnotationService, diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossier-listing-screen/dossier-listing-screen.component.html b/apps/red-ui/src/app/modules/dossier/screens/dossier-listing-screen/dossier-listing-screen.component.html index 0b39ef8ad..e65ce9f1a 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossier-listing-screen/dossier-listing-screen.component.html +++ b/apps/red-ui/src/app/modules/dossier/screens/dossier-listing-screen/dossier-listing-screen.component.html @@ -8,52 +8,25 @@
-
- - {{ 'dossier-listing.table-header.title' | translate: { length: (displayedEntities$ | async)?.length || 0 } }} - - - -
- -
- - - - - - - - -
-
+ - +
diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossier-listing-screen/dossier-listing-screen.component.ts b/apps/red-ui/src/app/modules/dossier/screens/dossier-listing-screen/dossier-listing-screen.component.ts index 2b1720166..ff41dbbcd 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossier-listing-screen/dossier-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/dossier-listing-screen/dossier-listing-screen.component.ts @@ -1,14 +1,13 @@ -import { Component, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectorRef, Component, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { Dossier, DossierTemplateModel } from '@redaction/red-ui-http'; import { AppStateService } from '@state/app-state.service'; import { UserService } from '@services/user.service'; import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component'; import { groupBy } from '@utils/functions'; import { TranslateService } from '@ngx-translate/core'; -import { PermissionsService } from '@services/permissions.service'; import { DossierWrapper } from '@state/model/dossier.wrapper'; -import { Subscription, timer } from 'rxjs'; -import { filter, tap } from 'rxjs/operators'; +import { timer } from 'rxjs'; +import { filter } from 'rxjs/operators'; import { TranslateChartService } from '@services/translate-chart.service'; import { RedactionFilterSorter } from '@utils/sorters/redaction-filter-sorter'; import { StatusSorter } from '@utils/sorters/status-sorter'; @@ -28,16 +27,22 @@ import { FilterService } from '@shared/services/filter.service'; import { SearchService } from '@shared/services/search.service'; import { ScreenStateService } from '@shared/services/screen-state.service'; import { BaseListingComponent } from '@shared/base/base-listing.component'; -import { ScreenNames, SortingService } from '@services/sorting.service'; +import { SortingService } from '@services/sorting.service'; +import { TableColConfig } from '../../../shared/components/table-col-name/table-col-name.component'; + +const isLeavingScreen = event => event instanceof NavigationStart && event.url !== '/main/dossiers'; @Component({ templateUrl: './dossier-listing-screen.component.html', styleUrls: ['./dossier-listing-screen.component.scss'], providers: [FilterService, SearchService, ScreenStateService, SortingService] }) -export class DossierListingScreenComponent extends BaseListingComponent implements OnInit, OnDestroy, OnAttach, OnDetach { - dossiersChartData: DoughnutChartConfig[] = []; - documentsChartData: DoughnutChartConfig[] = []; +export class DossierListingScreenComponent + extends BaseListingComponent + implements OnInit, AfterViewInit, OnDestroy, OnAttach, OnDetach +{ + readonly itemSize = 95; + protected readonly _primaryKey = 'dossierName'; buttonConfigs: ButtonConfig[] = [ { label: this._translateService.instant('dossier-listing.add-new'), @@ -47,19 +52,34 @@ export class DossierListingScreenComponent extends BaseListingComponent; constructor( - readonly permissionsService: PermissionsService, private readonly _translateChartService: TranslateChartService, private readonly _userService: UserService, private readonly _dialogService: DossiersDialogService, @@ -67,70 +87,47 @@ export class DossierListingScreenComponent extends BaseListingComponent p.dossier.status === Dossier.StatusEnum.ACTIVE).length; - } - - private get _inactiveDossiersCount(): number { - return this._screenStateService.entities.length - this._activeDossiersCount; - } - ngOnInit(): void { this.calculateData(); - this._dossierAutoUpdateTimer = timer(0, 10000) - .pipe( - tap(async () => { - await this._appStateService.loadAllDossiers(); - this._loadEntitiesFromState(); - this.calculateData(); - }) - ) - .subscribe(); - - this._fileChangedSub = this._appStateService.fileChanged.subscribe(() => { + this.addSubscription = timer(0, 10000).subscribe(async () => { + await this._appStateService.loadAllDossiers(); + this._loadEntitiesFromState(); this.calculateData(); }); - this._routerEventsScrollPositionSub = this._router.events - .pipe(filter(event => event instanceof NavigationStart)) - .subscribe((event: NavigationStart) => { - if (event.url !== '/main/dossiers') { - this._lastScrollPosition = this.scrollViewport.measureScrollOffset('top'); - } - }); + this.addSubscription = this._appStateService.fileChanged.subscribe(() => { + this.calculateData(); + }); + + this.addSubscription = this._router.events.pipe(filter(isLeavingScreen)).subscribe(() => { + this._lastScrollPosition = this.scrollViewport.measureScrollOffset('top'); + }); + } + + ngAfterViewInit() { + this.changeDetectorRef.detectChanges(); } ngOnAttach() { + this.scrollViewport.scrollTo({ top: this._lastScrollPosition }); this._appStateService.reset(); this._loadEntitiesFromState(); this.ngOnInit(); - this.scrollViewport.scrollTo({ top: this._lastScrollPosition }); } - ngOnDetach() { + ngOnDetach(): void { this.ngOnDestroy(); } - ngOnDestroy(): void { - this._dossierAutoUpdateTimer.unsubscribe(); - this._routerEventsScrollPositionSub.unsubscribe(); - this._fileChangedSub.unsubscribe(); - } - getDossierTemplate(dw: DossierWrapper): DossierTemplateModel { return this._appStateService.getDossierTemplateById(dw.dossier.dossierTemplateId); } @@ -147,6 +144,18 @@ export class DossierListingScreenComponent extends BaseListingComponent p.dossier.status === Dossier.StatusEnum.ACTIVE).length; + } + + private get _inactiveDossiersCount(): number { + return this.screenStateService.allEntities.length - this._activeDossiersCount; + } + calculateData() { this._computeAllFilters(); @@ -165,12 +174,12 @@ export class DossierListingScreenComponent extends BaseListingComponent StatusSorter[a.key] - StatusSorter[b.key]); + this.documentsChartData.sort(StatusSorter.byStatus); this.documentsChartData = this._translateChartService.translateStatus(this.documentsChartData); } private _loadEntitiesFromState() { - this._screenStateService.setEntities(this._appStateService.allDossiers); + this.screenStateService.setEntities(this._appStateService.allDossiers); } private _computeAllFilters() { @@ -179,7 +188,7 @@ export class DossierListingScreenComponent extends BaseListingComponent(); const allDistinctDossierTemplates = new Set(); - this._screenStateService?.entities?.forEach(entry => { + this.screenStateService?.allEntities?.forEach(entry => { // all people entry.dossier.memberIds.forEach(f => allDistinctPeople.add(f)); // Needs work @@ -200,11 +209,11 @@ export class DossierListingScreenComponent extends BaseListingComponent quickFilters.reduce((acc, f) => acc || (f.checked && f.checker(dw)), false) }); - - this.filterService.filterEntities(); } private _createQuickFilters() { @@ -267,22 +274,22 @@ export class DossierListingScreenComponent extends BaseListingComponent dw.ownerId === this._user.id + checker: (dw: DossierWrapper) => dw.ownerId === this._userId }, { key: 'to-approve', label: this._translateService.instant('dossier-listing.quick-filters.to-approve'), - checker: (dw: DossierWrapper) => dw.approverIds.includes(this._user.id) + checker: (dw: DossierWrapper) => dw.approverIds.includes(this._userId) }, { key: 'to-review', label: this._translateService.instant('dossier-listing.quick-filters.to-review'), - checker: (dw: DossierWrapper) => dw.memberIds.includes(this._user.id) + checker: (dw: DossierWrapper) => dw.memberIds.includes(this._userId) }, { key: 'other', label: this._translateService.instant('dossier-listing.quick-filters.other'), - checker: (dw: DossierWrapper) => !dw.memberIds.includes(this._user.id) + checker: (dw: DossierWrapper) => !dw.memberIds.includes(this._userId) } ]; diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.html b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.html index 6321336dd..95d46d14b 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.html +++ b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.html @@ -5,17 +5,17 @@ [showCloseButton]="true" >
- {{ 'dossier-overview.table-header.title' | translate: { length: (displayedEntities$ | async)?.length || 0 } }} + {{ 'dossier-overview.table-header.title' | translate: { length: (screenStateService.displayedLength$ | async) || 0 } }} - +
-
+
- +
implements OnInit, OnDestroy, OnDetach, OnAttach { - collapsedDetails = false; readonly itemSize = 80; + protected readonly _primaryKey = 'filename'; + collapsedDetails = false; actionConfigs: ActionConfig[]; dossierAttributes: DossierAttributeWithValue[] = []; @ViewChild(DossierDetailsComponent, { static: false }) private readonly _dossierDetailsComponent: DossierDetailsComponent; - private _filesAutoUpdateTimer: Subscription; - private _routerEventsScrollPositionSub: Subscription; - private _fileChangedSub: Subscription; + private readonly _lastOpenedFileKey = 'Dossier-Recent-' + this.activeDossier.dossierId; private _lastScrollPosition: number; - private _lastOpenedFileId = ''; + @ViewChild('needsWorkTemplate', { read: TemplateRef, static: true }) private readonly _needsWorkTemplate: TemplateRef; @ViewChild('fileInput') private _fileInput: ElementRef; constructor( - readonly permissionsService: PermissionsService, private readonly _userService: UserService, - private readonly _notificationService: NotificationService, + private readonly _toaster: Toaster, private readonly _dialogService: DossiersDialogService, private readonly _fileUploadService: FileUploadService, private readonly _statusOverlayService: StatusOverlayService, @@ -69,7 +66,7 @@ export class DossierOverviewScreenComponent private readonly _translateService: TranslateService, private readonly _fileDropOverlayService: FileDropOverlayService, private readonly _appStateService: AppStateService, - private readonly _userPreferenceControllerService: UserPreferenceControllerService, + private readonly _userPreferenceService: UserPreferenceService, private readonly _appConfigService: AppConfigService, private readonly _changeDetectorRef: ChangeDetectorRef, private readonly _loadingService: LoadingService, @@ -77,9 +74,6 @@ export class DossierOverviewScreenComponent protected readonly _injector: Injector ) { super(_injector); - this._sortingService.setScreenName(ScreenNames.DOSSIER_OVERVIEW); - this._searchService.setSearchKey('searchField'); - this._screenStateService.setIdKey('fileId'); this._loadEntitiesFromState(); } @@ -87,42 +81,38 @@ export class DossierOverviewScreenComponent return this._appStateService.activeDossier; } - get user() { - return this._userService.user; + get userId() { + return this._userService.userId; } get checkedRequiredFilters() { - return this.filterService.getFilter('quickFilters')?.values.filter(f => f.required && f.checked); + return this.filterService.getFilterGroup('quickFilters')?.values.filter(f => f.required && f.checked); } get checkedNotRequiredFilters() { - return this.filterService.getFilter('quickFilters')?.values.filter(f => !f.required && f.checked); + return this.filterService.getFilterGroup('quickFilters')?.values.filter(f => !f.required && f.checked); } - isLastOpenedFile(fileStatus: FileStatusWrapper): boolean { - return this._lastOpenedFileId === fileStatus.fileId; + isLastOpenedFile({ fileId }: FileStatusWrapper): boolean { + return this._userPreferenceService.getLastOpenedFileId(this._lastOpenedFileKey) === fileId; } - async ngOnInit() { + async ngOnInit(): Promise { this._fileDropOverlayService.initFileDropHandling(); this.calculateData(); - this._filesAutoUpdateTimer = timer(0, 7500) - .pipe( - tap(async () => { - await this._appStateService.reloadActiveDossierFilesIfNecessary(); - this._loadEntitiesFromState(); - }) - ) - .subscribe(); + this.addSubscription = timer(0, 7500).subscribe(async () => { + await this._appStateService.reloadActiveDossierFilesIfNecessary(); + this._loadEntitiesFromState(); + }); - this._fileChangedSub = this._appStateService.fileChanged.subscribe(() => { + this.addSubscription = this._appStateService.fileChanged.subscribe(() => { this.calculateData(); }); - this._routerEventsScrollPositionSub = this._router.events - .pipe(filter(events => events instanceof NavigationStart)) + this.addSubscription = this._router.events + .pipe(filter(event => event instanceof NavigationStart)) .subscribe((event: NavigationStart) => { if (!event.url.endsWith(this._appStateService.activeDossierId)) { this._lastScrollPosition = this.scrollViewport.measureScrollOffset('top'); @@ -131,13 +121,6 @@ export class DossierOverviewScreenComponent this._loadingService.start(); - const userAttributes = await this._userPreferenceControllerService.getAllUserAttributes(); - if (userAttributes === null || userAttributes === undefined) return; - const key = 'Dossier-Recent-' + this.activeDossier.dossierId; - if (userAttributes[key]?.length > 0) { - this._lastOpenedFileId = userAttributes[key][0]; - } - this.dossierAttributes = await this._dossierAttributesService.getValues(this.activeDossier); this._loadingService.stop(); @@ -145,9 +128,7 @@ export class DossierOverviewScreenComponent ngOnDestroy(): void { this._fileDropOverlayService.cleanupFileDropHandling(); - this._filesAutoUpdateTimer.unsubscribe(); - this._fileChangedSub.unsubscribe(); - this._routerEventsScrollPositionSub.unsubscribe(); + super.ngOnDestroy(); } async ngOnAttach() { @@ -165,33 +146,13 @@ export class DossierOverviewScreenComponent .reanalyzeDossier() .then(() => { this.reloadDossiers(); - this._notificationService.showToastNotification( - this._translateService.instant('dossier-overview.reanalyse-dossier.success'), - null, - NotificationType.SUCCESS - ); + this._toaster.success('dossier-overview.reanalyse-dossier.success'); }) - .catch(() => { - this._notificationService.showToastNotification( - this._translateService.instant('dossier-overview.reanalyse-dossier.error'), - null, - NotificationType.ERROR - ); - }); - } - - isError(fileStatusWrapper: FileStatusWrapper) { - return fileStatusWrapper.status === FileStatus.StatusEnum.ERROR; - } - - isProcessing(fileStatusWrapper: FileStatusWrapper) { - return [FileStatus.StatusEnum.REPROCESS, FileStatus.StatusEnum.FULLREPROCESS, FileStatus.StatusEnum.PROCESSING].includes( - fileStatusWrapper.status - ); + .catch(() => this._toaster.error('dossier-overview.reanalyse-dossier.error')); } reloadDossiers() { - this._appStateService.getFiles(this._appStateService.activeDossier, false).then(() => { + this._appStateService.getFiles(this.activeDossier, false).then(() => { this.calculateData(); }); } @@ -202,16 +163,10 @@ export class DossierOverviewScreenComponent this._loadEntitiesFromState(); this._computeAllFilters(); - this.filterService.filterEntities(); - this._dossierDetailsComponent?.calculateChartConfig(); this._changeDetectorRef.detectChanges(); } - trackByFileId(index: number, item: FileStatusWrapper) { - return item.fileId; - } - @HostListener('drop', ['$event']) onDrop(event: DragEvent) { handleFileDrop(event, this.activeDossier, this._uploadFiles.bind(this)); @@ -235,7 +190,7 @@ export class DossierOverviewScreenComponent } bulkActionPerformed() { - this._screenStateService.selectedEntitiesIds$.next([]); + this.screenStateService.setSelectedEntities([]); this.reloadDossiers(); } @@ -271,15 +226,12 @@ export class DossierOverviewScreenComponent moment(file.lastUpdated).add(this._appConfigService.getConfig(AppConfigKey.RECENT_PERIOD_IN_HOURS), 'hours').isAfter(moment()); private _loadEntitiesFromState() { - if (this.activeDossier) this._screenStateService.setEntities(this.activeDossier.files); + if (this.activeDossier) this.screenStateService.setEntities(this.activeDossier.files); } private async _uploadFiles(files: FileUploadModel[]) { const fileCount = await this._fileUploadService.uploadFiles(files); - if (fileCount) { - this._statusOverlayService.openUploadStatusOverlay(); - } - // this._changeDetectorRef.detectChanges(); + if (fileCount) this._statusOverlayService.openUploadStatusOverlay(); } private _computeAllFilters() { @@ -290,7 +242,7 @@ export class DossierOverviewScreenComponent const allDistinctAddedDates = new Set(); const allDistinctNeedsWork = new Set(); - this._screenStateService.entities.forEach(file => { + this.screenStateService.allEntities.forEach(file => { allDistinctPeople.add(file.currentReviewer); allDistinctFileStatusWrapper.add(file.status); allDistinctAddedDates.add(moment(file.added).format('DD/MM/YYYY')); @@ -309,11 +261,11 @@ export class DossierOverviewScreenComponent label: this._translateService.instant(item) })); - this.filterService.addFilter({ + this.filterService.addFilterGroup({ slug: 'statusFilters', label: this._translateService.instant('filters.status'), icon: 'red:status', - values: statusFilters.sort(StatusSorter.byKey), + values: statusFilters.sort(StatusSorter.byStatus), checker: keyChecker('status') }); @@ -332,7 +284,7 @@ export class DossierOverviewScreenComponent label: this._userService.getNameForId(userId) }); }); - this.filterService.addFilter({ + this.filterService.addFilterGroup({ slug: 'peopleFilters', label: this._translateService.instant('filters.assigned-people'), icon: 'red:user', @@ -345,7 +297,7 @@ export class DossierOverviewScreenComponent label: this._translateService.instant('filter.' + item) })); - this.filterService.addFilter({ + this.filterService.addFilterGroup({ slug: 'needsWorkFilters', label: this._translateService.instant('filters.needs-work'), icon: 'red:needs-work', @@ -356,7 +308,7 @@ export class DossierOverviewScreenComponent checkerArgs: this.permissionsService }); - this.filterService.addFilter({ + this.filterService.addFilterGroup({ slug: 'quickFilters', values: this._createQuickFilters(), checker: (file: FileStatusWrapper) => @@ -370,7 +322,7 @@ export class DossierOverviewScreenComponent private _createQuickFilters() { let quickFilters = []; - if (this._screenStateService.entities.filter(this.recentlyModifiedChecker).length > 0) { + if (this.screenStateService.allEntities.filter(this.recentlyModifiedChecker).length > 0) { const recentPeriod = this._appConfigService.getConfig(AppConfigKey.RECENT_PERIOD_IN_HOURS); quickFilters = [ { @@ -389,7 +341,7 @@ export class DossierOverviewScreenComponent { key: 'assigned-to-me', label: this._translateService.instant('dossier-overview.quick-filters.assigned-to-me'), - checker: (file: FileStatusWrapper) => file.currentReviewer === this.user.id + checker: (file: FileStatusWrapper) => file.currentReviewer === this.userId }, { key: 'unassigned', @@ -399,7 +351,7 @@ export class DossierOverviewScreenComponent { key: 'assigned-to-others', label: this._translateService.instant('dossier-overview.quick-filters.assigned-to-others'), - checker: (file: FileStatusWrapper) => !!file.currentReviewer && file.currentReviewer !== this.user.id + checker: (file: FileStatusWrapper) => !!file.currentReviewer && file.currentReviewer !== this.userId } ]; } diff --git a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts index 6c08ed19e..5552393f2 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts @@ -12,8 +12,7 @@ import { AnnotationData, FileDataModel } from '@models/file/file-data.model'; import { FileActionService } from '../../services/file-action.service'; import { AnnotationDrawService } from '../../services/annotation-draw.service'; import { AnnotationProcessingService } from '../../services/annotation-processing.service'; -import { tap } from 'rxjs/operators'; -import { NotificationService } from '@services/notification.service'; +import { Toaster } from '../../../../services/toaster.service'; import { FileStatusWrapper } from '@models/file/file-status.wrapper'; import { PermissionsService } from '@services/permissions.service'; import { Subscription, timer } from 'rxjs'; @@ -34,9 +33,10 @@ import { DossiersDialogService } from '../../services/dossiers-dialog.service'; import { OnAttach, OnDetach } from '@utils/custom-route-reuse.strategy'; import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model'; import { handleFilterDelta, processFilters } from '@shared/components/filters/popup-filter/utils/filter-utils'; -import { LoadingService } from '@services/loading.service'; +import { LoadingService } from '../../../../services/loading.service'; import { stampPDFPage } from '../../../../utils/page-stamper'; import { TranslateService } from '@ngx-translate/core'; +import { AutoUnsubscribeComponent } from '../../../shared/base/auto-unsubscribe.component'; const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f']; @@ -45,7 +45,7 @@ const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f']; templateUrl: './file-preview-screen.component.html', styleUrls: ['./file-preview-screen.component.scss'] }) -export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach, OnDetach { +export class FilePreviewScreenComponent extends AutoUnsubscribeComponent implements OnInit, OnDestroy, OnAttach, OnDetach { dialogRef: MatDialogRef; viewMode: ViewMode = 'STANDARD'; fullScreen = false; @@ -59,7 +59,6 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach, secondaryFilters: FilterModel[]; canPerformAnnotationActions: boolean; filesAutoUpdateTimer: Subscription; - fileReanalysedSubscription: Subscription; hideSkipped = false; displayPDFViewer = false; viewDocumentInfo = false; @@ -81,7 +80,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach, private readonly _activatedRoute: ActivatedRoute, private readonly _dialogService: DossiersDialogService, private readonly _router: Router, - private readonly _notificationService: NotificationService, + private readonly _toaster: Toaster, private readonly _annotationProcessingService: AnnotationProcessingService, private readonly _annotationDrawService: AnnotationDrawService, private readonly _fileActionService: FileActionService, @@ -92,6 +91,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach, private readonly _loadingService: LoadingService, private readonly _translateService: TranslateService ) { + super(); document.documentElement.addEventListener('fullscreenchange', () => { if (!document.fullscreenElement) { this.fullScreen = false; @@ -225,7 +225,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach, ngOnDetach() { this.displayPDFViewer = false; this.viewReady = false; - this._unsubscribeFromFileUpdates(); + super.ngOnDestroy(); } async ngOnAttach(previousRoute: ActivatedRouteSnapshot) { @@ -253,10 +253,6 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach, this.viewReady = true; } - ngOnDestroy(): void { - this._unsubscribeFromFileUpdates(); - } - rebuildFilters(deletePreviousAnnotations: boolean = false) { const startTime = new Date().getTime(); if (deletePreviousAnnotations) { @@ -473,7 +469,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach, await this._statusControllerService.setFileReviewer(dossierId, fileId, reviewerId).toPromise(); const msg = `Successfully assigned ${reviewerName} to file: ${filename}`; - this._notificationService.showToastNotification(msg); + this._toaster.info(msg); await this.appStateService.reloadActiveFile(); this._updateCanPerformActions(); this.editingReviewer = false; @@ -492,7 +488,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach, } downloadOriginalFile() { - this._fileManagementControllerService + this.addSubscription = this._fileManagementControllerService .downloadOriginalFile(this.dossierId, this.fileId, true, this.fileData.fileStatus.cacheIdentifier, 'response') .subscribe(data => { download(data, this.fileData.fileStatus.filename); @@ -543,10 +539,8 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach, } private _subscribeToFileUpdates(): void { - this.filesAutoUpdateTimer = timer(0, 5000) - .pipe(tap(async () => await this.appStateService.reloadActiveFile())) - .subscribe(); - this.fileReanalysedSubscription = this.appStateService.fileReanalysed.subscribe(async (fileStatus: FileStatusWrapper) => { + this.addSubscription = timer(0, 5000).subscribe(async () => await this.appStateService.reloadActiveFile()); + this.addSubscription = this.appStateService.fileReanalysed.subscribe(async (fileStatus: FileStatusWrapper) => { if (fileStatus.fileId === this.fileId) { await this._loadFileData(!this._reloadFileOnReanalysis); this._reloadFileOnReanalysis = false; @@ -557,11 +551,6 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach, }); } - private _unsubscribeFromFileUpdates(): void { - this.filesAutoUpdateTimer.unsubscribe(); - this.fileReanalysedSubscription.unsubscribe(); - } - private _updateCanPerformActions() { this.canPerformAnnotationActions = this.permissionsService.canPerformAnnotationActions() && diff --git a/apps/red-ui/src/app/modules/dossier/services/dossiers.service.ts b/apps/red-ui/src/app/modules/dossier/services/dossiers.service.ts index df92dfe8f..1379a4b02 100644 --- a/apps/red-ui/src/app/modules/dossier/services/dossiers.service.ts +++ b/apps/red-ui/src/app/modules/dossier/services/dossiers.service.ts @@ -1,11 +1,25 @@ import { Injectable } from '@angular/core'; -import { DossierControllerService } from '@redaction/red-ui-http'; +import { Dossier, DossierControllerService } from '@redaction/red-ui-http'; -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class DossiersService { constructor(private readonly _dossierControllerService: DossierControllerService) {} - getDeletedDossiers() { + createOrUpdate(dossier: Dossier): Promise { + return this._dossierControllerService.createOrUpdateDossier(dossier).toPromise(); + } + + delete(dossierId: string): Promise { + return this._dossierControllerService.deleteDossier(dossierId).toPromise(); + } + + getAll(): Promise { + return this._dossierControllerService.getDossiers().toPromise(); + } + + getDeleted(): Promise { return this._dossierControllerService.getDeletedDossiers().toPromise(); } diff --git a/apps/red-ui/src/app/modules/dossier/services/manual-annotation.service.ts b/apps/red-ui/src/app/modules/dossier/services/manual-annotation.service.ts index 055e3670d..102baff26 100644 --- a/apps/red-ui/src/app/modules/dossier/services/manual-annotation.service.ts +++ b/apps/red-ui/src/app/modules/dossier/services/manual-annotation.service.ts @@ -7,7 +7,7 @@ import { ManualRedactionControllerService } from '@redaction/red-ui-http'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; -import { NotificationService, NotificationType } from '@services/notification.service'; +import { Toaster } from '../../../services/toaster.service'; import { TranslateService } from '@ngx-translate/core'; import { tap } from 'rxjs/operators'; import { UserService } from '@services/user.service'; @@ -38,7 +38,7 @@ export class ManualAnnotationService { private readonly _appStateService: AppStateService, private readonly _userService: UserService, private readonly _translateService: TranslateService, - private readonly _notificationService: NotificationService, + private readonly _toaster: Toaster, private readonly _manualRedactionControllerService: ManualRedactionControllerService, private readonly _dictionaryControllerService: DictionaryControllerService, private readonly _permissionsService: PermissionsService @@ -76,8 +76,12 @@ export class ManualAnnotationService { return obs.pipe( tap( - () => this._notify(this._getMessage(mode)), - error => this._notify(this._getMessage(mode, modifyDictionary, true), NotificationType.ERROR, error) + () => this._toaster.success(this._getMessage(mode), { positionClass: 'toast-file-preview' }), + error => + this._toaster.error(this._getMessage(mode, modifyDictionary, true), { + params: error, + positionClass: 'toast-file-preview' + }) ) ); } @@ -223,13 +227,6 @@ export class ManualAnnotationService { } } - private _notify(key: string, type: NotificationType = NotificationType.SUCCESS, data?: any) { - this._notificationService.showToastNotification(this._translateService.instant(key, data), null, type, { - positionClass: 'toast-file-preview', - actions: [] - }); - } - private _getMessage(mode: Mode, modifyDictionary?: boolean, error: boolean = false) { return ( 'annotation-actions.message.' + diff --git a/apps/red-ui/src/app/modules/shared/base/auto-unsubscribe.component.ts b/apps/red-ui/src/app/modules/shared/base/auto-unsubscribe.component.ts new file mode 100644 index 000000000..761726bf7 --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/base/auto-unsubscribe.component.ts @@ -0,0 +1,28 @@ +import { Component, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; + +/** + * Inherit this class when you need to subscribe to observables in your components + */ +@Component({ template: '' }) +export abstract class AutoUnsubscribeComponent implements OnDestroy { + private _subscriptions = new Subscription(); + + /** + * Call this method when you want to subscribe to an observable + * @param subscription - the new subscription to add to subscriptions array + */ + set addSubscription(subscription: Subscription) { + this._subscriptions.closed = false; + this._subscriptions.add(subscription); + } + + /** + * This method unsubscribes active subscriptions + * If you implement OnDestroy in a component that inherits AutoUnsubscribeComponent, + * then you must explicitly call super.ngOnDestroy() + */ + ngOnDestroy(): void { + this._subscriptions.unsubscribe(); + } +} diff --git a/apps/red-ui/src/app/modules/shared/base/base-listing.component.ts b/apps/red-ui/src/app/modules/shared/base/base-listing.component.ts index cdb5858b6..9a3d2e34c 100644 --- a/apps/red-ui/src/app/modules/shared/base/base-listing.component.ts +++ b/apps/red-ui/src/app/modules/shared/base/base-listing.component.ts @@ -1,95 +1,97 @@ -import { Component, Injector, ViewChild } from '@angular/core'; -import { SortingOption, SortingService } from '@services/sorting.service'; +import { Component, Injector, OnDestroy, ViewChild } from '@angular/core'; +import { SortingOrders, SortingService } from '@services/sorting.service'; import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { FilterService } from '../services/filter.service'; import { SearchService } from '../services/search.service'; import { ScreenStateService } from '../services/screen-state.service'; -import { Observable } from 'rxjs'; -import { FilterModel } from '../components/filters/popup-filter/model/filter.model'; +import { combineLatest, Observable } from 'rxjs'; +import { AutoUnsubscribeComponent } from './auto-unsubscribe.component'; +import { distinctUntilChanged, map } from 'rxjs/operators'; +import { PermissionsService } from '../../../services/permissions.service'; @Component({ template: '' }) -export abstract class BaseListingComponent { +export abstract class BaseListingComponent extends AutoUnsubscribeComponent implements OnDestroy { @ViewChild(CdkVirtualScrollViewport) readonly scrollViewport: CdkVirtualScrollViewport; - readonly filterService: FilterService; - protected readonly _sortingService: SortingService; - protected readonly _searchService: SearchService; - protected readonly _screenStateService: ScreenStateService; + readonly permissionsService: PermissionsService; + readonly filterService: FilterService; + readonly sortingService: SortingService; + readonly searchService: SearchService; + readonly screenStateService: ScreenStateService; + + readonly sortedDisplayedEntities$: Observable; + readonly noMatch$: Observable; + + /** + * Key used in the *trackBy* function with **ngFor* or **cdkVirtualFor* + * and in the default sorting and as the search field + * @protected + */ + protected abstract _primaryKey: string; protected constructor(protected readonly _injector: Injector) { - this.filterService = this._injector.get>(FilterService); - this._sortingService = this._injector.get(SortingService); - this._searchService = this._injector.get>(SearchService); - this._screenStateService = this._injector.get>(ScreenStateService); + super(); + this.trackByPrimaryKey = this.trackByPrimaryKey.bind(this); + this.permissionsService = this._injector.get(PermissionsService); + this.filterService = this._injector.get(FilterService); + this.sortingService = this._injector.get(SortingService); + this.searchService = this._injector.get(SearchService); + this.screenStateService = this._injector.get>(ScreenStateService); + setTimeout(() => this.setInitialConfig()); + + this.sortedDisplayedEntities$ = this._sortedDisplayedEntities$; + this.noMatch$ = this._noMatch$; } - get selectedEntitiesIds$(): Observable { - return this._screenStateService.selectedEntitiesIds$; + setInitialConfig() { + this.sortingService.setSortingOption({ + column: this._primaryKey, + order: SortingOrders.ASC + }); + this.searchService.setSearchKey(this._primaryKey); } - get displayedEntities$(): Observable { - return this._screenStateService.displayedEntities$; + ngOnDestroy(): void { + super.ngOnDestroy(); } - get allEntities$(): Observable { - return this._screenStateService.entities$; - } - - get displayedEntities(): T[] { - return this._screenStateService.displayedEntities; + private get _sortedDisplayedEntities$(): Observable { + return this.screenStateService.displayedEntities$.pipe(map(entities => this.sortingService.defaultSort(entities))); } get allEntities(): T[] { - return this._screenStateService.entities; + return this.screenStateService.allEntities; } - get areAllEntitiesSelected() { - return this._screenStateService.areAllEntitiesSelected; + private get _noMatch$(): Observable { + return combineLatest([this.screenStateService.allEntitiesLength$, this.screenStateService.displayedLength$]).pipe( + map(([hasEntities, hasDisplayedEntities]) => hasEntities && !hasDisplayedEntities), + distinctUntilChanged() + ); } - get areSomeEntitiesSelected$() { - return this._screenStateService.areSomeEntitiesSelected$; - } - - get sortingOption(): SortingOption { - return this._sortingService.getSortingOption(); - } - - get searchForm() { - return this._searchService.searchForm; - } - - get noMatch(): boolean { - return this.allEntities.length && this.displayedEntities?.length === 0; - } - - get noData(): boolean { - return this.allEntities.length === 0; - } - - getFilter$(slug: string): Observable { - return this.filterService.getFilter$(slug); - } - - resetFilters() { - this.filterService.reset(); - } - - toggleSort($event) { - this._sortingService.toggleSort($event); + canBulkDelete$(hasPermission = true): Observable { + return this.screenStateService.areSomeEntitiesSelected$.pipe( + map(areSomeEntitiesSelected => areSomeEntitiesSelected && hasPermission), + distinctUntilChanged() + ); } toggleSelectAll() { - return this._screenStateService.toggleSelectAll(); + return this.screenStateService.selectEntities(); } toggleEntitySelected(event: MouseEvent, entity: T) { event.stopPropagation(); - return this._screenStateService.toggleEntitySelected(entity); + return this.screenStateService.selectEntities([entity]); } - isSelected(entity: T) { - return this._screenStateService.isSelected(entity); + isSelected(entity: T): boolean { + return this.screenStateService.isSelected(entity); + } + + trackByPrimaryKey(index: number, item: T) { + return item[this._primaryKey]; } } diff --git a/apps/red-ui/src/app/modules/shared/components/buttons/file-download-btn/file-download-btn.component.ts b/apps/red-ui/src/app/modules/shared/components/buttons/file-download-btn/file-download-btn.component.ts index dde07ed4c..081ec4fbd 100644 --- a/apps/red-ui/src/app/modules/shared/components/buttons/file-download-btn/file-download-btn.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/buttons/file-download-btn/file-download-btn.component.ts @@ -1,11 +1,12 @@ -import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, Input, OnDestroy } from '@angular/core'; import { PermissionsService } from '@services/permissions.service'; import { DossierWrapper } from '@state/model/dossier.wrapper'; import { FileStatusWrapper } from '@models/file/file-status.wrapper'; import { FileDownloadService } from '@upload-download/services/file-download.service'; -import { NotificationService } from '@services/notification.service'; -import { TranslateService } from '@ngx-translate/core'; +import { Toaster } from '@services/toaster.service'; import { BASE_HREF } from '../../../../../tokens'; +import { AutoUnsubscribeComponent } from '@shared/base/auto-unsubscribe.component'; +import { TranslateService } from '@ngx-translate/core'; export type MenuState = 'OPEN' | 'CLOSED'; @@ -15,7 +16,7 @@ export type MenuState = 'OPEN' | 'CLOSED'; styleUrls: ['./file-download-btn.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class FileDownloadBtnComponent { +export class FileDownloadBtnComponent extends AutoUnsubscribeComponent implements OnDestroy { @Input() dossier: DossierWrapper; @Input() file: FileStatusWrapper | FileStatusWrapper[]; @Input() tooltipPosition: 'above' | 'below' | 'before' | 'after' = 'above'; @@ -27,9 +28,11 @@ export class FileDownloadBtnComponent { @Inject(BASE_HREF) private readonly _baseHref: string, private readonly _permissionsService: PermissionsService, private readonly _fileDownloadService: FileDownloadService, - private readonly _translateService: TranslateService, - private readonly _notificationService: NotificationService - ) {} + private readonly _toaster: Toaster, + private readonly _translateService: TranslateService + ) { + super(); + } get canDownloadFiles() { if (!Array.isArray(this.file)) { @@ -47,12 +50,10 @@ export class FileDownloadBtnComponent { downloadFiles($event: MouseEvent) { $event.stopPropagation(); - this._fileDownloadService.downloadFiles(Array.isArray(this.file) ? this.file : [this.file], this.dossier).subscribe(() => { - this._notificationService.showToastNotification( - this._translateService.instant('download-status.queued', { - baseUrl: this._baseHref - }) - ); - }); + this.addSubscription = this._fileDownloadService + .downloadFiles(Array.isArray(this.file) ? this.file : [this.file], this.dossier) + .subscribe(() => { + this._toaster.info('download-status.queued', { params: { baseUrl: this._baseHref } }); + }); } } diff --git a/apps/red-ui/src/app/modules/shared/components/empty-state/empty-state.component.ts b/apps/red-ui/src/app/modules/shared/components/empty-state/empty-state.component.ts index 2f545af24..e3376736c 100644 --- a/apps/red-ui/src/app/modules/shared/components/empty-state/empty-state.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/empty-state/empty-state.component.ts @@ -1,9 +1,10 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; @Component({ selector: 'redaction-empty-state', templateUrl: './empty-state.component.html', - styleUrls: ['./empty-state.component.scss'] + styleUrls: ['./empty-state.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class EmptyStateComponent implements OnInit { @Input() text: string; @@ -15,8 +16,6 @@ export class EmptyStateComponent implements OnInit { @Input() verticalPadding = 120; @Output() action = new EventEmitter(); - constructor() {} - ngOnInit(): void { this.showButton = this.showButton && this.action.observers.length > 0; } diff --git a/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/model/filter-wrapper.model.ts b/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/model/filter-wrapper.model.ts index 1b19edc1d..ccdb3e6a8 100644 --- a/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/model/filter-wrapper.model.ts +++ b/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/model/filter-wrapper.model.ts @@ -1,7 +1,7 @@ import { FilterModel } from './filter.model'; import { TemplateRef } from '@angular/core'; -export interface FilterWrapper { +export interface FilterGroup { slug: string; label?: string; icon?: string; diff --git a/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/utils/filter-utils.ts b/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/utils/filter-utils.ts index 753c65f68..720d31006 100644 --- a/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/utils/filter-utils.ts +++ b/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/utils/filter-utils.ts @@ -2,7 +2,7 @@ import { FilterModel } from '../model/filter.model'; import { FileStatusWrapper } from '@models/file/file-status.wrapper'; import { DossierWrapper } from '@state/model/dossier.wrapper'; import { PermissionsService } from '@services/permissions.service'; -import { FilterWrapper } from '@shared/components/filters/popup-filter/model/filter-wrapper.model'; +import { FilterGroup } from '@shared/components/filters/popup-filter/model/filter-wrapper.model'; export function processFilters(oldFilters: FilterModel[], newFilters: FilterModel[]) { copySettings(oldFilters, newFilters); @@ -159,7 +159,7 @@ export const addedDateChecker = (dw: DossierWrapper, filter: FilterModel) => dw. export const dossierApproverChecker = (dw: DossierWrapper, filter: FilterModel) => dw.approverIds.includes(filter.key); -export function getFilteredEntities(entities: T[], filters: FilterWrapper[]) { +export function getFilteredEntities(entities: T[], filters: FilterGroup[]) { const filteredEntities: T[] = []; for (const entity of entities) { let add = true; diff --git a/apps/red-ui/src/app/modules/shared/components/filters/quick-filters/quick-filters.component.html b/apps/red-ui/src/app/modules/shared/components/filters/quick-filters/quick-filters.component.html index 6b7e6d732..533c9d10a 100644 --- a/apps/red-ui/src/app/modules/shared/components/filters/quick-filters/quick-filters.component.html +++ b/apps/red-ui/src/app/modules/shared/components/filters/quick-filters/quick-filters.component.html @@ -1,6 +1,6 @@
diff --git a/apps/red-ui/src/app/modules/shared/components/filters/quick-filters/quick-filters.component.ts b/apps/red-ui/src/app/modules/shared/components/filters/quick-filters/quick-filters.component.ts index 632f0350e..fa29fc3bc 100644 --- a/apps/red-ui/src/app/modules/shared/components/filters/quick-filters/quick-filters.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/filters/quick-filters/quick-filters.component.ts @@ -1,14 +1,14 @@ -import { Component, EventEmitter, Output } from '@angular/core'; -import { FilterModel } from '../popup-filter/model/filter.model'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { FilterService } from '@shared/services/filter.service'; @Component({ selector: 'redaction-quick-filters', templateUrl: './quick-filters.component.html', - styleUrls: ['./quick-filters.component.scss'] + styleUrls: ['./quick-filters.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) -export class QuickFiltersComponent { - @Output() filtersChanged = new EventEmitter(); +export class QuickFiltersComponent { + readonly quickFilters$ = this.filterService.getFilterModels$('quickFilters'); - constructor(readonly filterService: FilterService) {} + constructor(readonly filterService: FilterService) {} } diff --git a/apps/red-ui/src/app/modules/shared/components/page-header/page-header.component.html b/apps/red-ui/src/app/modules/shared/components/page-header/page-header.component.html index 009356fc0..d8eda0bc2 100644 --- a/apps/red-ui/src/app/modules/shared/components/page-header/page-header.component.html +++ b/apps/red-ui/src/app/modules/shared/components/page-header/page-header.component.html @@ -6,7 +6,7 @@ -
+
- - - +
+ + + -
{ @Input() buttonConfigs: ButtonConfig[]; @Input() searchPlaceholder: string; - constructor( - readonly permissionsService: PermissionsService, - readonly filterService: FilterService, - readonly searchService: SearchService - ) {} + readonly filters$ = this.filterService?.filterGroups$.pipe(map(all => all.filter(f => f.icon))); + readonly showResetFilters$ = this._showResetFilters$; - get filters$() { - return this.filterService.allFilters$.pipe(map(all => all.filter(f => f.icon))); + constructor(@Optional() readonly filterService: FilterService, @Optional() readonly searchService: SearchService) {} + + get _showResetFilters$(): Observable { + if (!this.filterService) return of(false); + + const filtersLength$ = this.filters$.pipe( + map(f => f.length), + distinctUntilChanged() + ); + return combineLatest([filtersLength$, this.filterService.showResetFilters$, this.searchService.valueChanges$]).pipe( + map(([hasFilters, showResetFilters, searchValue]) => hasFilters && (showResetFilters || !!searchValue)), + distinctUntilChanged() + ); } - resetFilters() { + resetFilters(): void { this.filterService.reset(); this.searchService.reset(); } - trackByLabel(index: number, item) { + trackByLabel(index: number, item: K): string { return item.label; } } diff --git a/apps/red-ui/src/app/modules/shared/components/simple-doughnut-chart/simple-doughnut-chart.component.html b/apps/red-ui/src/app/modules/shared/components/simple-doughnut-chart/simple-doughnut-chart.component.html index a35236909..af8c784fc 100644 --- a/apps/red-ui/src/app/modules/shared/components/simple-doughnut-chart/simple-doughnut-chart.component.html +++ b/apps/red-ui/src/app/modules/shared/components/simple-doughnut-chart/simple-doughnut-chart.component.html @@ -2,7 +2,7 @@ +
{{ label }} -
- - +
+ +
diff --git a/apps/red-ui/src/app/modules/shared/components/table-col-name/table-col-name.component.ts b/apps/red-ui/src/app/modules/shared/components/table-col-name/table-col-name.component.ts index 34328716e..f3dd88176 100644 --- a/apps/red-ui/src/app/modules/shared/components/table-col-name/table-col-name.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/table-col-name/table-col-name.component.ts @@ -1,5 +1,15 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { SortingOption } from '@services/sorting.service'; +import { Component, Input, Optional } from '@angular/core'; +import { SortingService } from '@services/sorting.service'; + +export interface TableColConfig { + readonly column?: string; + readonly label: string; + readonly withSort?: boolean; + readonly class?: string; + readonly leftIcon?: string; + readonly rightIcon?: string; + readonly rightIconTooltip?: string; +} @Component({ selector: 'redaction-table-col-name', @@ -7,7 +17,6 @@ import { SortingOption } from '@services/sorting.service'; styleUrls: ['./table-col-name.component.scss'] }) export class TableColNameComponent { - @Input() activeSortingOption: SortingOption; @Input() column: string; @Input() label: string; @Input() withSort = false; @@ -16,5 +25,5 @@ export class TableColNameComponent { @Input() rightIcon: string; @Input() rightIconTooltip: string; - @Output() toggleSort = new EventEmitter(); + constructor(@Optional() readonly sortingService: SortingService) {} } diff --git a/apps/red-ui/src/app/modules/shared/components/table-header/table-header.component.html b/apps/red-ui/src/app/modules/shared/components/table-header/table-header.component.html new file mode 100644 index 000000000..d21aacde1 --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/components/table-header/table-header.component.html @@ -0,0 +1,22 @@ +
+ + {{ tableHeaderLabel | translate: { length: (screenStateService.displayedLength$ | async) } }} + + + +
+ +
+ + +
+
diff --git a/apps/red-ui/src/app/modules/shared/components/table-header/table-header.component.scss b/apps/red-ui/src/app/modules/shared/components/table-header/table-header.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/apps/red-ui/src/app/modules/shared/components/table-header/table-header.component.ts b/apps/red-ui/src/app/modules/shared/components/table-header/table-header.component.ts new file mode 100644 index 000000000..e1b8b7b32 --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/components/table-header/table-header.component.ts @@ -0,0 +1,16 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { TableColConfig } from '@shared/components/table-col-name/table-col-name.component'; +import { ScreenStateService } from '@shared/services/screen-state.service'; + +@Component({ + selector: 'redaction-table-header', + templateUrl: './table-header.component.html', + styleUrls: ['./table-header.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TableHeaderComponent { + @Input() tableHeaderLabel: string; + @Input() tableColConfigs: TableColConfig[]; + + constructor(readonly screenStateService: ScreenStateService) {} +} diff --git a/apps/red-ui/src/app/modules/shared/directives/sync-width.directive.ts b/apps/red-ui/src/app/modules/shared/directives/sync-width.directive.ts index 2f6affe25..8a2398c4d 100644 --- a/apps/red-ui/src/app/modules/shared/directives/sync-width.directive.ts +++ b/apps/red-ui/src/app/modules/shared/directives/sync-width.directive.ts @@ -25,6 +25,7 @@ export class SyncWidthDirective implements AfterViewInit, OnDestroy { @debounce(10) matchWidth() { const headerItems = this._elementRef.nativeElement.children; + // const tableRows = document.getElementsByClassName(this.redactionSyncWidth); const tableRows = this._elementRef.nativeElement.parentElement.getElementsByClassName(this.redactionSyncWidth); if (!tableRows || !tableRows.length) { diff --git a/apps/red-ui/src/app/modules/shared/pipes/sort-by.pipe.ts b/apps/red-ui/src/app/modules/shared/pipes/sort-by.pipe.ts new file mode 100644 index 000000000..f9275b187 --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/pipes/sort-by.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { SortingService } from '../../../services/sorting.service'; + +@Pipe({ name: 'sortBy' }) +export class SortByPipe implements PipeTransform { + constructor(private readonly _sortingService: SortingService) {} + + transform(value: T[], order = '', column: string = ''): T[] { + return this._sortingService.sort(value, order, column); + } +} diff --git a/apps/red-ui/src/app/modules/shared/services/dictionary-save.service.ts b/apps/red-ui/src/app/modules/shared/services/dictionary-save.service.ts index 4f6886952..5bce425e3 100644 --- a/apps/red-ui/src/app/modules/shared/services/dictionary-save.service.ts +++ b/apps/red-ui/src/app/modules/shared/services/dictionary-save.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; import { Observable, throwError } from 'rxjs'; -import { NotificationService, NotificationType } from '@services/notification.service'; -import { TranslateService } from '@ngx-translate/core'; +import { Toaster } from '../../../services/toaster.service'; import { DictionaryControllerService } from '@redaction/red-ui-http'; import { tap } from 'rxjs/operators'; @@ -11,11 +10,7 @@ const MIN_WORD_LENGTH = 2; providedIn: 'root' }) export class DictionarySaveService { - constructor( - private readonly _notificationService: NotificationService, - private readonly _translateService: TranslateService, - private readonly _dictionaryControllerService: DictionaryControllerService - ) {} + constructor(private readonly _toaster: Toaster, private readonly _dictionaryControllerService: DictionaryControllerService) {} saveEntries( entries: string[], @@ -44,29 +39,13 @@ export class DictionarySaveService { return obs.pipe( tap( () => { - if (showToast) { - this._notificationService.showToastNotification( - this._translateService.instant('dictionary-overview.success.generic'), - null, - NotificationType.SUCCESS - ); - } + if (showToast) this._toaster.success('dictionary-overview.success.generic'); }, - () => { - this._notificationService.showToastNotification( - this._translateService.instant('dictionary-overview.error.generic'), - null, - NotificationType.ERROR - ); - } + () => this._toaster.error('dictionary-overview.error.generic') ) ); } else { - this._notificationService.showToastNotification( - this._translateService.instant('dictionary-overview.error.entries-too-short'), - null, - NotificationType.ERROR - ); + this._toaster.error('dictionary-overview.error.entries-too-short'); return throwError('Entries too short'); } diff --git a/apps/red-ui/src/app/modules/shared/services/filter.service.ts b/apps/red-ui/src/app/modules/shared/services/filter.service.ts index 50d87d3ca..692298ae6 100644 --- a/apps/red-ui/src/app/modules/shared/services/filter.service.ts +++ b/apps/red-ui/src/app/modules/shared/services/filter.service.ts @@ -1,86 +1,59 @@ -import { ChangeDetectorRef, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model'; import { getFilteredEntities, processFilters } from '@shared/components/filters/popup-filter/utils/filter-utils'; -import { FilterWrapper } from '@shared/components/filters/popup-filter/model/filter-wrapper.model'; import { ScreenStateService } from '@shared/services/screen-state.service'; import { SearchService } from '@shared/services/search.service'; +import { FilterGroup } from '@shared/components/filters/popup-filter/model/filter-wrapper.model'; import { BehaviorSubject, Observable } from 'rxjs'; -import { distinctUntilChanged, filter, map } from 'rxjs/operators'; +import { distinctUntilChanged, map, switchMap } from 'rxjs/operators'; @Injectable() -export class FilterService { - _allFilters$ = new BehaviorSubject([]); +export class FilterService { + private readonly _filterGroups$ = new BehaviorSubject([]); + private readonly _refresh$ = new BehaviorSubject(null); - constructor( - private readonly _screenStateService: ScreenStateService, - private readonly _searchService: SearchService, - private readonly _changeDetector: ChangeDetectorRef - ) {} + readonly filterGroups$ = this._refresh$.pipe(switchMap(() => this._filterGroups$.asObservable())); + readonly showResetFilters$ = this._showResetFilters$; - get filters() { - return Object.values(this._allFilters$.getValue()); + get filterGroups(): FilterGroup[] { + return Object.values(this._filterGroups$.getValue()); } - get showResetFilters$() { - return this.allFilters$.pipe( - map(all => this._toFlatFilters(all)), - filter(f => !!f.find(el => el.checked)), - distinctUntilChanged() - ); - } - - filterChecked$(slug: string, key: string) { - const filters = this.getFilter$(slug); - return filters.pipe(map(all => all.find(f => f.key === key)?.checked)); + refresh(): void { + this._refresh$.next(null); } toggleFilter(slug: string, key: string) { - const filters = this.filters.find(f => f.slug === slug); - let found = filters.values.find(f => f.key === key); - if (!found) found = filters.values.map(f => f.filters?.find(ff => ff.key === key))[0]; + const filters = this.filterGroups.find(f => f.slug === slug).values; + let found = filters.find(f => f.key === key); + if (!found) found = filters.map(f => f.filters?.find(ff => ff.key === key))[0]; found.checked = !found.checked; - this._allFilters$.next(this.filters); - this.filterEntities(); + this.refresh(); } - filterEntities(): void { - const filtered = getFilteredEntities(this._screenStateService.entities, this.filters); - this._screenStateService.setFilteredEntities(filtered); - this._searchService.executeSearchImmediately(); - - this._changeDetector.detectChanges(); - } - - addFilter(value: FilterWrapper): void { - const oldFilters = this.getFilter(value.slug)?.values; - if (!oldFilters) return this._allFilters$.next([...this.filters, value]); + addFilterGroup(value: FilterGroup): void { + const oldFilters = this.getFilterGroup(value.slug)?.values; + if (!oldFilters) return this._filterGroups$.next([...this.filterGroups, value]); value.values = processFilters(oldFilters, value.values); - this._allFilters$.next([...this.filters.filter(f => f.slug !== value.slug), value]); + this._filterGroups$.next([...this.filterGroups.filter(f => f.slug !== value.slug), value]); } - getFilter(slug: string): FilterWrapper { - return this.filters.find(f => f?.slug === slug); + getFilterGroup(slug: string): FilterGroup { + return this.filterGroups.find(f => f.slug === slug); } - getFilter$(slug: string): Observable { - return this.getFilterWrapper$(slug).pipe( - filter(f => f !== null && f !== undefined), - map(f => f?.values) - ); + getFilterModels$(filterGroupSlug: string): Observable { + return this.getFilterGroup$(filterGroupSlug).pipe(map(f => f?.values)); } - getFilterWrapper$(slug: string): Observable { - return this.allFilters$.pipe(map(all => all.find(f => f?.slug === slug))); - } - - get allFilters$(): Observable { - return this._allFilters$.asObservable(); + getFilterGroup$(slug: string): Observable { + return this.filterGroups$.pipe(map(all => all.find(f => f.slug === slug))); } reset(): void { - this.filters.forEach(item => { + this.filterGroups.forEach(item => { item.values.forEach(child => { child.checked = false; child.indeterminate = false; @@ -90,11 +63,19 @@ export class FilterService { }); }); }); - this._allFilters$.next(this.filters); - this.filterEntities(); + + this.refresh(); } - private _toFlatFilters(entities: FilterWrapper[]): FilterModel[] { + private get _showResetFilters$(): Observable { + return this.filterGroups$.pipe( + map(all => this._toFlatFilters(all)), + map(f => !!f.find(el => el.checked)), + distinctUntilChanged() + ); + } + + private _toFlatFilters(entities: FilterGroup[]): FilterModel[] { const flatChildren = (filters: FilterModel[]) => (filters ?? []).reduce((acc, f) => [...acc, ...(f?.filters ?? [])], []); return entities.reduce((acc, f) => [...acc, ...f.values, ...flatChildren(f.values)], []); diff --git a/apps/red-ui/src/app/modules/shared/services/screen-state.service.ts b/apps/red-ui/src/app/modules/shared/services/screen-state.service.ts index d52457f7a..9ad198c66 100644 --- a/apps/red-ui/src/app/modules/shared/services/screen-state.service.ts +++ b/apps/red-ui/src/app/modules/shared/services/screen-state.service.ts @@ -1,107 +1,152 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { distinctUntilChanged, map } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; +import { distinctUntilChanged, map, tap } from 'rxjs/operators'; +import { FilterService } from '@shared/services/filter.service'; +import { SearchService } from '@shared/services/search.service'; +import { getFilteredEntities } from '@shared/components/filters/popup-filter/utils/filter-utils'; + +const toLengthValue = entities => entities?.length ?? 0; @Injectable() export class ScreenStateService { - entities$ = new BehaviorSubject([]); - filteredEntities$ = new BehaviorSubject([]); - displayedEntities$ = new BehaviorSubject([]); - selectedEntitiesIds$ = new BehaviorSubject([]); + private readonly _allEntities$ = new BehaviorSubject([]); + readonly allEntities$ = this._allEntities$.asObservable(); + readonly allEntitiesLength$ = this._allEntitiesLength$; - private _idKey: string; + private readonly _displayedEntities$ = new BehaviorSubject([]); + readonly displayedEntities$ = this._getDisplayedEntities$; + readonly displayedLength$ = this._displayedLength$; - get entities(): T[] { - return Object.values(this.entities$.getValue()); + private readonly _selectedEntities$ = new BehaviorSubject([]); + readonly selectedEntities$ = this._selectedEntities$.asObservable(); + readonly selectedLength$ = this._selectedLength$; + + readonly noData$ = this._noData$; + readonly areAllEntitiesSelected$ = this._areAllEntitiesSelected$; + readonly areSomeEntitiesSelected$ = this._areSomeEntitiesSelected$; + readonly notAllEntitiesSelected$ = this._notAllEntitiesSelected$; + + constructor(private readonly _filterService: FilterService, private readonly _searchService: SearchService) { + // setInterval(() => { + // console.log('All entities subs: ', this._allEntities$.observers); + // console.log('Displayed entities subs: ', this._displayedEntities$.observers); + // console.log('Selected entities subs: ', this._selectedEntities$.observers); + // }, 10000); } - get filteredEntities(): T[] { - return Object.values(this.filteredEntities$.getValue()); + get allEntities(): T[] { + return Object.values(this._allEntities$.getValue()); } - get selectedEntitiesIds(): string[] { - return Object.values(this.selectedEntitiesIds$.getValue()); + get selectedEntities(): T[] { + return Object.values(this._selectedEntities$.getValue()); } get displayedEntities(): T[] { - return Object.values(this.displayedEntities$.getValue()); + return Object.values(this._displayedEntities$.getValue()); } - map(func: (state: T[]) => K): Observable { - return this.entities$.asObservable().pipe( - map((state: T[]) => func(state)), + setEntities(newEntities: Partial): void { + this._allEntities$.next(newEntities); + } + + setSelectedEntities(newEntities: Partial): void { + this._selectedEntities$.next(newEntities); + } + + isSelected(entity: T): boolean { + return this.selectedEntities.indexOf(entity) !== -1; + } + + selectEntities(entities?: T[]): void { + if (entities !== undefined && entities !== null && entities.length > 0) { + return entities.forEach(entity => this._selectOne(entity)); + } + return this._selectAll(); + } + + updateSelection(): void { + const items = this.displayedEntities.filter(item => this.selectedEntities.includes(item)); + this.setSelectedEntities(items); + } + + logCurrentState(): void { + console.log('Entities', this.allEntities); + console.log('Displayed', this.displayedEntities); + console.log('Selected', this.selectedEntities); + } + + get _getDisplayedEntities$(): Observable { + const filterGroups$ = this._filterService.filterGroups$; + const searchValue$ = this._searchService.valueChanges$; + + return combineLatest([this.allEntities$, filterGroups$, searchValue$]).pipe( + map(([entities, filterGroups]) => getFilteredEntities(entities, filterGroups)), + map(entities => this._searchService.searchIn(entities)), + tap(entities => this._displayedEntities$.next(entities)) + ); + } + + private get _allEntitiesLength$(): Observable { + return this.allEntities$.pipe(map(toLengthValue), distinctUntilChanged()); + } + + private get _displayedLength$(): Observable { + return this.displayedEntities$.pipe(map(toLengthValue), distinctUntilChanged()); + } + + private get _selectedLength$(): Observable { + return this.selectedEntities$.pipe(map(toLengthValue), distinctUntilChanged()); + } + + private get _areAllEntitiesSelected$(): Observable { + return combineLatest([this.displayedLength$, this.selectedLength$]).pipe( + map(([displayedLength, selectedLength]) => displayedLength && displayedLength === selectedLength), distinctUntilChanged() ); } - setEntities(newEntities: Partial): void { - this.entities$.next(newEntities); + /** + * Indicates that some entities are selected. If all are selected this returns true + */ + private get _areSomeEntitiesSelected$(): Observable { + return this.selectedLength$.pipe( + map(value => !!value), + distinctUntilChanged() + ); } - setFilteredEntities(newEntities: Partial): void { - this.filteredEntities$.next(newEntities); + /** + * Indicates that some entities are selected, but not all + */ + private get _notAllEntitiesSelected$(): Observable { + return combineLatest([this.areAllEntitiesSelected$, this.areSomeEntitiesSelected$]).pipe( + map(([allEntitiesAreSelected, someEntitiesAreSelected]) => !allEntitiesAreSelected && someEntitiesAreSelected), + distinctUntilChanged() + ); } - setSelectedEntitiesIds(newEntities: Partial): void { - this.selectedEntitiesIds$.next(newEntities); + private get _noData$(): Observable { + return this.allEntitiesLength$.pipe( + map(length => length === 0), + distinctUntilChanged() + ); } - setDisplayedEntities(newEntities: Partial): void { - this.displayedEntities$.next(newEntities); - } - - setIdKey(value: string): void { - this._idKey = value; - } - - get areAllEntitiesSelected(): boolean { - return this.displayedEntities.length !== 0 && this.selectedEntitiesIds.length === this.displayedEntities.length; - } - - get areSomeEntitiesSelected$(): Observable { - return this.selectedEntitiesIds$.pipe(map(all => all.length > 0)); - } - - isSelected(entity: T): boolean { - return this.selectedEntitiesIds.indexOf(entity[this._getIdKey]) !== -1; - } - - toggleEntitySelected(entity: T): void { - const currentEntityIdx = this.selectedEntitiesIds.indexOf(entity[this._getIdKey]); + private _selectOne(entity: T): void { + const currentEntityIdx = this.selectedEntities.indexOf(entity); if (currentEntityIdx === -1) { - const currentEntityId = entity[this._getIdKey]; - return this.setSelectedEntitiesIds([...this.selectedEntitiesIds, currentEntityId]); + return this.setSelectedEntities([...this.selectedEntities, entity]); } - - this.setSelectedEntitiesIds(this.selectedEntitiesIds.filter((el, idx) => idx !== currentEntityIdx)); + this.setSelectedEntities(this.selectedEntities.filter((el, idx) => idx !== currentEntityIdx)); } - toggleSelectAll(): void { - if (this.areAllEntitiesSelected) return this.setSelectedEntitiesIds([]); - this.setSelectedEntitiesIds(this._displayedEntitiesIds); + private _selectAll(): void { + if (this._allEntitiesSelected) return this.setSelectedEntities([]); + this.setSelectedEntities(this.displayedEntities); } - updateSelection(): void { - if (!this._idKey) return; - - const ids = this._displayedEntitiesIds.filter(id => this.selectedEntitiesIds.includes(id)); - this.setSelectedEntitiesIds(ids); - } - - logCurrentState(): void { - console.log('Entities', this.entities); - console.log('Displayed', this.displayedEntities); - console.log('Filtered', this.filteredEntities); - console.log('Selected', this.selectedEntitiesIds); - } - - private get _displayedEntitiesIds(): string[] { - return this.displayedEntities.map(entity => entity[this._getIdKey]); - } - - private get _getIdKey(): string { - if (!this._idKey) throw new Error('Not implemented'); - - return this._idKey; + private get _allEntitiesSelected() { + return this.displayedEntities.length !== 0 && this.displayedEntities.length === this.selectedEntities.length; } } diff --git a/apps/red-ui/src/app/modules/shared/services/search.service.ts b/apps/red-ui/src/app/modules/shared/services/search.service.ts index 3098c3be3..93a4453e9 100644 --- a/apps/red-ui/src/app/modules/shared/services/search.service.ts +++ b/apps/red-ui/src/app/modules/shared/services/search.service.ts @@ -1,48 +1,30 @@ import { Injectable } from '@angular/core'; -import { debounce } from '@utils/debounce'; -import { ScreenStateService } from '@shared/services/screen-state.service'; import { FormBuilder } from '@angular/forms'; +import { startWith } from 'rxjs/operators'; @Injectable() export class SearchService { - private _searchValue = ''; private _searchKey: string; readonly searchForm = this._formBuilder.group({ query: [''] }); - constructor(private readonly _screenStateService: ScreenStateService, private readonly _formBuilder: FormBuilder) { - this.searchForm.valueChanges.subscribe(() => this.executeSearch()); - } + readonly valueChanges$ = this.searchForm.get('query').valueChanges.pipe(startWith('')); - @debounce(200) - executeSearch(): void { - this._searchValue = this.searchValue.toLowerCase(); - this.executeSearchImmediately(); - } + constructor(private readonly _formBuilder: FormBuilder) {} - executeSearchImmediately(): void { - const displayed = this._screenStateService.filteredEntities || this._screenStateService.entities; + searchIn(entities: T[]) { + if (!this._searchKey) return entities; - if (!this._searchKey) { - return this._screenStateService.setDisplayedEntities(displayed); - } - - this._screenStateService.setDisplayedEntities( - displayed.filter(entity => this._searchField(entity).toLowerCase().includes(this._searchValue)) - ); - this._screenStateService.updateSelection(); + const searchValue = this.searchValue.toLowerCase(); + return entities.filter(entity => this._searchField(entity).includes(searchValue)); } setSearchKey(value: string): void { this._searchKey = value; } - get isSearchNeeded(): boolean { - return !!this._searchKey; - } - get searchValue(): string { return this.searchForm.get('query').value; } @@ -51,7 +33,7 @@ export class SearchService { this.searchForm.reset({ query: '' }); } - protected _searchField(entity: T): string { - return entity[this._searchKey]; + private _searchField(entity: T): string { + return entity[this._searchKey].toLowerCase(); } } diff --git a/apps/red-ui/src/app/modules/shared/shared.module.ts b/apps/red-ui/src/app/modules/shared/shared.module.ts index d9f61970b..87a37d79e 100644 --- a/apps/red-ui/src/app/modules/shared/shared.module.ts +++ b/apps/red-ui/src/app/modules/shared/shared.module.ts @@ -24,7 +24,7 @@ import { DictionaryAnnotationIconComponent } from './components/dictionary-annot import { HiddenActionComponent } from './components/hidden-action/hidden-action.component'; import { ConfirmationDialogComponent } from './dialogs/confirmation-dialog/confirmation-dialog.component'; import { EmptyStateComponent } from './components/empty-state/empty-state.component'; -import { SortByPipe } from './components/sort-pipe/sort-by.pipe'; +import { SortByPipe } from './pipes/sort-by.pipe'; import { RoundCheckboxComponent } from './components/checkbox/round-checkbox.component'; import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core'; import { MomentDateAdapter } from '@angular/material-moment-adapter'; @@ -39,6 +39,7 @@ import { AssignUserDropdownComponent } from './components/assign-user-dropdown/a import { InputWithActionComponent } from '@shared/components/input-with-action/input-with-action.component'; import { PageHeaderComponent } from './components/page-header/page-header.component'; import { DatePipe } from '@shared/pipes/date.pipe'; +import { TableHeaderComponent } from './components/table-header/table-header.component'; const buttons = [ChevronButtonComponent, CircleButtonComponent, FileDownloadBtnComponent, IconButtonComponent, UserButtonComponent]; @@ -73,9 +74,9 @@ const utils = [HumanizePipe, DatePipe, SyncWidthDirective, HasScrollbarDirective const modules = [MatConfigModule, TranslateModule, ScrollingModule, IconsModule, FormsModule, ReactiveFormsModule]; @NgModule({ - declarations: [...components, ...utils], + declarations: [...components, ...utils, TableHeaderComponent], imports: [CommonModule, ...modules, MonacoEditorModule], - exports: [...modules, ...components, ...utils], + exports: [...modules, ...components, ...utils, TableHeaderComponent], providers: [ { provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] }, { diff --git a/apps/red-ui/src/app/services/error-message.service.ts b/apps/red-ui/src/app/services/error-message.service.ts index d8d311fb1..68fd90874 100644 --- a/apps/red-ui/src/app/services/error-message.service.ts +++ b/apps/red-ui/src/app/services/error-message.service.ts @@ -8,11 +8,11 @@ import { HttpErrorResponse } from '@angular/common/http'; export class ErrorMessageService { constructor(private readonly _translateService: TranslateService) {} - _parseErrorResponse(err: HttpErrorResponse) { + _parseErrorResponse(err: HttpErrorResponse): string { return err?.error?.message?.includes('message') ? ` ${err.error.message.match('"message":"(.*?)\\"')[1]}` : ''; } - getMessage(err: HttpErrorResponse, defaultMessage: string) { - return this._translateService.instant(defaultMessage) + this._parseErrorResponse(err); + getMessage(error: HttpErrorResponse, defaultMessage: string): string { + return this._translateService.instant(defaultMessage) + this._parseErrorResponse(error); } } diff --git a/apps/red-ui/src/app/services/loading.service.ts b/apps/red-ui/src/app/services/loading.service.ts index 15cc93b61..66f938fe1 100644 --- a/apps/red-ui/src/app/services/loading.service.ts +++ b/apps/red-ui/src/app/services/loading.service.ts @@ -1,5 +1,5 @@ -import { EventEmitter, Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; const MIN_LOADING_TIME = 300; @@ -7,16 +7,15 @@ const MIN_LOADING_TIME = 300; providedIn: 'root' }) export class LoadingService { - private readonly _loadingEvent = new EventEmitter(); + private readonly _loadingEvent = new BehaviorSubject(false); private _loadingStarted: number; get isLoading(): Observable { - return this._loadingEvent; + return this._loadingEvent.asObservable(); } start(): void { - // setTimeout is used so that value doesn't change after it was checked for changes - setTimeout(() => this._loadingEvent.next(true)); + this._loadingEvent.next(true); this._loadingStarted = new Date().getTime(); } @@ -37,7 +36,7 @@ export class LoadingService { } private _stop() { - this._loadingEvent.next(false); + setTimeout(() => this._loadingEvent.next(false)); } private _stopAfter(timeout: number) { diff --git a/apps/red-ui/src/app/services/notification.service.ts b/apps/red-ui/src/app/services/notification.service.ts deleted file mode 100644 index 9431503f7..000000000 --- a/apps/red-ui/src/app/services/notification.service.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Injectable } from '@angular/core'; -import { ActiveToast, ToastrService } from 'ngx-toastr'; -import { IndividualConfig } from 'ngx-toastr/toastr/toastr-config'; -import { NavigationStart, Router } from '@angular/router'; - -export enum NotificationType { - SUCCESS = 'SUCCESS', - WARNING = 'WARNING', - ERROR = 'ERROR', - INFO = 'INFO' -} - -export class ToastAction { - title: string; - action?: () => any; -} - -@Injectable({ - providedIn: 'root' -}) -export class NotificationService { - constructor(private readonly _toastr: ToastrService, private readonly _router: Router) { - _router.events.subscribe(event => { - if (event instanceof NavigationStart) { - this._toastr.clear(); - } - }); - } - - showToastNotification( - message: string, - title?: string, - notificationType = NotificationType.INFO, - options?: Partial & { actions?: ToastAction[] } - ): ActiveToast { - switch (notificationType) { - case NotificationType.ERROR: - return this._toastr.error(message, title, options); - case NotificationType.SUCCESS: - return this._toastr.success(message, title, options); - case NotificationType.WARNING: - return this._toastr.warning(message, title, options); - case NotificationType.INFO: - return this._toastr.info(message, title, options); - } - } -} diff --git a/apps/red-ui/src/app/services/sorting.service.ts b/apps/red-ui/src/app/services/sorting.service.ts index b2a07b5db..c6f620aeb 100644 --- a/apps/red-ui/src/app/services/sorting.service.ts +++ b/apps/red-ui/src/app/services/sorting.service.ts @@ -1,8 +1,9 @@ import { Injectable } from '@angular/core'; +import { orderBy } from 'lodash'; export type SortingOrder = 'asc' | 'desc'; -export enum SortingOrders { +export const enum SortingOrders { ASC = 'asc', DESC = 'desc' } @@ -12,59 +13,52 @@ export interface SortingOption { column: string; } -export type ScreenName = - | 'dossier-listing' - | 'dossier-overview' - | 'dictionary-listing' - | 'dossier-templates-listing' - | 'default-colors' - | 'file-attributes-listing' - | 'dossier-attributes-listing'; - -export enum ScreenNames { - DOSSIER_LISTING = 'dossier-listing', - DOSSIER_OVERVIEW = 'dossier-overview', - DICTIONARY_LISTING = 'dictionary-listing', - DOSSIER_TEMPLATES_LISTING = 'dossier-templates-listing', - DEFAULT_COLORS = 'default-colors', - FILE_ATTRIBUTES_LISTING = 'file-attributes-listing', - DOSSIER_ATTRIBUTES_LISTING = 'dossier-attributes-listing' -} - @Injectable() export class SortingService { - private _currentScreenName: string; - private readonly _options: { [key in ScreenName]: SortingOption } = { - [ScreenNames.DOSSIER_LISTING]: { column: 'dossier.dossierName', order: SortingOrders.ASC }, - [ScreenNames.DOSSIER_OVERVIEW]: { column: 'filename', order: SortingOrders.ASC }, - [ScreenNames.DICTIONARY_LISTING]: { column: 'label', order: SortingOrders.ASC }, - [ScreenNames.DOSSIER_TEMPLATES_LISTING]: { column: 'name', order: SortingOrders.ASC }, - [ScreenNames.DEFAULT_COLORS]: { column: 'key', order: SortingOrders.ASC }, - [ScreenNames.FILE_ATTRIBUTES_LISTING]: { column: 'label', order: SortingOrders.ASC }, - [ScreenNames.DOSSIER_ATTRIBUTES_LISTING]: { column: 'label', order: 'asc' } - }; + private _sortingOption: SortingOption; - setScreenName(value: string) { - this._currentScreenName = value; + setSortingOption(value: SortingOption): void { + this._sortingOption = value; + } + + sort(values: T[], order = '', column: string = ''): T[] { + if (!values || order === '' || !order) { + return values; + } // no array + if (!column || column === '') { + if (order === SortingOrders.ASC) { + return values.sort(); + } else { + return values.sort().reverse(); + } + } // sort 1d array + if (values.length <= 1) { + return values; + } // array with only one item + return orderBy(values, [column], [order]); + } + + defaultSort(values: T[]) { + return this.sort(values, this.sortingOption?.order, this.sortingOption?.column); } toggleSort(column: string) { - if (this._options[this._currentScreenName].column === column) { + if (this._sortingOption.column === column) { this._currentOrder = this._currentOrder === SortingOrders.ASC ? SortingOrders.DESC : SortingOrders.ASC; } else { - this._options[this._currentScreenName] = { column, order: SortingOrders.ASC }; + this._sortingOption = { column, order: SortingOrders.ASC }; } } - getSortingOption() { - return this._options[this._currentScreenName]; + get sortingOption(): SortingOption { + return this._sortingOption; } - private get _currentOrder(): string { - return this._options[this._currentScreenName].order; + private get _currentOrder(): SortingOrder { + return this._sortingOption.order; } - private set _currentOrder(value: string) { - this._options[this._currentScreenName].order = value; + private set _currentOrder(value: SortingOrder) { + this._sortingOption.order = value; } } diff --git a/apps/red-ui/src/app/services/toaster.service.ts b/apps/red-ui/src/app/services/toaster.service.ts new file mode 100644 index 000000000..f389df656 --- /dev/null +++ b/apps/red-ui/src/app/services/toaster.service.ts @@ -0,0 +1,82 @@ +import { Injectable } from '@angular/core'; +import { ActiveToast, ToastrService } from 'ngx-toastr'; +import { IndividualConfig } from 'ngx-toastr/toastr/toastr-config'; +import { NavigationStart, Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { ErrorMessageService } from '@services/error-message.service'; +import { filter } from 'rxjs/operators'; + +const enum NotificationType { + SUCCESS = 'SUCCESS', + WARNING = 'WARNING', + INFO = 'INFO' +} + +export interface ToasterOptions extends IndividualConfig { + title?: string; + /** + * These params are used as interpolateParams for translate service + */ + params?: object; + actions?: { title?: string; action: () => void }[]; +} + +export interface ErrorToasterOptions extends ToasterOptions { + /** + * Pass an http error that will be processed by error message service and shown in toast + */ + error?: HttpErrorResponse; +} + +@Injectable({ + providedIn: 'root' +}) +export class Toaster { + constructor( + private readonly _toastr: ToastrService, + private readonly _router: Router, + private readonly _translateService: TranslateService, + private readonly _errorMessageService: ErrorMessageService + ) { + _router.events.pipe(filter(event => event instanceof NavigationStart)).subscribe(() => { + _toastr.clear(); + }); + } + + error(message: string, options?: Partial) { + if (options?.error) message = this._errorMessageService.getMessage(options.error, message); + else message = this._translateService.instant(message, options?.params); + + return this._toastr.error(message, options?.title, options); + } + + info(message: string, options?: Partial) { + return this._showToastNotification(message, NotificationType.INFO, options); + } + + success(message: string, options?: Partial) { + return this._showToastNotification(message, NotificationType.SUCCESS, options); + } + + warning(message: string, options?: Partial) { + return this._showToastNotification(message, NotificationType.WARNING, options); + } + + private _showToastNotification( + message: string, + notificationType = NotificationType.INFO, + options?: Partial + ): ActiveToast { + message = this._translateService.instant(message, options?.params); + + switch (notificationType) { + case NotificationType.SUCCESS: + return this._toastr.success(message, options?.title, options); + case NotificationType.WARNING: + return this._toastr.warning(message, options?.title, options); + case NotificationType.INFO: + return this._toastr.info(message, options?.title, options); + } + } +} diff --git a/apps/red-ui/src/app/services/user-preference.service.ts b/apps/red-ui/src/app/services/user-preference.service.ts index b77e13d58..e2999f0ce 100644 --- a/apps/red-ui/src/app/services/user-preference.service.ts +++ b/apps/red-ui/src/app/services/user-preference.service.ts @@ -1,15 +1,36 @@ import { Injectable } from '@angular/core'; +import { UserPreferenceControllerService } from '@redaction/red-ui-http'; + +interface UserAttributes { + [p: string]: string[]; +} @Injectable({ providedIn: 'root' }) export class UserPreferenceService { + private _userAttributes: UserAttributes = {}; + + constructor(private readonly _userPreferenceControllerService: UserPreferenceControllerService) { + _userPreferenceControllerService.getAllUserAttributes().subscribe(attributes => { + this._userAttributes = attributes ?? {}; + }); + } + + get userAttributes(): UserAttributes { + return this._userAttributes; + } + get areDevFeaturesEnabled() { const value = sessionStorage.getItem('redaction.enable-dev-features'); - if (value) { - return value === 'true'; + return value ? value === 'true' : false; + } + + getLastOpenedFileId(key: string): string { + if (this.userAttributes[key]?.length > 0) { + return this.userAttributes[key][0]; } - return false; + return ''; } toggleDevFeatures() { diff --git a/apps/red-ui/src/app/services/user.service.ts b/apps/red-ui/src/app/services/user.service.ts index efddbf487..306dcac07 100644 --- a/apps/red-ui/src/app/services/user.service.ts +++ b/apps/red-ui/src/app/services/user.service.ts @@ -16,7 +16,7 @@ export interface ProfileModel { export class UserWrapper { name: string; - constructor(private _currentUser: KeycloakProfile, public roles: string[], public id: string) { + constructor(private readonly _currentUser: KeycloakProfile, public roles: string[], public id: string) { this.name = this.firstName && this.lastName ? `${this.firstName} ${this.lastName}` : this.username; } @@ -150,8 +150,8 @@ export class UserService { return this.getName(this.getUserById(userId)); } - getName(user?: User) { - return user?.firstName && user?.lastName ? `${user.firstName} ${user.lastName}` : user?.username; + getName({ firstName, lastName, username }: User = {}) { + return firstName && lastName ? `${firstName} ${lastName}` : username; } isManager(user: User | UserWrapper = this.user): boolean { diff --git a/apps/red-ui/src/app/state/app-state.service.ts b/apps/red-ui/src/app/state/app-state.service.ts index f8117c4ea..ce9146e7d 100644 --- a/apps/red-ui/src/app/state/app-state.service.ts +++ b/apps/red-ui/src/app/state/app-state.service.ts @@ -2,7 +2,6 @@ import { EventEmitter, Injectable } from '@angular/core'; import { DictionaryControllerService, Dossier, - DossierControllerService, DossierTemplateControllerService, FileAttributesConfig, FileAttributesControllerService, @@ -10,7 +9,7 @@ import { ReanalysisControllerService, StatusControllerService } from '@redaction/red-ui-http'; -import { NotificationService, NotificationType } from '@services/notification.service'; +import { Toaster } from '../services/toaster.service'; import { TranslateService } from '@ngx-translate/core'; import { Event, NavigationEnd, ResolveStart, Router } from '@angular/router'; import { UserService } from '@services/user.service'; @@ -21,6 +20,7 @@ import { FileStatusWrapper } from '@models/file/file-status.wrapper'; import { DossierWrapper } from './model/dossier.wrapper'; import { TypeValueWrapper } from '@models/file/type-value.wrapper'; import { DossierTemplateModelWrapper } from '@models/file/dossier-template-model.wrapper'; +import { DossiersService } from '../modules/dossier/services/dossiers.service'; export interface AppState { dossiers: DossierWrapper[]; @@ -46,8 +46,8 @@ export class AppStateService { constructor( private readonly _router: Router, private readonly _userService: UserService, - private readonly _dossierControllerService: DossierControllerService, - private readonly _notificationService: NotificationService, + private readonly _dossiersService: DossiersService, + private readonly _toaster: Toaster, private readonly _reanalysisControllerService: ReanalysisControllerService, private readonly _translateService: TranslateService, private readonly _dictionaryControllerService: DictionaryControllerService, @@ -65,15 +65,15 @@ export class AppStateService { activeDictionaryType: null }; - this._router.events.subscribe((event: Event) => { + _router.events.subscribe((event: Event) => { if (AppStateService._isFileOverviewRoute(event)) { const url = (event as ResolveStart).url.replace('/main/dossiers/', ''); const [dossierId, , fileId] = url.split(/[/?]/); - this.activateFile(dossierId, fileId); + return this.activateFile(dossierId, fileId); } if (AppStateService._isDossierOverviewRoute(event)) { const dossierId = (event as ResolveStart).url.replace('/main/dossiers/', ''); - this.activateDossier(dossierId); + return this.activateDossier(dossierId); } if (AppStateService._isRandomRoute(event)) { this._appState.activeDossierId = null; @@ -88,11 +88,7 @@ export class AppStateService { } get aggregatedFiles(): FileStatusWrapper[] { - const result: FileStatusWrapper[] = []; - this._appState.dossiers.forEach(p => { - result.push(...p.files); - }); - return result; + return this.allDossiers.reduce((acc, { files }) => [...acc, ...files], []); } get activeDossierTemplateId(): string { @@ -128,7 +124,7 @@ export class AppStateService { } get activeDossier(): DossierWrapper | undefined { - return this._appState.dossiers.find(p => p.dossierId === this.activeDossierId); + return this.allDossiers.find(p => p.dossierId === this.activeDossierId); } get allDossiers(): DossierWrapper[] { @@ -147,18 +143,14 @@ export class AppStateService { return this._appState.activeFileId; } - get totalAnalysedPages() { + get totalAnalysedPages(): number { return this._appState.totalAnalysedPages; } - get totalPeople() { + get totalPeople(): number { return this._appState.totalPeople; } - get totalDocuments() { - return this._appState.totalDocuments; - } - private static _isFileOverviewRoute(event: Event) { return event instanceof ResolveStart && event.url.includes('/main/dossiers/') && event.url.includes('/file/'); } @@ -177,18 +169,15 @@ export class AppStateService { } } - getDictionaryColor(type?: string, dossierTemplateId?: string) { - if (!dossierTemplateId && this.activeDossier) { - dossierTemplateId = this.activeDossier.dossierTemplateId; - } + getDictionaryColor(type?: string, dossierTemplateId = this.activeDossier?.dossierTemplateId) { if (!dossierTemplateId) { - dossierTemplateId = this.dossierTemplates.length > 0 ? this.dossierTemplates[0].dossierTemplateId : undefined; + dossierTemplateId = this.dossierTemplates[0]?.dossierTemplateId; } if (!dossierTemplateId) { return undefined; } const color = this._dictionaryData[dossierTemplateId][type]?.hexColor; - return color ? color : this._dictionaryData[dossierTemplateId]['default'].hexColor; + return color ?? this._dictionaryData[dossierTemplateId]['default'].hexColor; } getDossierTemplateById(id: string): DossierTemplateModelWrapper { @@ -220,20 +209,21 @@ export class AppStateService { } async loadAllDossiers(emitEvents: boolean = true) { - const dossiers = await this._dossierControllerService.getDossiers().toPromise(); - if (dossiers) { - const mappedDossiers = dossiers.map(p => new DossierWrapper(p, this._getExistingFiles(p.dossierId))); - - const fileData = await this._statusControllerService.getFileStatusForDossiers(mappedDossiers.map(p => p.dossierId)).toPromise(); - - for (const dossierId of Object.keys(fileData)) { - const dossier = mappedDossiers.find(p => p.dossierId === dossierId); - this._processFiles(dossier, fileData[dossierId], emitEvents); - } - - this._appState.dossiers = mappedDossiers; - this._computeStats(); + const dossiers = await this._dossiersService.getAll(); + if (!dossiers) { + return; } + + const mappedDossiers = dossiers.map(p => new DossierWrapper(p, this._getExistingFiles(p.dossierId))); + const fileData = await this._statusControllerService.getFileStatusForDossiers(mappedDossiers.map(p => p.dossierId)).toPromise(); + + for (const dossierId of Object.keys(fileData)) { + const dossier = mappedDossiers.find(p => p.dossierId === dossierId); + this._processFiles(dossier, fileData[dossierId], emitEvents); + } + + this._appState.dossiers = mappedDossiers; + this._computeStats(); } async reloadActiveFile() { @@ -246,10 +236,10 @@ export class AppStateService { const activeFileWrapper = new FileStatusWrapper( activeFile, this._userService.getNameForId(activeFile.currentReviewer), - this.activeDossier.dossierTemplateId, - this._appState.fileAttributesConfig[this.activeDossier.dossierTemplateId] + this.activeDossierTemplateId, + this._appState.fileAttributesConfig[this.activeDossierTemplateId] ); - this.activeDossier.files = this.activeDossier.files.map(file => + this.activeDossier.files = this.activeDossier?.files.map(file => file.fileId === activeFileWrapper.fileId ? activeFileWrapper : file ); @@ -261,32 +251,26 @@ export class AppStateService { return activeFileWrapper; } - async getFiles(dossier?: DossierWrapper, emitEvents: boolean = true) { - if (!dossier) { - dossier = this.activeDossier; - } + async getFiles(dossier: DossierWrapper = this.activeDossier, emitEvents = true) { const files = await this._statusControllerService.getDossierStatus(dossier.dossierId).toPromise(); return this._processFiles(dossier, files, emitEvents); } - async reanalyzeDossier(dossier?: DossierWrapper) { - if (!dossier) { - dossier = this.activeDossier; - } - await this._reanalysisControllerService.reanalyzeDossier(dossier.dossierId).toPromise(); + async reanalyzeDossier({ dossierId }: DossierWrapper = this.activeDossier) { + await this._reanalysisControllerService.reanalyzeDossier(dossierId).toPromise(); } - activateDossier(dossierId: string) { + activateDossier(dossierId: string): void { this._appState.activeFileId = null; this._appState.activeDossierId = dossierId; if (!this.activeDossier) { this._appState.activeDossierId = null; - this._router.navigate(['/main/dossiers']); + this._router.navigate(['/main/dossiers']).then(); return; - } else { - this.updateDossierDictionary(this.activeDossier.dossierTemplateId, dossierId); } + + this.updateDossierDictionary(this.activeDossier.dossierTemplateId, dossierId); } updateDossierDictionary(dossierTemplateId: string, dossierId: string) { @@ -302,7 +286,7 @@ export class AppStateService { } activateFile(dossierId: string, fileId: string) { - if (this._appState.activeDossierId === dossierId && this._appState.activeFileId === fileId) return; + if (this.activeDossierId === dossierId && this.activeFileId === fileId) return; this.activateDossier(dossierId); if (this.activeDossier) { this._appState.activeFileId = fileId; @@ -341,29 +325,19 @@ export class AppStateService { } deleteDossier(dossier: DossierWrapper) { - return this._dossierControllerService - .deleteDossier(dossier.dossierId) - .toPromise() - .then( - () => { - const index = this._appState.dossiers.findIndex(p => p.dossier.dossierId === dossier.dossierId); - this._appState.dossiers.splice(index, 1); - this._appState.dossiers = [...this._appState.dossiers]; - }, - () => { - this._notificationService.showToastNotification( - this._translateService.instant('dossiers.delete.delete-failed', dossier), - null, - NotificationType.ERROR - ); - } - ); + return this._dossiersService.delete(dossier.dossierId).then( + () => { + const index = this.allDossiers.findIndex(p => p.dossierId === dossier.dossierId); + this._appState.dossiers.splice(index, 1); + }, + () => this._toaster.error('dossiers.delete.delete-failed', { params: dossier }) + ); } - async addOrUpdateDossier(dossier: Dossier) { + async createOrUpdateDossier(dossier: Dossier) { try { - const updatedDossier = await this._dossierControllerService.createOrUpdateDossier(dossier).toPromise(); - let foundDossier = this._appState.dossiers.find(p => p.dossier.dossierId === updatedDossier.dossierId); + const updatedDossier = await this._dossiersService.createOrUpdate(dossier); + let foundDossier = this.allDossiers.find(p => p.dossierId === updatedDossier.dossierId); if (foundDossier) { Object.assign((foundDossier.dossier = updatedDossier)); } else { @@ -373,19 +347,15 @@ export class AppStateService { this._appState.dossiers = [...this._appState.dossiers]; return foundDossier; } catch (error) { - this._notificationService.showToastNotification( - this._translateService.instant( - error.status === 409 ? 'add-dossier-dialog.errors.dossier-already-exists' : 'add-dossier-dialog.errors.generic' - ), - null, - NotificationType.ERROR + this._toaster.error( + error.status === 409 ? 'add-dossier-dialog.errors.dossier-already-exists' : 'add-dossier-dialog.errors.generic' ); } } async reloadActiveDossierFiles() { if (this.activeDossierId) { - await this.getFiles(this.activeDossier); + await this.getFiles(); } } @@ -408,7 +378,7 @@ export class AppStateService { } async loadAllDossiersIfNecessary() { - if (!this._appState.dossiers.length) { + if (!this.allDossiers.length) { await this.loadAllDossiers(); } } @@ -642,7 +612,7 @@ export class AppStateService { return [typeObs, colorsObs]; } - async loadDictionaryData() { + async loadDictionaryData(): Promise { const obj = {}; const observables = []; @@ -660,9 +630,9 @@ export class AppStateService { this._dictionaryData = obj; } - private _getExistingFiles(dossierId: string) { - const found = this._appState.dossiers.find(p => p.dossier.dossierId === dossierId); - return found ? found.files : []; + private _getExistingFiles(dossierId: string): FileStatusWrapper[] { + const dossier = this.allDossiers.find(p => p.dossierId === dossierId); + return dossier?.files ?? []; } private _processFiles(dossier: DossierWrapper, files: FileStatus[], emitEvents: boolean = true) { @@ -727,25 +697,18 @@ export class AppStateService { let totalAnalysedPages = 0; let totalDocuments = 0; const totalPeople = new Set(); - this._appState.dossiers.forEach(p => { - totalDocuments += p.files.length; - if (p.dossier.memberIds) { - p.dossier.memberIds.forEach(m => totalPeople.add(m)); + this.allDossiers.forEach(d => { + totalDocuments += d.files.length; + if (d.dossier.memberIds) { + d.dossier.memberIds.forEach(m => totalPeople.add(m)); } - let numberOfPages = 0; - p.files.forEach(f => { - numberOfPages += f.numberOfPages; - }); - p.totalNumberOfPages = numberOfPages; - totalAnalysedPages += numberOfPages; + + d.totalNumberOfPages = d.files.reduce((acc, file) => acc + file.numberOfPages, 0); + totalAnalysedPages += d.totalNumberOfPages; }); this._appState.totalPeople = totalPeople.size; this._appState.totalAnalysedPages = totalAnalysedPages; this._appState.totalDocuments = totalDocuments; - - if (this.activeDossierId && this.activeFileId) { - this.activateFile(this.activeDossierId, this.activeFileId); - } } } diff --git a/apps/red-ui/src/app/utils/sorters/status-sorter.ts b/apps/red-ui/src/app/utils/sorters/status-sorter.ts index d4d2ed8d8..bb840b6ef 100644 --- a/apps/red-ui/src/app/utils/sorters/status-sorter.ts +++ b/apps/red-ui/src/app/utils/sorters/status-sorter.ts @@ -1,3 +1,13 @@ +type StatusSorterItem = { key?: string } | string; + +const byStatus = (a: StatusSorterItem, b: StatusSorterItem) => { + if (typeof a !== typeof b) return; + + const x = typeof a === 'string' ? a : a.key; + const y = typeof b === 'string' ? b : b.key; + return (StatusSorter[x] = StatusSorter[y]); +}; + export const StatusSorter = { ERROR: 0, UNPROCESSED: 1, @@ -9,5 +19,5 @@ export const StatusSorter = { UNDER_REVIEW: 15, UNDER_APPROVAL: 20, APPROVED: 25, - byKey: (a: { key: string }, b: { key: string }) => StatusSorter[a.key] - StatusSorter[b.key] + byStatus: byStatus }; diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index 07c8a0b8d..865f88981 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -1337,6 +1337,36 @@ "title": "No dossiers match your current filters." } }, + "sorting": { + "alphabetically": "Alphabetically", + "custom": "Custom", + "number-of-analyses": "Number of analyses", + "number-of-pages": "Number of pages", + "oldest": "Oldest", + "recent": "Recent" + }, + "submitted": "Submitted", + "suggestion": "Suggestion for redaction", + "top-bar": { + "navigation-items": { + "back": "Back", + "dossiers": "Active Dossier", + "my-account": { + "children": { + "admin": "Settings", + "downloads": "My Downloads", + "language": { + "de": "German", + "en": "English", + "label": "Language" + }, + "my-profile": "My Profile", + "trash": "Trash", + "logout": "Logout" + } + } + } + }, "type": "Type", "upload-status": { "dialog": {