add inapp notification ui

This commit is contained in:
amentis 2024-01-08 18:32:07 +02:00
parent c36aff22e8
commit 3f85f1c9af
27 changed files with 1163 additions and 4 deletions

View File

@ -0,0 +1,304 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Simple Transactional Email</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%; }
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%; }
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%; }
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top; }
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%; }
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px; }
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 580px;
padding: 10px; }
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%; }
.wrapper {
box-sizing: border-box;
padding: 20px; }
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%; }
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center; }
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
Margin-bottom: 30px; }
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize; }
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
Margin-bottom: 15px; }
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px; }
a {
color: #3498db;
text-decoration: underline; }
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%; }
.btn > tbody > tr > td {
padding-bottom: 15px; }
.btn table {
width: auto; }
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center; }
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize; }
.btn-primary table td {
background-color: #3498db; }
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff; }
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0; }
.first {
margin-top: 0; }
.align-center {
text-align: center; }
.align-right {
text-align: right; }
.align-left {
text-align: left; }
.clear {
clear: both; }
.mt0 {
margin-top: 0; }
.mb0 {
margin-bottom: 0; }
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0; }
.powered-by a {
text-decoration: none; }
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0; }
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important; }
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important; }
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important; }
table[class=body] .content {
padding: 0 !important; }
table[class=body] .container {
padding: 0 !important;
width: 100% !important; }
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important; }
table[class=body] .btn table {
width: 100% !important; }
table[class=body] .btn a {
width: 100% !important; }
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important; }}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%; }
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%; }
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important; }
.btn-primary table td:hover {
background-color: #34495e !important; }
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important; } }
</style>
</head>
<body class="">
<table border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
<table class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p>Dear {recipient},</p>
<p>{reasonName} just add you to collaborate to Data Management plan {dmpname} with role {dmprole}.</p>
<p>Click the button to redirect to {dmpname}.</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
<tbody>
<tr>
<td align="left">
<table border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td> <a href="{installation-url}/plans/edit/{id}" target="_blank">Join</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer">
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -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', path: 'index-managment',
loadChildren: () => import('./ui/admin/index-managment/index-managment.module').then(m => m.IndexManagmentModule), loadChildren: () => import('./ui/admin/index-managment/index-managment.module').then(m => m.IndexManagmentModule),

View File

@ -0,0 +1,4 @@
export enum NotificationInAppTracking {
Stored = 0,
Delivered = 1
}

View File

@ -34,6 +34,7 @@ export enum AppPermission {
ViewTenantPage = 'ViewTenantPage', ViewTenantPage = 'ViewTenantPage',
ViewLanguagePage = "ViewLanguagePage", ViewLanguagePage = "ViewLanguagePage",
ViewNotificationTemplatePage = "ViewNotificationTemplatePage", ViewNotificationTemplatePage = "ViewNotificationTemplatePage",
ViewInAppNotificationPage = "ViewInAppNotificationPage",
//ReferenceType //ReferenceType
BrowseReferenceType = "BrowseReferenceType", BrowseReferenceType = "BrowseReferenceType",

View File

@ -44,6 +44,7 @@ import { UserService } from './services/user/user.service';
import { FileUtils } from './services/utilities/file-utils.service'; import { FileUtils } from './services/utilities/file-utils.service';
import { QueryParamsService } from './services/utilities/query-params.service'; import { QueryParamsService } from './services/utilities/query-params.service';
import { FileTransformerHttpService } from './services/file-transformer/file-transformer.http.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. // This is shared module that provides all the services. Its imported only once on the AppModule.
@ -106,7 +107,8 @@ export class CoreServiceModule {
TagService, TagService,
CanDeactivateGuard, CanDeactivateGuard,
FileTransformerService, FileTransformerService,
FileTransformerHttpService FileTransformerHttpService,
InAppNotificationService
], ],
}; };
} }

View File

@ -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;
}

View File

@ -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[];
}

View File

