diff --git a/dmp-backend/notification-service/notification-web/target/classes/notification_templates/dmpinvitationexistinguser/inapp/body.en.html b/dmp-backend/notification-service/notification-web/target/classes/notification_templates/dmpinvitationexistinguser/inapp/body.en.html new file mode 100644 index 000000000..75a586097 --- /dev/null +++ b/dmp-backend/notification-service/notification-web/target/classes/notification_templates/dmpinvitationexistinguser/inapp/body.en.html @@ -0,0 +1,304 @@ + + + + + + Simple Transactional Email + + + + + + + + + +
  +
+ + + This is preheader text. Some clients will show this text as a preview. + + + + + + + + +
+ + + + +
+

Dear {recipient},

+

{reasonName} just add you to collaborate to Data Management plan {dmpname} with role {dmprole}.

+

Click the button to redirect to {dmpname}.

+ + + + + + + +
+ + + + + + +
Join
+
+
+
+ + + + + + +
+
 
+ + \ No newline at end of file diff --git a/dmp-backend/notification-service/notification-web/target/classes/notification_templates/dmpinvitationexistinguser/inapp/subject.en.txt b/dmp-backend/notification-service/notification-web/target/classes/notification_templates/dmpinvitationexistinguser/inapp/subject.en.txt new file mode 100644 index 000000000..2c36d1524 --- /dev/null +++ b/dmp-backend/notification-service/notification-web/target/classes/notification_templates/dmpinvitationexistinguser/inapp/subject.en.txt @@ -0,0 +1 @@ +ARGOS - Data Management Plan Invite \ No newline at end of file diff --git a/dmp-frontend/src/app/app-routing.module.ts b/dmp-frontend/src/app/app-routing.module.ts index 797d49a70..40d903ebc 100644 --- a/dmp-frontend/src/app/app-routing.module.ts +++ b/dmp-frontend/src/app/app-routing.module.ts @@ -282,6 +282,18 @@ const appRoutes: Routes = [ }) }, }, + { + path: 'inapp-notifications', + loadChildren: () => import('./ui/inapp-notification/inapp-notification.module').then(m => m.InAppNotificationModule), + data: { + authContext: { + permissions: [AppPermission.ViewInAppNotificationPage] + }, + ...BreadcrumbService.generateRouteDataConfiguration({ + title: 'BREADCRUMBS.INAPP-NOTIFICATIONS' + }) + }, + }, { path: 'index-managment', loadChildren: () => import('./ui/admin/index-managment/index-managment.module').then(m => m.IndexManagmentModule), diff --git a/dmp-frontend/src/app/core/common/enum/notification-inapp-tracking.enum.ts b/dmp-frontend/src/app/core/common/enum/notification-inapp-tracking.enum.ts new file mode 100644 index 000000000..92422833e --- /dev/null +++ b/dmp-frontend/src/app/core/common/enum/notification-inapp-tracking.enum.ts @@ -0,0 +1,4 @@ +export enum NotificationInAppTracking { + Stored = 0, + Delivered = 1 +} diff --git a/dmp-frontend/src/app/core/common/enum/permission.enum.ts b/dmp-frontend/src/app/core/common/enum/permission.enum.ts index 205ec77ca..5f0772dd1 100644 --- a/dmp-frontend/src/app/core/common/enum/permission.enum.ts +++ b/dmp-frontend/src/app/core/common/enum/permission.enum.ts @@ -34,6 +34,7 @@ export enum AppPermission { ViewTenantPage = 'ViewTenantPage', ViewLanguagePage = "ViewLanguagePage", ViewNotificationTemplatePage = "ViewNotificationTemplatePage", + ViewInAppNotificationPage = "ViewInAppNotificationPage", //ReferenceType BrowseReferenceType = "BrowseReferenceType", diff --git a/dmp-frontend/src/app/core/core-service.module.ts b/dmp-frontend/src/app/core/core-service.module.ts index ae5601e81..824193065 100644 --- a/dmp-frontend/src/app/core/core-service.module.ts +++ b/dmp-frontend/src/app/core/core-service.module.ts @@ -44,6 +44,7 @@ import { UserService } from './services/user/user.service'; import { FileUtils } from './services/utilities/file-utils.service'; import { QueryParamsService } from './services/utilities/query-params.service'; import { FileTransformerHttpService } from './services/file-transformer/file-transformer.http.service'; +import { InAppNotificationService } from './services/inapp-notification/inapp-notification.service'; // // // This is shared module that provides all the services. Its imported only once on the AppModule. @@ -106,7 +107,8 @@ export class CoreServiceModule { TagService, CanDeactivateGuard, FileTransformerService, - FileTransformerHttpService + FileTransformerHttpService, + InAppNotificationService ], }; } diff --git a/dmp-frontend/src/app/core/model/inapp-notification/inapp-notification.model.ts b/dmp-frontend/src/app/core/model/inapp-notification/inapp-notification.model.ts new file mode 100644 index 000000000..b0a3a5bb2 --- /dev/null +++ b/dmp-frontend/src/app/core/model/inapp-notification/inapp-notification.model.ts @@ -0,0 +1,17 @@ +import { Guid } from '@common/types/guid'; +import { User } from '../user/user'; +import { IsActive } from '@app/core/common/enum/is-active.enum'; +import { NotificationInAppTracking } from '@app/core/common/enum/notification-inapp-tracking.enum'; + +export interface InAppNotification { + id: Guid; + user: User; + isActive: IsActive; + type: Guid; + trackingState: NotificationInAppTracking; + subject: string; + body: string; + createdAt: Date; + updatedAt: Date; + hash: string; +} diff --git a/dmp-frontend/src/app/core/query/inapp-notification.lookup.ts b/dmp-frontend/src/app/core/query/inapp-notification.lookup.ts new file mode 100644 index 000000000..e4ae69cfe --- /dev/null +++ b/dmp-frontend/src/app/core/query/inapp-notification.lookup.ts @@ -0,0 +1,22 @@ +import { Lookup } from '@common/model/lookup'; +import { NotificationType } from '../common/enum/notification-type'; +import { NotificationInAppTracking } from '../common/enum/notification-inapp-tracking.enum'; +import { IsActive } from '../common/enum/is-active.enum'; + +export class InAppNotificationLookup extends Lookup implements InAppNotificationFilter { + like: string; + isActive: IsActive[]; + type: NotificationType[]; + trackingState: NotificationInAppTracking[]; + + constructor() { + super(); + } +} + +export interface InAppNotificationFilter { + like: string; + isActive: IsActive[]; + trackingState: NotificationInAppTracking[]; + type: NotificationType[]; +} diff --git a/dmp-frontend/src/app/core/services/inapp-notification/inapp-notification.service.ts b/dmp-frontend/src/app/core/services/inapp-notification/inapp-notification.service.ts new file mode 100644 index 000000000..761495df3 --- /dev/null +++ b/dmp-frontend/src/app/core/services/inapp-notification/inapp-notification.service.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@angular/core'; +import { InAppNotification } from '@app/core/model/inapp-notification/inapp-notification.model'; +import { InAppNotificationLookup } from '@app/core/query/inapp-notification.lookup'; +import { BaseHttpParams } from '@common/http/base-http-params'; +import { InterceptorType } from '@common/http/interceptors/interceptor-type'; +import { QueryResult } from '@common/model/query-result'; +import { Guid } from '@common/types/guid'; +import { Observable, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { BaseHttpV2Service } from '../http/base-http-v2.service'; +import { ConfigurationService } from '../configuration/configuration.service'; + +@Injectable() +export class InAppNotificationService { + + constructor(private http: BaseHttpV2Service, private configurationService: ConfigurationService) { + } + + private get apiBase(): string { return `${this.configurationService.notificationServiceAddress}inapp-notification`; } + + + query(q: InAppNotificationLookup): Observable> { + const url = `${this.apiBase}/query`; + + return this.http + .post>(url, q).pipe( + catchError((error: any) => throwError(error))); + } + + getSingle(id: Guid, reqFields: string[] = []): Observable { + const url = `${this.apiBase}/${id}`; + const options = { params: { f: reqFields } }; + + return this.http + .get(url, options).pipe( + catchError((error: any) => throwError(error))); + } + + read(id: Guid): Observable { + const url = `${this.apiBase}/${id}/read`; + + return this.http + .post(url, {}).pipe( + catchError((error: any) => throwError(error))); + } + + countUnread(): Observable { + const url = `${this.apiBase}/count-unread`; + const params = new BaseHttpParams(); + params.interceptorContext = { + excludedInterceptors: [InterceptorType.ProgressIndication] + }; + const options = { params: params }; + + return this.http + .get(url, options).pipe( + catchError((error: any) => throwError(error))); + } + + delete(id: Guid): Observable { + const url = `${this.apiBase}/${id}`; + return this.http + .delete(url).pipe( + catchError((error: any) => throwError(error))); + } +} diff --git a/dmp-frontend/src/app/core/services/utilities/enum-utils.service.ts b/dmp-frontend/src/app/core/services/utilities/enum-utils.service.ts index ad1835599..92b453c9e 100644 --- a/dmp-frontend/src/app/core/services/utilities/enum-utils.service.ts +++ b/dmp-frontend/src/app/core/services/utilities/enum-utils.service.ts @@ -27,6 +27,7 @@ import { DmpStatus } from '../../common/enum/dmp-status'; import { ValidationType } from '../../common/enum/validation-type'; import { RecentActivityOrder } from '@app/core/common/enum/recent-activity-order'; import { NotificationType } from '@app/core/common/enum/notification-type'; +import { NotificationInAppTracking } from '@app/core/common/enum/notification-inapp-tracking.enum'; @Injectable() export class EnumUtils { @@ -334,4 +335,11 @@ export class EnumUtils { case NotificationType.publicContactSupportType: return this.language.instant('TYPES.NOTIFICATION-TEMPLATE-NOTIFICATION-TYPE.PUBLIC-CONTACT-SUPPORT'); } } + + public toNotificationInAppTrackingString(value: NotificationInAppTracking): string { + switch (value) { + case NotificationInAppTracking.Delivered: return this.language.instant('TYPES.NOTIFICATION-INAPP-TRACKING.DELIVERED'); + case NotificationInAppTracking.Stored: return this.language.instant('TYPES.NOTIFICATION-INAPP-TRACKING.STORED'); + } + } } diff --git a/dmp-frontend/src/app/ui/inapp-notification/editor/inapp-notification-editor.component.html b/dmp-frontend/src/app/ui/inapp-notification/editor/inapp-notification-editor.component.html new file mode 100644 index 000000000..fa1640e12 --- /dev/null +++ b/dmp-frontend/src/app/ui/inapp-notification/editor/inapp-notification-editor.component.html @@ -0,0 +1,21 @@ +
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/dmp-frontend/src/app/ui/inapp-notification/editor/inapp-notification-editor.component.scss b/dmp-frontend/src/app/ui/inapp-notification/editor/inapp-notification-editor.component.scss new file mode 100644 index 000000000..c916223d1 --- /dev/null +++ b/dmp-frontend/src/app/ui/inapp-notification/editor/inapp-notification-editor.component.scss @@ -0,0 +1,7 @@ +.inapp-notification-editor { + padding-top: 1em; + + .editor-actions { + margin-top: 30px; + } +} diff --git a/dmp-frontend/src/app/ui/inapp-notification/editor/inapp-notification-editor.component.ts b/dmp-frontend/src/app/ui/inapp-notification/editor/inapp-notification-editor.component.ts new file mode 100644 index 000000000..60657b505 --- /dev/null +++ b/dmp-frontend/src/app/ui/inapp-notification/editor/inapp-notification-editor.component.ts @@ -0,0 +1,130 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, OnInit } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, ParamMap, Router } from '@angular/router'; +import { BaseComponent } from '@common/base/base.component'; +import { FormService } from '@common/forms/form-service'; +import { LoggingService } from '@app/core/services/logging/logging-service'; +import { ConfirmationDialogComponent } from '@common/modules/confirmation-dialog/confirmation-dialog.component'; +import { HttpError, HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service'; +import { Guid } from '@common/types/guid'; +import { TranslateService } from '@ngx-translate/core'; +import { IsActive } from '@app/core/common/enum/is-active.enum'; +import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; +import { map, takeUntil } from 'rxjs/operators'; +import { nameof } from 'ts-simple-nameof'; +import { AuthService } from '@app/core/services/auth/auth.service'; +import { InAppNotification } from '@app/core/model/inapp-notification/inapp-notification.model'; +import { InAppNotificationService } from '@app/core/services/inapp-notification/inapp-notification.service'; +import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; +import { NotificationInAppTracking } from '@app/core/common/enum/notification-inapp-tracking.enum'; + +@Component({ + selector: 'app-inapp-notification-editor', + templateUrl: './inapp-notification-editor.component.html', + styleUrls: ['./inapp-notification-editor.component.scss'] +}) +export class InAppNotificationEditorComponent extends BaseComponent implements OnInit { + + isDeleted = false; + isRead = false; + isNew = false; + isFromDialog = false; + inappNotification: InAppNotification; + + constructor( + public authService: AuthService, + private dialog: MatDialog, + private inappNotificationService: InAppNotificationService, + private route: ActivatedRoute, + private router: Router, + private language: TranslateService, + public enumUtils: EnumUtils, + private formService: FormService, + private uiNotificationService: UiNotificationService, + private logger: LoggingService, + private httpErrorHandlingService: HttpErrorHandlingService + ) { + super(); + } + + ngOnInit(): void { + this.route.paramMap.pipe(takeUntil(this._destroyed)).subscribe((paramMap: ParamMap) => { + const itemId = paramMap.get('id'); + this.isFromDialog = this.route.snapshot.data ? this.route.snapshot.data['isFromDialog'] as boolean : false; + + if (itemId != null) { + this.inappNotificationService.getSingle(Guid.parse(itemId), + [ + nameof(x => x.id), + nameof(x => x.subject), + nameof(x => x.body), + nameof(x => x.type), + nameof(x => x.trackingState), + nameof(x => x.isActive), + nameof(x => x.hash), + nameof(x => x.updatedAt) + ]) + .pipe(map(data => data as InAppNotification), takeUntil(this._destroyed)) + .subscribe( + data => { + this.inappNotification = data; + this.isDeleted = this.inappNotification.isActive === IsActive.Inactive; + this.isRead = this.inappNotification.trackingState === NotificationInAppTracking.Delivered; + }, + error => this.onCallbackError(error) + ); + } else { + this.inappNotification = null; + } + }); + } + + public markAsRead() { + const value = this.inappNotification; + this.inappNotificationService.read(value.id).pipe(takeUntil(this._destroyed)) + .subscribe( + complete => this.onCallbackSuccess(), + error => this.onCallbackError(error) + ); + // this.clearErrorModel(); + } + + public delete() { + const value = this.inappNotification; + if (value.id) { + const dialogRef = this.dialog.open(ConfirmationDialogComponent, { + maxWidth: '300px', + restoreFocus: false, + data: { + message: this.language.instant('COMMONS.CONFIRMATION-DIALOG.DELETE-ITEM'), + confirmButton: this.language.instant('COMMONS.CONFIRMATION-DIALOG.ACTIONS.CONFIRM'), + cancelButton: this.language.instant('COMMONS.CONFIRMATION-DIALOG.ACTIONS.CANCEL') + } + }); + dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => { + if (result) { + this.inappNotificationService.delete(value.id).pipe(takeUntil(this._destroyed)) + .subscribe( + complete => this.onCallbackSuccess(), + error => this.onCallbackError(error) + ); + } + }); + } + } + + public cancel(): void { + this.router.navigate(['/inapp-notifications']); + } + + onCallbackSuccess(data?: any): void { + this.uiNotificationService.snackBarNotification(this.language.instant('COMMONS.SNACK-BAR.SUCCESSFUL-UPDATE'), SnackBarNotificationLevel.Success); + this.router.navigate(['/inapp-notifications']); + } + + onCallbackError(errorResponse: HttpErrorResponse) { + const error: HttpError = this.httpErrorHandlingService.getError(errorResponse); + this.uiNotificationService.snackBarNotification(error.getMessagesString(), SnackBarNotificationLevel.Warning); + } +} diff --git a/dmp-frontend/src/app/ui/inapp-notification/inapp-notification-routing.module.ts b/dmp-frontend/src/app/ui/inapp-notification/inapp-notification-routing.module.ts new file mode 100644 index 000000000..4dad21bfa --- /dev/null +++ b/dmp-frontend/src/app/ui/inapp-notification/inapp-notification-routing.module.ts @@ -0,0 +1,35 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AuthGuard } from '@app/core/auth-guard.service'; +import { InAppNotificationListingComponent } from './listing/inapp-notification-listing.component'; +import { InAppNotificationEditorComponent } from './editor/inapp-notification-editor.component'; + +const routes: Routes = [ + { + path: '', + component: InAppNotificationListingComponent, + data: { + }, + canActivate: [AuthGuard] + }, + { + path: 'dialog/:id', + canActivate: [AuthGuard], + data: { + isFromDialog: true, + }, + component: InAppNotificationEditorComponent + }, + { + path: ':id', + canActivate: [AuthGuard], + component: InAppNotificationEditorComponent + }, + { path: '**', loadChildren: () => import('@common/modules/page-not-found/page-not-found.module').then(m => m.PageNotFoundModule) }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class InAppNotificationRoutingModule { } diff --git a/dmp-frontend/src/app/ui/inapp-notification/inapp-notification.module.ts b/dmp-frontend/src/app/ui/inapp-notification/inapp-notification.module.ts new file mode 100644 index 000000000..33f09eac3 --- /dev/null +++ b/dmp-frontend/src/app/ui/inapp-notification/inapp-notification.module.ts @@ -0,0 +1,43 @@ +import { NgModule } from '@angular/core'; +import { CommonFormsModule } from '@common/forms/common-forms.module'; +import { ConfirmationDialogModule } from '@common/modules/confirmation-dialog/confirmation-dialog.module'; +import { TextFilterModule } from '@common/modules/text-filter/text-filter.module'; +import { UserSettingsModule } from '@common/modules/user-settings/user-settings.module'; +import { CommonUiModule } from '@common/ui/common-ui.module'; +import { InAppNotificationRoutingModule } from './inapp-notification-routing.module'; +import { InAppNotificationListingComponent } from './listing/inapp-notification-listing.component'; +import { HybridListingModule } from '@common/modules/hybrid-listing/hybrid-listing.module'; +import { InAppNotificationEditorComponent } from './editor/inapp-notification-editor.component'; +import { InAppNotificationListingFiltersComponent } from './listing/filters/inapp-notification-listing-filters.component'; +import { InAppNotificationListingDialogComponent } from './listing-dialog/inapp-notification-listing-dialog.component'; +import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; +import { NotificationInAppTrackingTypePipe } from '@common/formatting/pipes/notification-inapp-tracking-type.pipe'; +import { DatePipe } from '@angular/common'; +import { CommonFormattingModule } from '@common/formatting/common-formatting.module'; + +@NgModule({ + imports: [ + CommonUiModule, + CommonFormsModule, + ConfirmationDialogModule, + HybridListingModule, + TextFilterModule, + InAppNotificationRoutingModule, + UserSettingsModule, + CommonFormattingModule + ], + declarations: [ + InAppNotificationListingComponent, + InAppNotificationEditorComponent, + InAppNotificationListingFiltersComponent, + InAppNotificationListingDialogComponent + ], + // entryComponents: [ + // InAppNotificationListingDialogComponent + // ], + exports: [ + InAppNotificationListingComponent, + InAppNotificationListingDialogComponent + ] +}) +export class InAppNotificationModule { } diff --git a/dmp-frontend/src/app/ui/inapp-notification/listing-dialog/inapp-notification-listing-dialog.component.html b/dmp-frontend/src/app/ui/inapp-notification/listing-dialog/inapp-notification-listing-dialog.component.html new file mode 100644 index 000000000..1ecd1ee51 --- /dev/null +++ b/dmp-frontend/src/app/ui/inapp-notification/listing-dialog/inapp-notification-listing-dialog.component.html @@ -0,0 +1,13 @@ + + + drafts + mail + {{ inappNotification.subject }} + {{ inappNotification.createdAt | date : 'short'}} + + + + {{'APP.NAVIGATION.ALL-INAPP-NOTIFICATIONS' + | translate}} + + diff --git a/dmp-frontend/src/app/ui/inapp-notification/listing-dialog/inapp-notification-listing-dialog.component.scss b/dmp-frontend/src/app/ui/inapp-notification/listing-dialog/inapp-notification-listing-dialog.component.scss new file mode 100644 index 000000000..7d81257f9 --- /dev/null +++ b/dmp-frontend/src/app/ui/inapp-notification/listing-dialog/inapp-notification-listing-dialog.component.scss @@ -0,0 +1,11 @@ +.inapp-notification-listing-dialog { + padding-top: 0em; + + .unread{ + font-weight:bold + } + + .secondary-text { + color: rgba(0, 0, 0, 0.54); + } +} diff --git a/dmp-frontend/src/app/ui/inapp-notification/listing-dialog/inapp-notification-listing-dialog.component.ts b/dmp-frontend/src/app/ui/inapp-notification/listing-dialog/inapp-notification-listing-dialog.component.ts new file mode 100644 index 000000000..b95f85caa --- /dev/null +++ b/dmp-frontend/src/app/ui/inapp-notification/listing-dialog/inapp-notification-listing-dialog.component.ts @@ -0,0 +1,88 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, Inject, OnInit } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { AuthService } from '@app/core/services/auth/auth.service'; +import { HttpError, HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service'; +import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; +import { BaseComponent } from '@common/base/base.component'; +import { NotificationInAppTracking } from '@app/core/common/enum/notification-inapp-tracking.enum'; +import { InAppNotification } from '@app/core/model/inapp-notification/inapp-notification.model'; +import { InAppNotificationLookup } from '@app/core/query/inapp-notification.lookup'; +import { InAppNotificationService } from '@app/core/services/inapp-notification/inapp-notification.service'; +import { takeUntil } from 'rxjs/operators'; +import { nameof } from 'ts-simple-nameof'; + +@Component({ + selector: 'app-inapp-notification-listing-dialog', + templateUrl: './inapp-notification-listing-dialog.component.html', + styleUrls: ['./inapp-notification-listing-dialog.component.scss'] +}) +export class InAppNotificationListingDialogComponent extends BaseComponent implements OnInit { + public inappNotifications = new Array(); + public notificationInAppTrackingEnum = NotificationInAppTracking; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public dialogData: any, + private inappNotificationService: InAppNotificationService, + private router: Router, + private uiNotificationService: UiNotificationService, + private httpErrorHandlingService: HttpErrorHandlingService, + public authService: AuthService, + ) { + super(); + } + + ngOnInit() { + const lookup = new InAppNotificationLookup(); + lookup.project = { + fields: [ + nameof(x => x.id), + nameof(x => x.subject), + nameof(x => x.createdAt), + nameof(x => x.trackingState), + ] + }; + lookup.page = { offset: 0, size: 10 }; + lookup.order = { items: ['-' + nameof(x => x.createdAt)] }; + this.inappNotificationService.query(lookup) + .pipe(takeUntil(this._destroyed)) + .subscribe( + data => { + this.inappNotifications = data.items; + }, + error => this.onCallbackError(error), + ); + } + + private onCallbackError(errorResponse: HttpErrorResponse) { + const error: HttpError = this.httpErrorHandlingService.getError(errorResponse); + this.uiNotificationService.snackBarNotification(error.getMessagesString(), SnackBarNotificationLevel.Warning); + } + + goToNotification(item: InAppNotification) { + if (item.trackingState === NotificationInAppTracking.Stored) { + this.inappNotificationService.read(item.id) + .pipe(takeUntil(this._destroyed)) + .subscribe( + data => { + this.dialogRef.close(); + this.router.navigate(['/inapp-notifications/dialog/' + item.id]); + }, + error => { + this.dialogRef.close(); + this.router.navigate(['/inapp-notifications/dialog/' + item.id]); + }, + ); + } else { + this.dialogRef.close(); + this.router.navigate(['/inapp-notifications/dialog/' + item.id]); + } + } + + goToNotifications() { + this.router.navigate(['/inapp-notifications']); + this.dialogRef.close(); + } +} diff --git a/dmp-frontend/src/app/ui/inapp-notification/listing/filters/inapp-notification-listing-filters.component.html b/dmp-frontend/src/app/ui/inapp-notification/listing/filters/inapp-notification-listing-filters.component.html new file mode 100644 index 000000000..7221b4ad0 --- /dev/null +++ b/dmp-frontend/src/app/ui/inapp-notification/listing/filters/inapp-notification-listing-filters.component.html @@ -0,0 +1,30 @@ +
+ + +
+ +
+ +
+
+
+
+ + {{'NOTIFICATION-SERVICE.INAPP-NOTIFICATION-LISTING.FILTER.TRACKING-STATE' | translate}} + + + {{enumUtils.toNotificationInAppTrackingString(trackingState)}} + + +
+ + {{'NOTIFICATION-SERVICE.INAPP-NOTIFICATION-LISTING.FILTER.SHOW-INACTIVE' | translate}} + +
+
+
+
diff --git a/dmp-frontend/src/app/ui/inapp-notification/listing/filters/inapp-notification-listing-filters.component.scss b/dmp-frontend/src/app/ui/inapp-notification/listing/filters/inapp-notification-listing-filters.component.scss new file mode 100644 index 000000000..b533340b2 --- /dev/null +++ b/dmp-frontend/src/app/ui/inapp-notification/listing/filters/inapp-notification-listing-filters.component.scss @@ -0,0 +1,22 @@ +.inapp-notification-listing-filters { + margin: 1em 0; + + .filter-actions { + display: flex; + } + + .toggle { + font-weight: 400; + height: 49px; + min-height: 53.59px; + background-color: white; + border-radius: 0.4em; + border: 1px solid #e0e0e0; + } + } + + :host ::ng-deep .mat-form-field-wrapper { + padding-bottom: 0; + margin: 1em 0 !important; + } + \ No newline at end of file diff --git a/dmp-frontend/src/app/ui/inapp-notification/listing/filters/inapp-notification-listing-filters.component.ts b/dmp-frontend/src/app/ui/inapp-notification/listing/filters/inapp-notification-listing-filters.component.ts new file mode 100644 index 000000000..3ee0ab0a8 --- /dev/null +++ b/dmp-frontend/src/app/ui/inapp-notification/listing/filters/inapp-notification-listing-filters.component.ts @@ -0,0 +1,66 @@ +import { Component, EventEmitter, Input, OnInit, Output, SimpleChanges, OnChanges } from '@angular/core'; +import { BaseComponent } from '@common/base/base.component'; +import { IsActive } from '@app/core/common/enum/is-active.enum'; +import { NotificationInAppTracking } from '@app/core/common/enum/notification-inapp-tracking.enum'; +import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; +import { InAppNotificationFilter } from '@app/core/query/inapp-notification.lookup'; +@Component({ + selector: 'app-inapp-notification-listing-filters', + templateUrl: './inapp-notification-listing-filters.component.html', + styleUrls: ['./inapp-notification-listing-filters.component.scss'] +}) +export class InAppNotificationListingFiltersComponent extends BaseComponent implements OnInit, OnChanges { + + @Input() filter: InAppNotificationFilter; + @Output() filterChange = new EventEmitter(); + panelExpanded = false; + trackingStates: NotificationInAppTracking[] = this.enumUtils.getEnumValues(NotificationInAppTracking); + + constructor( + public enumUtils: EnumUtils + ) { super(); } + + ngOnInit() { + this.panelExpanded = !this.areHiddenFieldsEmpty(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['filter']) { this.panelExpanded = !this.areHiddenFieldsEmpty(); } + } + + onFilterChange() { + this.filterChange.emit(this.filter); + } + + private areHiddenFieldsEmpty(): boolean { + return (!this.filter.isActive || this.filter.isActive.length === 0 || (this.filter.isActive.length === 1 && this.filter.isActive[0] === IsActive.Active)); + } + + // + // Filter getters / setters + // Implement here any custom logic regarding how these fields are applied to the lookup. + // + get like(): string { + return this.filter.like; + } + set like(value: string) { + this.filter.like = value; + this.filterChange.emit(this.filter); + } + + get isActive(): boolean { + return this.filter.isActive ? this.filter.isActive.includes(IsActive.Inactive) : true; + } + set isActive(value: boolean) { + this.filter.isActive = value ? [IsActive.Active, IsActive.Inactive] : [IsActive.Active]; + this.filterChange.emit(this.filter); + } + + get trackingState(): NotificationInAppTracking[] { + return this.filter.trackingState ? this.filter.trackingState : null; + } + set trackingState(value: NotificationInAppTracking[]) { + this.filter.trackingState = value && value.length > 0 ? value : null; + this.filterChange.emit(this.filter); + } +} diff --git a/dmp-frontend/src/app/ui/inapp-notification/listing/inapp-notification-listing-user-settings.ts b/dmp-frontend/src/app/ui/inapp-notification/listing/inapp-notification-listing-user-settings.ts new file mode 100644 index 000000000..ae5cf811c --- /dev/null +++ b/dmp-frontend/src/app/ui/inapp-notification/listing/inapp-notification-listing-user-settings.ts @@ -0,0 +1,52 @@ +import { Serializable } from '@common/types/json/serializable'; +import { Lookup } from '@common/model/lookup'; +import { IsActive } from '@app/core/common/enum/is-active.enum'; +import { InAppNotification } from '@app/core/model/inapp-notification/inapp-notification.model'; +import { InAppNotificationLookup } from '@app/core/query/inapp-notification.lookup'; +import { UserSettingsInformation, UserSettingsLookupBuilder } from '@app/core/model/user-settings/user-settings.model'; +import { nameof } from 'ts-simple-nameof'; + +export class InAppNotificationListingUserSettings implements Serializable, UserSettingsLookupBuilder { + + private like: string; + private isActive: IsActive[] = [IsActive.Active]; + private order: Lookup.Ordering = { items: [nameof(x => x.createdAt)] }; + private project: Lookup.FieldDirectives = { + fields: [ + nameof(x => x.id), + nameof(x => x.subject), + nameof(x => x.trackingState), + nameof(x => x.createdAt) + ] + }; + + static getUserSettingsInformation(): UserSettingsInformation { + return { + key: 'InAppNotificationListingUserSettings', + type: InAppNotificationListingUserSettings + }; + } + + public fromJSONObject(item: any): InAppNotificationListingUserSettings { + this.like = item.like; + this.isActive = item.isActive; + this.order = item.order; + this.project = item.project; + return this; + } + + public update(lookup: InAppNotificationLookup) { + this.like = lookup.like; + this.isActive = lookup.isActive; + this.order = lookup.order; + this.project = lookup.project; + } + + public apply(lookup: InAppNotificationLookup): InAppNotificationLookup { + lookup.like = this.like; + lookup.isActive = this.isActive; + lookup.order = this.order; + lookup.project = this.project; + return lookup; + } +} diff --git a/dmp-frontend/src/app/ui/inapp-notification/listing/inapp-notification-listing.component.html b/dmp-frontend/src/app/ui/inapp-notification/listing/inapp-notification-listing.component.html new file mode 100644 index 000000000..b0d2d3803 --- /dev/null +++ b/dmp-frontend/src/app/ui/inapp-notification/listing/inapp-notification-listing.component.html @@ -0,0 +1,46 @@ +
+
+
+
+ +
+ + +
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ + + + + drafts + mail + +
+
+
+
diff --git a/dmp-frontend/src/app/ui/inapp-notification/listing/inapp-notification-listing.component.scss b/dmp-frontend/src/app/ui/inapp-notification/listing/inapp-notification-listing.component.scss new file mode 100644 index 000000000..299948e33 --- /dev/null +++ b/dmp-frontend/src/app/ui/inapp-notification/listing/inapp-notification-listing.component.scss @@ -0,0 +1,28 @@ +.inapp-notification-listing { + // padding-top: 1em; + + .toggle-row { + min-height: 56px; + } + + .preview-btn { + color: rgb(193, 202, 211); + } + + .preview-btn-active { + color: black; + } + + .view-mode-toggle-container { + padding-left: 30px; + } + + .mat-fab-bottom-right { + top: auto !important; + right: 20px !important; + bottom: 10px !important; + left: auto !important; + position: fixed !important; + } + } + \ No newline at end of file diff --git a/dmp-frontend/src/app/ui/inapp-notification/listing/inapp-notification-listing.component.ts b/dmp-frontend/src/app/ui/inapp-notification/listing/inapp-notification-listing.component.ts new file mode 100644 index 000000000..6c8503151 --- /dev/null +++ b/dmp-frontend/src/app/ui/inapp-notification/listing/inapp-notification-listing.component.ts @@ -0,0 +1,115 @@ +import { Component, OnInit, TemplateRef, ViewChild, EventEmitter, Output, Input } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AuthService } from '@app/core/services/auth/auth.service'; +import { QueryParamsService } from '@app/core/services/utilities/query-params.service'; +import { BaseListingComponent } from '@common/base/base-listing-component'; +import { PipeService } from '@common/formatting/pipe.service'; +import { DataTableDateTimeFormatPipe } from '@common/formatting/pipes/date-time-format.pipe'; +import { HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service'; +import { ColumnsChangedEvent, PageLoadEvent } from '@common/modules/hybrid-listing/hybrid-listing.component'; +import { UiNotificationService } from '@app/core/services/notification/ui-notification-service'; +import { InAppNotification } from '@app/core/model/inapp-notification/inapp-notification.model'; +import { InAppNotificationLookup } from '@app/core/query/inapp-notification.lookup'; +import { InAppNotificationService } from '@app/core/services/inapp-notification/inapp-notification.service'; +import { IsActive } from '@app/core/common/enum/is-active.enum'; +import { nameof } from 'ts-simple-nameof'; +import { NotificationInAppTrackingTypePipe } from '@common/formatting/pipes/notification-inapp-tracking-type.pipe'; +import { NotificationInAppTracking } from '@app/core/common/enum/notification-inapp-tracking.enum'; +import { Observable } from 'rxjs'; +import { QueryResult } from '@common/model/query-result'; + +@Component({ + selector: 'app-inapp-notification-listing', + templateUrl: './inapp-notification-listing.component.html', + styleUrls: ['./inapp-notification-listing.component.scss'] +}) +export class InAppNotificationListingComponent extends BaseListingComponent implements OnInit { + @ViewChild('readColumnTemplate', { static: true }) readColumnTemplate: TemplateRef; + @Input() isPreviewList; + @Output() previewModeChange = new EventEmitter(); + + userSettingsKey = { key: 'InAppNotificationListingUserSettings' }; + public notificationInAppTrackingEnum = NotificationInAppTracking; + + constructor( + protected router: Router, + protected route: ActivatedRoute, + protected uiNotificationService: UiNotificationService, + protected httpErrorHandlingService: HttpErrorHandlingService, + protected queryParamsService: QueryParamsService, + private inappNotificationService: InAppNotificationService, + public authService: AuthService, + private pipeService: PipeService, + ) { + super(router, route, uiNotificationService, httpErrorHandlingService, queryParamsService); + // Lookup setup + // Default lookup values are defined in the user settings class. + this.lookup = this.initializeLookup(); + } + + ngOnInit() { + super.ngOnInit(); + } + + protected initializeLookup(): InAppNotificationLookup { + const lookup = new InAppNotificationLookup(); + lookup.metadata = { countAll: true }; + lookup.page = { offset: 10, size: 10 }; + + lookup.isActive = [IsActive.Active]; + lookup.order = { items: [nameof(x => x.updatedAt)] }; + lookup.project = { + fields: [ + nameof(x => x.id), + nameof(x => x.subject), + nameof(x => x.trackingState), + nameof(x => x.createdAt) + ] + }; + + return lookup; + } + + + protected setupColumns() { + this.gridColumns.push(...[{ + cellTemplate: this.readColumnTemplate, + alwaysShown: true + }, { + prop: nameof(x => x.subject), + sortable: true, + languageName: 'INAPP-NOTIFICATION-LISTING.FIELDS.SUBJECT' + }, { + prop: nameof(x => x.trackingState), + sortable: true, + languageName: 'INAPP-NOTIFICATION-LISTING.FIELDS.TRACKING-STATE', + pipe: this.pipeService.getPipe(NotificationInAppTrackingTypePipe) + }, { + prop: nameof(x => x.createdAt), + sortable: true, + languageName: 'INAPP-NOTIFICATION-LISTING.FIELDS.CREATED-AT', + pipe: this.pipeService.getPipe(DataTableDateTimeFormatPipe).withFormat('short') + }]); + } + + onColumnsChanged(event: ColumnsChangedEvent) { + // Here are defined the projection fields that always requested from the api. + this.lookup.project = { + fields: [ + nameof(x => x.id), + nameof(x => x.trackingState), + ...event.properties.filter(x => x).map(x => x.toString()) + ] + }; + this.onPageLoad({ offset: 0 } as PageLoadEvent); + } + + protected loadListing() : Observable> { + return this.inappNotificationService.query(this.lookup); + } + + togglePreviewMode(value: boolean) { + this.isPreviewList = value; + this.previewModeChange.emit(value); + } +} diff --git a/dmp-frontend/src/common/formatting/common-formatting.module.ts b/dmp-frontend/src/common/formatting/common-formatting.module.ts index a16d284a8..2c54e7751 100644 --- a/dmp-frontend/src/common/formatting/common-formatting.module.ts +++ b/dmp-frontend/src/common/formatting/common-formatting.module.ts @@ -9,6 +9,7 @@ import { ReferenceTypePipe } from './pipes/reference-type.pipe'; import { ReferenceSourceTypePipe } from './pipes/reference-source-type.pipe'; import { NotificationTemplateChannelPipe } from './pipes/notification-template-channel.pipe'; import { NotificationTemplateKindPipe } from './pipes/notification-template-kind.pipe'; +import { NotificationInAppTrackingTypePipe } from './pipes/notification-inapp-tracking-type.pipe'; // // @@ -29,7 +30,8 @@ import { NotificationTemplateKindPipe } from './pipes/notification-template-kind ReferenceTypePipe, ReferenceSourceTypePipe, NotificationTemplateChannelPipe, - NotificationTemplateKindPipe + NotificationTemplateKindPipe, + NotificationInAppTrackingTypePipe ], exports: [ DateFormatPipe, @@ -44,7 +46,8 @@ import { NotificationTemplateKindPipe } from './pipes/notification-template-kind ReferenceTypePipe, ReferenceSourceTypePipe, NotificationTemplateChannelPipe, - NotificationTemplateKindPipe + NotificationTemplateKindPipe, + NotificationInAppTrackingTypePipe ], providers: [ DateFormatPipe, @@ -59,7 +62,8 @@ import { NotificationTemplateKindPipe } from './pipes/notification-template-kind ReferenceTypePipe, ReferenceSourceTypePipe, NotificationTemplateChannelPipe, - NotificationTemplateKindPipe + NotificationTemplateKindPipe, + NotificationInAppTrackingTypePipe ] }) export class CommonFormattingModule { } diff --git a/dmp-frontend/src/common/formatting/pipes/notification-inapp-tracking-type.pipe.ts b/dmp-frontend/src/common/formatting/pipes/notification-inapp-tracking-type.pipe.ts new file mode 100644 index 000000000..413a7d6b6 --- /dev/null +++ b/dmp-frontend/src/common/formatting/pipes/notification-inapp-tracking-type.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; + +@Pipe({ name: 'NotificationInAppTrackingTypeFormat' }) +export class NotificationInAppTrackingTypePipe implements PipeTransform { + constructor(private enumUtils: EnumUtils) { } + + public transform(value): any { + return this.enumUtils.toNotificationInAppTrackingString(value); + } +}