diff --git a/apps/red-ui/src/app/components/notifications/notifications.component.html b/apps/red-ui/src/app/components/notifications/notifications.component.html index c1d447aab..a744fdf0d 100644 --- a/apps/red-ui/src/app/components/notifications/notifications.component.html +++ b/apps/red-ui/src/app/components/notifications/notifications.component.html @@ -13,11 +13,11 @@
{{ group.date }}
-
View all
+
View all
{{ notification.time }}
; - hasUnreadNotifications$: Observable; - groupedNotifications$: Observable; - private _notifications$ = new BehaviorSubject([]); +export class NotificationsComponent { + readonly hasUnreadNotifications$: Observable; + readonly groupedNotifications$: Observable; constructor( private readonly _translateService: TranslateService, @@ -36,40 +31,30 @@ export class NotificationsComponent extends AutoUnsubscribe implements OnInit { private readonly _activeDossiersService: ActiveDossiersService, private readonly _datePipe: DatePipe, ) { - super(); - this.notifications$ = this._notifications$.asObservable().pipe(shareLast()); - this.groupedNotifications$ = this.notifications$.pipe(map(notifications => this._groupNotifications(notifications))); + this.groupedNotifications$ = this._notificationsService.all$.pipe(map(notifications => this._groupNotifications(notifications))); this.hasUnreadNotifications$ = this._hasUnreadNotifications$; } private get _hasUnreadNotifications$(): Observable { - return this.notifications$.pipe( + return this._notificationsService.all$.pipe( map(notifications => notifications.filter(n => !n.readDate).length > 0), distinctUntilChanged(), shareLast(), ); } - async ngOnInit(): Promise { - await this._loadData(); - - this.addSubscription = timer(CHANGED_CHECK_INTERVAL, CHANGED_CHECK_INTERVAL) - .pipe( - switchMap(() => this._notificationsService.getNotificationsIfChanged(INCLUDE_SEEN)), - tap(notifications => this._notifications$.next(notifications)), - ) - .subscribe(); - } - - async markRead($event, notifications: List = this._notifications$.getValue().map(n => n.id), isRead = true): Promise { + async markRead($event, notifications: Notification[] = this._notificationsService.all, isRead = true): Promise { $event.stopPropagation(); - await firstValueFrom(this._notificationsService.toggleNotificationRead(notifications, isRead)); - await this._loadData(); - } - - private async _loadData(): Promise { - const notifications = await firstValueFrom(this._notificationsService.getNotifications(INCLUDE_SEEN)); - this._notifications$.next(notifications); + if (!notifications.find(notification => !!notification.readDate !== isRead)) { + // If no notification changes status after the request, abort + return; + } + await firstValueFrom( + this._notificationsService.toggleNotificationRead( + notifications.map(n => n.id), + isRead, + ), + ); } private _groupNotifications(notifications: Notification[]): NotificationsGroup[] { diff --git a/apps/red-ui/src/app/modules/account/account-side-nav/account-side-nav.component.ts b/apps/red-ui/src/app/modules/account/account-side-nav/account-side-nav.component.ts index 9aafdd008..a3405ac88 100644 --- a/apps/red-ui/src/app/modules/account/account-side-nav/account-side-nav.component.ts +++ b/apps/red-ui/src/app/modules/account/account-side-nav/account-side-nav.component.ts @@ -22,7 +22,7 @@ export class AccountSideNavComponent { }, { screen: 'notifications', - label: _('notifications'), + label: _('notifications.label'), hideIf: !this._userService.currentUser.isUser, }, ]; 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 732c73209..ba3adff2c 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,12 +1,13 @@ -import { Component, OnDestroy } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { lastIndexOfEnd } from '@utils/functions'; -import { AutoUnsubscribe, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui'; +import { IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { UserService } from '@services/user.service'; import { RouterHistoryService } from '@services/router-history.service'; import { DigitalSignatureService } from '../../services/digital-signature.service'; -import { IDigitalSignature, IDigitalSignatureRequest } from '@red/domain'; +import { IDigitalSignature } from '@red/domain'; +import { firstValueFrom } from 'rxjs'; import { HttpStatusCode } from '@angular/common/http'; @Component({ @@ -14,7 +15,7 @@ import { HttpStatusCode } from '@angular/common/http'; templateUrl: './digital-signature-screen.component.html', styleUrls: ['./digital-signature-screen.component.scss'], }) -export class DigitalSignatureScreenComponent extends AutoUnsubscribe implements OnDestroy { +export class DigitalSignatureScreenComponent implements OnInit { readonly iconButtonTypes = IconButtonTypes; readonly currentUser = this._userService.currentUser; @@ -30,16 +31,18 @@ export class DigitalSignatureScreenComponent extends AutoUnsubscribe implements private readonly _loadingService: LoadingService, readonly routerHistoryService: RouterHistoryService, private readonly _digitalSignatureService: DigitalSignatureService, - ) { - super(); - this.loadDigitalSignatureAndInitializeForm(); - } + ) {} get hasDigitalSignatureSet() { return this.digitalSignatureExists || !!this.form.get('base64EncodedPrivateKey').value; } - saveDigitalSignature() { + async ngOnInit(): Promise { + await this.loadDigitalSignatureAndInitializeForm(); + } + + async saveDigitalSignature(): Promise { + this._loadingService.start(); const formValue = this.form.getRawValue(); const digitalSignature: IDigitalSignature = { ...formValue, @@ -51,29 +54,32 @@ export class DigitalSignatureScreenComponent extends AutoUnsubscribe implements ? this._digitalSignatureService.update(digitalSignature) : this._digitalSignatureService.save(digitalSignature); - this.addSubscription = observable.subscribe({ - next: () => { - this.loadDigitalSignatureAndInitializeForm(); - this._toaster.success(_('digital-signature-screen.action.save-success')); - }, - error: error => { - if (error.status === HttpStatusCode.BadRequest) { - this._toaster.error(_('digital-signature-screen.action.certificate-not-valid-error')); - } else { - this._toaster.error(_('digital-signature-screen.action.save-error')); - } - }, - }); + try { + await firstValueFrom(observable); + await this.loadDigitalSignatureAndInitializeForm(); + this._toaster.success(_('digital-signature-screen.action.save-success')); + } catch (error) { + console.error(error); + if (error.status === HttpStatusCode.BadRequest) { + this._toaster.error(_('digital-signature-screen.action.certificate-not-valid-error')); + } else { + this._toaster.error(_('digital-signature-screen.action.save-error')); + } + } + + this._loadingService.stop(); } - removeDigitalSignature() { - this.addSubscription = this._digitalSignatureService.delete().subscribe( - () => { - this.loadDigitalSignatureAndInitializeForm(); - this._toaster.success(_('digital-signature-screen.action.delete-success')); - }, - () => this._toaster.error(_('digital-signature-screen.action.delete-error')), - ); + async removeDigitalSignature(): Promise { + this._loadingService.start(); + try { + await firstValueFrom(this._digitalSignatureService.delete()); + await this.loadDigitalSignatureAndInitializeForm(); + this._toaster.success(_('digital-signature-screen.action.delete-success')); + } catch (error) { + console.error(error); + this._toaster.error(_('digital-signature-screen.action.delete-error')); + } } fileChanged(event, input: HTMLInputElement) { @@ -89,24 +95,19 @@ export class DigitalSignatureScreenComponent extends AutoUnsubscribe implements fileReader.readAsDataURL(file as Blob); } - loadDigitalSignatureAndInitializeForm() { + async loadDigitalSignatureAndInitializeForm(): Promise { this._loadingService.start(); - this._digitalSignatureService - .getSignature() - .subscribe({ - next: digitalSignature => { - this.digitalSignatureExists = true; - this.digitalSignature = digitalSignature; - }, - error: () => { - this.digitalSignatureExists = false; - this.digitalSignature = {}; - }, - }) - .add(() => { - this.form = this._getForm(); - this._loadingService.stop(); - }); + try { + const digitalSignature = await firstValueFrom(this._digitalSignatureService.getSignature()); + this.digitalSignatureExists = true; + this.digitalSignature = digitalSignature; + } catch (error) { + this.digitalSignatureExists = false; + this.digitalSignature = {}; + } + + this.form = this._getForm(); + this._loadingService.stop(); } private _getForm(): FormGroup { diff --git a/apps/red-ui/src/app/services/notifications.service.ts b/apps/red-ui/src/app/services/notifications.service.ts index f25f49a88..e68359f12 100644 --- a/apps/red-ui/src/app/services/notifications.service.ts +++ b/apps/red-ui/src/app/services/notifications.service.ts @@ -1,56 +1,65 @@ -import { Injectable, Injector } from '@angular/core'; -import { GenericService, List, mapEach, QueryParam, RequiredParam, Validate } from '@iqser/common-ui'; +import { Inject, Injectable, Injector } from '@angular/core'; +import { EntitiesService, List, mapEach, QueryParam, RequiredParam, Validate } from '@iqser/common-ui'; import { TranslateService } from '@ngx-translate/core'; -import { EMPTY, iif, Observable } from 'rxjs'; -import { INotification, Notification, NotificationTypes } from '@red/domain'; -import { map, switchMap } from 'rxjs/operators'; +import { EMPTY, iif, Observable, of, timer } from 'rxjs'; +import { Dossier, INotification, Notification, NotificationTypes } from '@red/domain'; +import { map, switchMap, tap } from 'rxjs/operators'; import { notificationsTranslations } from '../translations/notifications-translations'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { ActiveDossiersService } from './dossiers/active-dossiers.service'; import { UserService } from '@services/user.service'; -import { FilesMapService } from '@services/entity-services/files-map.service'; import dayjs from 'dayjs'; +import { CHANGED_CHECK_INTERVAL } from '@utils/constants'; +import { BASE_HREF } from '../tokens'; +import { ArchivedDossiersService } from '@services/dossiers/archived-dossiers.service'; + +const INCLUDE_SEEN = false; @Injectable({ providedIn: 'root', }) -export class NotificationsService extends GenericService { +export class NotificationsService extends EntitiesService { constructor( + @Inject(BASE_HREF) private readonly _baseHref: string, protected readonly _injector: Injector, private readonly _translateService: TranslateService, private readonly _activeDossiersService: ActiveDossiersService, + private readonly _archivedDossiersService: ArchivedDossiersService, private readonly _userService: UserService, - private readonly _filesMapService: FilesMapService, ) { - super(_injector, 'notification'); + super(_injector, Notification, 'notification'); + + timer(0, CHANGED_CHECK_INTERVAL) + .pipe( + switchMap(() => (this._activeDossiersService.all.length ? of(null) : this._activeDossiersService.loadAll())), + switchMap(() => (this._archivedDossiersService.all.length ? of(null) : this._archivedDossiersService.loadAll())), + switchMap(() => this.#loadNotificationsIfChanged()), + ) + .subscribe(); } @Validate() - getNotifications(@RequiredParam() includeSeen: boolean): Observable { - let queryParam: QueryParam; - if (includeSeen !== undefined && includeSeen !== null) { - queryParam = { key: 'includeSeen', value: includeSeen }; - } - - return this.getAll<{ notifications: Notification[] }>(this._defaultModelPath, [queryParam]).pipe( - map(response => response.notifications.filter(n => n.notificationType in NotificationTypes)), - mapEach(notification => this._new(notification)), - ); - } - - @Validate() - getNotificationsIfChanged(@RequiredParam() includeSeen: boolean): Observable { - return this.hasChanges$().pipe(switchMap(changed => iif(() => changed, this.getNotifications(includeSeen), EMPTY))); - } - - @Validate() - toggleNotificationRead(@RequiredParam() body: List, @RequiredParam() setRead: boolean) { + toggleNotificationRead(@RequiredParam() body: List, @RequiredParam() setRead: boolean): Observable { let queryParam: QueryParam; if (setRead !== undefined && setRead !== null) { queryParam = { key: 'setRead', value: setRead }; } - return this._post(body, `${this._defaultModelPath}/toggle-read`, [queryParam]); + return this._post(body, `${this._defaultModelPath}/toggle-read`, [queryParam]).pipe(switchMap(() => this.#loadAll())); + } + + #loadAll(includeSeen = INCLUDE_SEEN): Observable { + const queryParam: QueryParam = { key: 'includeSeen', value: includeSeen }; + + return this.getAll<{ notifications: Notification[] }>(this._defaultModelPath, [queryParam]).pipe( + map(response => response.notifications.filter(n => n.notificationType in NotificationTypes)), + mapEach(notification => this._new(notification)), + tap(notifications => this.setEntities(notifications)), + ); + } + + #loadNotificationsIfChanged(): Observable { + return this.hasChanges$().pipe(switchMap(changed => iif(() => changed, this.#loadAll(), EMPTY))); } private _new(notification: INotification) { @@ -66,25 +75,27 @@ export class NotificationsService extends GenericService { private _translate(notification: INotification, translation: string): string { const fileId = notification.target.fileId; const dossierId = notification.target.dossierId; - const dossier = this._activeDossiersService.find(dossierId); + const dossier = this._activeDossiersService.find(dossierId) || this._archivedDossiersService.find(dossierId); const fileName = notification.target.fileName; return this._translateService.instant(translation, { - fileHref: this._getFileHref(dossierId, fileId), - dossierHref: this._getDossierHref(dossierId), - dossierName: notification.target?.dossierName || dossier?.dossierName || this._translateService.instant(_('dossier')), + fileHref: this._getFileHref(dossier, fileId), + dossierHref: this._getDossierHref(dossier), + dossierName: + notification.target?.dossierName || + dossier?.dossierName || + this._translateService.instant(_('notifications.deleted-dossier')), fileName: fileName || this._translateService.instant(_('file')), user: this._getUsername(notification.userId), }); } - private _getFileHref(dossierId: string, fileId: string): string { - const dossierHref = this._getDossierHref(dossierId); - return `${dossierHref}/file/${fileId}`; + private _getFileHref(dossier: Dossier, fileId: string): string { + return dossier ? `${this._getDossierHref(dossier)}/file/${fileId}` : null; } - private _getDossierHref(dossierId: string): string { - return `/ui/main/dossiers/${dossierId}`; + private _getDossierHref(dossier: Dossier): string { + return dossier ? `${this._baseHref}${dossier.routerLink}` : null; } private _getUsername(userId: string | undefined) { diff --git a/apps/red-ui/src/assets/i18n/de.json b/apps/red-ui/src/assets/i18n/de.json index 0bbc03d45..9de143c7d 100644 --- a/apps/red-ui/src/assets/i18n/de.json +++ b/apps/red-ui/src/assets/i18n/de.json @@ -665,7 +665,6 @@ "save": "Dokumenteninformation speichern", "title": "Datei-Attribute anlegen" }, - "dossier": "Dossier", "dossier-attribute-types": { "date": "Datum", "image": "Bild", @@ -1530,21 +1529,20 @@ } }, "notification": { - "assign-approver": "Sie wurden dem Dokument {fileName} im Dossier {dossierName} als Genehmiger zugewiesen!", - "assign-reviewer": "Sie wurden dem Dokument {fileName} im Dossier {dossierName} als Reviewer zugewiesen!", - "document-approved": "{fileName} wurde genehmigt!", + "assign-approver": "Sie wurden dem Dokument {fileHref, select, null{{fileName}} other{{fileName}}} im Dossier {dossierHref, select, null{{dossierName}} other{{dossierName}}} als Genehmiger zugewiesen!", + "assign-reviewer": "Sie wurden dem Dokument {fileHref, select, null{{fileName}} other{{fileName}}} im Dossier {dossierHref, select, null{{dossierName}} other{{dossierName}}} als Reviewer zugewiesen!", + "document-approved": "{fileHref, select, null{{fileName}} other{{fileName}}} wurde genehmigt!", "dossier-deleted": "Dossier: {dossierName} wurde gelöscht!", - "dossier-owner-removed": "Der Dossier-Owner von {dossierName} wurde entfernt!", - "dossier-owner-set": "Eigentümer von {dossierName} geändert zu {user}!", + "dossier-owner-removed": "Der Dossier-Owner von {dossierHref, select, null{{dossierName}} other{{dossierName}}} wurde entfernt!", + "dossier-owner-set": "Eigentümer von {dossierHref, select, null{{dossierName}} other{{dossierName}}} geändert zu {user}!", "download-ready": "Ihr Download ist fertig!", "no-data": "Du hast aktuell keine Benachrichtigungen", - "unassigned-from-file": "Sie wurden vom Dokument {fileName} im Dossier {dossierName} entfernt!", - "user-becomes-dossier-member": "{user} ist jetzt Mitglied des Dossiers {dossierName}!", - "user-demoted-to-reviewer": "{user} wurde im Dossier {dossierName} auf die Reviewer-Berechtigung heruntergestuft!", - "user-promoted-to-approver": "{user} wurde im Dossier {dossierName} zum Genehmiger ernannt!", - "user-removed-as-dossier-member": "{user} wurde als Mitglied von: {dossierName} entfernt!" + "unassigned-from-file": "Sie wurden vom Dokument {fileHref, select, null{{fileName}} other{{fileName}}} im Dossier {dossierHref, select, null{{dossierName}} other{{dossierName}}} entfernt!", + "user-becomes-dossier-member": "{user} ist jetzt Mitglied des Dossiers {dossierHref, select, null{{dossierName}} other{{dossierName}}}!", + "user-demoted-to-reviewer": "{user} wurde im Dossier {dossierHref, select, null{{dossierName}} other{{dossierName}}} auf die Reviewer-Berechtigung heruntergestuft!", + "user-promoted-to-approver": "{user} wurde im Dossier {dossierHref, select, null{{dossierName}} other{{dossierName}}} zum Genehmiger ernannt!", + "user-removed-as-dossier-member": "{user} wurde als Mitglied von: {dossierHref, select, null{{dossierName}} other{{dossierName}}} entfernt!" }, - "notifications": "Benachrichtigungen", "notifications-screen": { "category": { "email-notifications": "E-Mail Benachrichtigungen", @@ -1583,6 +1581,11 @@ }, "title": "Benachrichtigungseinstellungen" }, + "notifications": { + "deleted-dossier": "", + "label": "Benachrichtigungen", + "mark-as": "" + }, "ocr": { "confirmation-dialog": { "cancel": "", diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index ff7c38da1..c314b77f9 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -665,7 +665,6 @@ "save": "Save Document Info", "title": "Introduce File Attributes" }, - "dossier": "Dossier", "dossier-attribute-types": { "date": "Date", "image": "Image", @@ -1530,21 +1529,20 @@ } }, "notification": { - "assign-approver": "You have been assigned as approver for {fileName} in dossier: {dossierName}!", - "assign-reviewer": "You have been assigned as reviewer for {fileName} in dossier: {dossierName}!", - "document-approved": " {fileName} has been approved!", + "assign-approver": "You have been assigned as approver for {fileHref, select, null{{fileName}} other{{fileName}}} in dossier: {dossierHref, select, null{{dossierName}} other{{dossierName}}}!", + "assign-reviewer": "You have been assigned as reviewer for {fileHref, select, null{{fileName}} other{{fileName}}} in dossier: {dossierHref, select, null{{dossierName}} other{{dossierName}}}!", + "document-approved": " {fileHref, select, null{{fileName}} other{{fileName}}} has been approved!", "dossier-deleted": "Dossier: {dossierName} has been deleted!", - "dossier-owner-removed": "You have been removed as dossier owner from {dossierName}!", - "dossier-owner-set": "You are now the dossier owner of {dossierName}!", + "dossier-owner-removed": "You have been removed as dossier owner from {dossierHref, select, null{{dossierName}} other{{dossierName}}}!", + "dossier-owner-set": "You are now the dossier owner of {dossierHref, select, null{{dossierName}} other{{dossierName}}}!", "download-ready": "Your download is ready!", "no-data": "You currently have no notifications", - "unassigned-from-file": "You have been unassigned from {fileName} in dossier: {dossierName}!", - "user-becomes-dossier-member": "You have been added to dossier: {dossierName}!", - "user-demoted-to-reviewer": "You have been demoted to reviewer in dossier: {dossierName}!", - "user-promoted-to-approver": "You have been promoted to approver in dossier: {dossierName}!", - "user-removed-as-dossier-member": "You have been removed as a member from dossier: {dossierName} !" + "unassigned-from-file": "You have been unassigned from {fileHref, select, null{{fileName}} other{{fileName}}} in dossier: {dossierHref, select, null{{dossierName}} other{{dossierHref, select, null{{dossierName}} other{{dossierName}}}}}!", + "user-becomes-dossier-member": "You have been added to dossier: {dossierHref, select, null{{dossierName}} other{{dossierName}}}!", + "user-demoted-to-reviewer": "You have been demoted to reviewer in dossier: {dossierHref, select, null{{dossierName}} other{{dossierName}}}!", + "user-promoted-to-approver": "You have been promoted to approver in dossier: {dossierHref, select, null{{dossierName}} other{{dossierName}}}!", + "user-removed-as-dossier-member": "You have been removed as a member from dossier: {dossierHref, select, null{{dossierName}} other{{dossierName}}}!" }, - "notifications": "Notifications", "notifications-screen": { "category": { "email-notifications": "Email Notifications", @@ -1583,6 +1581,11 @@ }, "title": "Notifications Preferences" }, + "notifications": { + "deleted-dossier": "Deleted Dossier", + "label": "Notifications", + "mark-as": "Mark as {type, select, read{read} unread{unread} other{}}" + }, "ocr": { "confirmation-dialog": { "cancel": "Cancel", diff --git a/package.json b/package.json index 66ecaf42d..f2d09f236 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redaction", - "version": "3.376.0", + "version": "3.377.0", "private": true, "license": "MIT", "scripts": { diff --git a/paligo-theme.tar.gz b/paligo-theme.tar.gz index 7c89c4820..625912511 100644 Binary files a/paligo-theme.tar.gz and b/paligo-theme.tar.gz differ