@ -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<QueryResult<InAppNotification>> {
const url = `${this.apiBase}/query`;
return this.http
.post<QueryResult<InAppNotification>>(url, q).pipe(
catchError((error: any) => throwError(error)));
}
getSingle(id: Guid, reqFields: string[] = []): Observable<InAppNotification> {
const url = `${this.apiBase}/${id}`;
const options = { params: { f: reqFields } };
return this.http
.get<InAppNotification>(url, options).pipe(
catchError((error: any) => throwError(error)));
}
read(id: Guid): Observable<void> {
const url = `${this.apiBase}/${id}/read`;
return this.http
.post<void>(url, {}).pipe(
catchError((error: any) => throwError(error)));
}
countUnread(): Observable<number> {
const url = `${this.apiBase}/count-unread`;
const params = new BaseHttpParams();
params.interceptorContext = {
excludedInterceptors: [InterceptorType.ProgressIndication]
};
const options = { params: params };
return this.http
.get<number>(url, options).pipe(
catchError((error: any) => throwError(error)));
}
delete(id: Guid): Observable<InAppNotification> {
const url = `${this.apiBase}/${id}`;
return this.http
.delete<InAppNotification>(url).pipe(
catchError((error: any) => throwError(error)));
}
}

View File

@ -27,6 +27,7 @@ import { DmpStatus } from '../../common/enum/dmp-status';
import { ValidationType } from '../../common/enum/validation-type'; import { ValidationType } from '../../common/enum/validation-type';
import { RecentActivityOrder } from '@app/core/common/enum/recent-activity-order'; import { RecentActivityOrder } from '@app/core/common/enum/recent-activity-order';
import { NotificationType } from '@app/core/common/enum/notification-type'; import { NotificationType } from '@app/core/common/enum/notification-type';
import { NotificationInAppTracking } from '@app/core/common/enum/notification-inapp-tracking.enum';
@Injectable() @Injectable()
export class EnumUtils { 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'); 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');
}
}
} }

View File

@ -0,0 +1,21 @@
<div *ngIf="inappNotification" class="row inapp-notification-editor">
<div class="col-md-10 offset-md-1 colums-gapped">
<div class="row align-items-center mb-4">
<div class="col-md-12">
<mat-card>
<mat-card-title>
<div [innerHTML]="inappNotification.subject"></div>
</mat-card-title>
<mat-card-content>
<div [innerHTML]="inappNotification.body"></div>
</mat-card-content>
</mat-card>
<div *ngIf="!isFromDialog" class="row editor-actions">
<div class="col"></div>
<div class="col-auto" *ngIf="!isDeleted && !isRead"><button mat-raised-button color="primary" (click)="markAsRead()" type="submit">{{'NOTIFICATION-SERVICE.INAPP-NOTIFICATION-EDITOR.ACTIONS.READ' | translate}}</button></div>
<div class="col-auto" *ngIf="!this.isDeleted && !isNew"><button mat-raised-button color="primary" type="button" (click)="delete()">{{'NOTIFICATION-SERVICE.INAPP-NOTIFICATION-EDITOR.ACTIONS.DELETE' | translate}}</button></div>
<div class="col-auto"><button mat-raised-button color="primary" (click)="cancel()" type="button">{{'NOTIFICATION-SERVICE.INAPP-NOTIFICATION-EDITOR.ACTIONS.CANCEL' | translate}}</button></div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,7 @@
.inapp-notification-editor {
padding-top: 1em;
.editor-actions {
margin-top: 30px;
}
}

View File

@ -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<InAppNotification>(x => x.id),
nameof<InAppNotification>(x => x.subject),
nameof<InAppNotification>(x => x.body),
nameof<InAppNotification>(x => x.type),
nameof<InAppNotification>(x => x.trackingState),
nameof<InAppNotification>(x => x.isActive),
nameof<InAppNotification>(x => x.hash),
nameof<InAppNotification>(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);
}
}

View File

@ -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 { }

View File

@ -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 { }

View File

@ -0,0 +1,13 @@
<mat-nav-list class="inapp-notification-listing-dialog">
<a *ngFor="let inappNotification of inappNotifications; last as last;" mat-list-item (click)="goToNotification(inappNotification)">
<mat-icon *ngIf="inappNotification.trackingState === notificationInAppTrackingEnum.Delivered" mat-list-icon>drafts</mat-icon>
<mat-icon *ngIf="inappNotification.trackingState === notificationInAppTrackingEnum.Stored" mat-list-icon>mail</mat-icon>
<span mat-line>{{ inappNotification.subject }}</span>
<span mat-line class="secondary-text">{{ inappNotification.createdAt | date : 'short'}}</span>
<mat-divider inset *ngIf="!last"></mat-divider>
</a>
<mat-list-item *ngIf="authService.hasPermission(authService.permissionEnum.ViewInAppNotificationPage)">
<a (click)="goToNotifications()">{{'APP.NAVIGATION.ALL-INAPP-NOTIFICATIONS'
| translate}}</a>
</mat-list-item>
</mat-nav-list>

View File

@ -0,0 +1,11 @@
.inapp-notification-listing-dialog {
padding-top: 0em;
.unread{
font-weight:bold
}
.secondary-text {
color: rgba(0, 0, 0, 0.54);
}
}

View File

@ -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<InAppNotification>();
public notificationInAppTrackingEnum = NotificationInAppTracking;
constructor(
public dialogRef: MatDialogRef<InAppNotificationListingDialogComponent>,
@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<InAppNotification>(x => x.id),
nameof<InAppNotification>(x => x.subject),
nameof<InAppNotification>(x => x.createdAt),
nameof<InAppNotification>(x => x.trackingState),
]
};
lookup.page = { offset: 0, size: 10 };
lookup.order = { items: ['-' + nameof<InAppNotification>(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();
}
}

View File

@ -0,0 +1,30 @@
<div class="inapp-notification-listing-filters">
<mat-expansion-panel class="expansion-panel" [(expanded)]="panelExpanded" [disabled]="true">
<mat-expansion-panel-header collapsedHeight="auto" expandedHeight="auto">
<div class="row w-100">
<app-text-filter class="col-auto" [(value)]="filter.like" (valueChange)="onFilterChange()"></app-text-filter>
<div class="filter-actions col-auto ml-auto">
<button mat-button color="primary" (click)="panelExpanded = !panelExpanded" [disableRipple]="true">
{{'NOTIFICATION-SERVICE.INAPP-NOTIFICATION-LISTING.FILTER.MORE-FILTERS' | translate}}
<mat-icon *ngIf="panelExpanded">keyboard_arrow_up</mat-icon>
<mat-icon *ngIf="!panelExpanded">keyboard_arrow_down</mat-icon>
</button>
</div>
</div>
</mat-expansion-panel-header>
<div class="row">
<mat-form-field class="col-md-3">
<mat-label>{{'NOTIFICATION-SERVICE.INAPP-NOTIFICATION-LISTING.FILTER.TRACKING-STATE' | translate}}</mat-label>
<mat-select [(ngModel)]="trackingState">
<mat-option *ngIf="trackingState !== null"></mat-option>
<mat-option *ngFor="let trackingState of trackingStates" [value]="trackingState">{{enumUtils.toNotificationInAppTrackingString(trackingState)}}</mat-option>
</mat-select>
</mat-form-field>
<div class="col-md-3 d-flex justify-content-center align-items-center">
<mat-slide-toggle [(ngModel)]="isActive" class="toggle col-auto w-100">
{{'NOTIFICATION-SERVICE.INAPP-NOTIFICATION-LISTING.FILTER.SHOW-INACTIVE' | translate}}
</mat-slide-toggle>
</div>
</div>
</mat-expansion-panel>
</div>

View File

@ -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;
}

View File

@ -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<InAppNotificationFilter>();
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);
}
}

View File

@ -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<InAppNotificationListingUserSettings>, UserSettingsLookupBuilder<InAppNotificationLookup> {
private like: string;
private isActive: IsActive[] = [IsActive.Active];
private order: Lookup.Ordering = { items: [nameof<InAppNotification>(x => x.createdAt)] };
private project: Lookup.FieldDirectives = {
fields: [
nameof<InAppNotification>(x => x.id),
nameof<InAppNotification>(x => x.subject),
nameof<InAppNotification>(x => x.trackingState),
nameof<InAppNotification>(x => x.createdAt)
]
};
static getUserSettingsInformation(): UserSettingsInformation<InAppNotificationListingUserSettings> {
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;
}
}

View File

@ -0,0 +1,46 @@
<div class="row inapp-notification-listing">
<div class="col-12">
<div class="row page-title">
<div class="col-auto pl-5 pr-0 align-self-center">
<i class="fa fa-2x fa-envelope-open-o accent-color"></i>
</div>
<!-- <div class="col-auto ml-auto mt-3 align-self-center">
<button mat-raised-button color="accent" *ngIf="authService.hasAnyPermission([authService.permissionEnum.EditDocument, authService.permissionEnum.DeferredAffiliation])" [routerLink]=" ['./new'] ">
<mat-icon class="mb-1 mr-2">add</mat-icon>{{'COMMONS.ACTIONS.NEW' | translate}}
</button>
</div> -->
<div class="col-auto p-0 user-settings-selector">
<app-user-settings-selector [key]="userSettingsKey" [lookup]="lookup" (onSettingSelected)="changeSetting($event)" [autoSelectUserSettings]="autoSelectUserSettings"></app-user-settings-selector>
</div>
</div>
<div class="row toggle-row">
<div class="col-auto align-self-center view-mode-toggle-container">
<button mat-icon-button class="preview-btn" [ngClass]="{'preview-btn-active': isPreviewList}" (click)="togglePreviewMode(true)" matTooltip="{{'APP.DOCUMENT-LISTING.PREVIEW.LIST' | translate}}">
<mat-icon>view_module</mat-icon>
</button>
<button mat-icon-button class="preview-btn" [ngClass]="{'preview-btn-active': !isPreviewList}" (click)="togglePreviewMode(false)" matTooltip="{{'APP.DOCUMENT-LISTING.PREVIEW.TABLE' | translate}}">
<mat-icon>table_chart</mat-icon>
</button>
</div>
</div>
<div class="row">
<div class="col-12 filter-row">
<app-inapp-notification-listing-filters [(filter)]="lookup" (filterChange)="filterChanged($event)"></app-inapp-notification-listing-filters>
</div>
</div>
<div class="row">
<div class="col-md-12 col-center-align">
<app-hybrid-listing [rows]="gridRows" [columns]="gridColumns" [visibleColumns]="visibleColumns" [count]="totalElements" [offset]="currentPageNumber" [limit]="lookup.page.size" [defaultSort]="lookup.order?.items" [externalSorting]="true" (rowActivated)="onRowActivated($event)" (pageLoad)="onPageLoad($event)" (columnSort)="onColumnSort($event)" (columnsChanged)="onColumnsChanged($event)">
</app-hybrid-listing>
<ng-template #readColumnTemplate ngx-datatable-cell-template let-row="row">
<mat-icon *ngIf="row.trackingState === notificationInAppTrackingEnum.Delivered" mat-list-icon>drafts</mat-icon>
<mat-icon *ngIf="row.trackingState === notificationInAppTrackingEnum.Stored" mat-list-icon>mail</mat-icon>
</ng-template>
</div>
</div>
</div>
</div>

View File

@ -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;
}
}

View File

@ -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<InAppNotification, InAppNotificationLookup> implements OnInit {
@ViewChild('readColumnTemplate', { static: true }) readColumnTemplate: TemplateRef<any>;
@Input() isPreviewList;
@Output() previewModeChange = new EventEmitter<boolean>();
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<InAppNotification>(x => x.updatedAt)] };
lookup.project = {
fields: [
nameof<InAppNotification>(x => x.id),
nameof<InAppNotification>(x => x.subject),
nameof<InAppNotification>(x => x.trackingState),
nameof<InAppNotification>(x => x.createdAt)
]
};
return lookup;
}
protected setupColumns() {
this.gridColumns.push(...[{
cellTemplate: this.readColumnTemplate,
alwaysShown: true
}, {
prop: nameof<InAppNotification>(x => x.subject),
sortable: true,
languageName: 'INAPP-NOTIFICATION-LISTING.FIELDS.SUBJECT'
}, {
prop: nameof<InAppNotification>(x => x.trackingState),
sortable: true,
languageName: 'INAPP-NOTIFICATION-LISTING.FIELDS.TRACKING-STATE',
pipe: this.pipeService.getPipe<NotificationInAppTrackingTypePipe>(NotificationInAppTrackingTypePipe)
}, {
prop: nameof<InAppNotification>(x => x.createdAt),
sortable: true,
languageName: 'INAPP-NOTIFICATION-LISTING.FIELDS.CREATED-AT',
pipe: this.pipeService.getPipe<DataTableDateTimeFormatPipe>(DataTableDateTimeFormatPipe).withFormat('short')
}]);
}
onColumnsChanged(event: ColumnsChangedEvent) {
// Here are defined the projection fields that always requested from the api.
this.lookup.project = {
fields: [
nameof<InAppNotification>(x => x.id),
nameof<InAppNotification>(x => x.trackingState),
...event.properties.filter(x => x).map(x => x.toString())
]
};
this.onPageLoad({ offset: 0 } as PageLoadEvent);
}
protected loadListing() : Observable<QueryResult<InAppNotification>> {
return this.inappNotificationService.query(this.lookup);
}
togglePreviewMode(value: boolean) {
this.isPreviewList = value;
this.previewModeChange.emit(value);
}
}

View File

@ -9,6 +9,7 @@ import { ReferenceTypePipe } from './pipes/reference-type.pipe';
import { ReferenceSourceTypePipe } from './pipes/reference-source-type.pipe'; import { ReferenceSourceTypePipe } from './pipes/reference-source-type.pipe';
import { NotificationTemplateChannelPipe } from './pipes/notification-template-channel.pipe'; import { NotificationTemplateChannelPipe } from './pipes/notification-template-channel.pipe';
import { NotificationTemplateKindPipe } from './pipes/notification-template-kind.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, ReferenceTypePipe,
ReferenceSourceTypePipe, ReferenceSourceTypePipe,
NotificationTemplateChannelPipe, NotificationTemplateChannelPipe,
NotificationTemplateKindPipe NotificationTemplateKindPipe,
NotificationInAppTrackingTypePipe
], ],
exports: [ exports: [
DateFormatPipe, DateFormatPipe,
@ -44,7 +46,8 @@ import { NotificationTemplateKindPipe } from './pipes/notification-template-kind
ReferenceTypePipe, ReferenceTypePipe,
ReferenceSourceTypePipe, ReferenceSourceTypePipe,
NotificationTemplateChannelPipe, NotificationTemplateChannelPipe,
NotificationTemplateKindPipe NotificationTemplateKindPipe,
NotificationInAppTrackingTypePipe
], ],
providers: [ providers: [
DateFormatPipe, DateFormatPipe,
@ -59,7 +62,8 @@ import { NotificationTemplateKindPipe } from './pipes/notification-template-kind
ReferenceTypePipe, ReferenceTypePipe,
ReferenceSourceTypePipe, ReferenceSourceTypePipe,
NotificationTemplateChannelPipe, NotificationTemplateChannelPipe,
NotificationTemplateKindPipe NotificationTemplateKindPipe,
NotificationInAppTrackingTypePipe
] ]
}) })
export class CommonFormattingModule { } export class CommonFormattingModule { }

View File

@ -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);
}
}