mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[AAE-4529] Refactor Notification History Component (#6620)
* [AAE-4529] Refactor Notification History Component * Improve code * Add maxNotifications * More changes * Add documentation * Rebase branch * Fix build * Update notification-history.component.md * Fix e2e tests Co-authored-by: Eugenio Romano <eromano@users.noreply.github.com>
This commit is contained in:
@@ -1,34 +1,73 @@
|
||||
<div (keyup)="onKeyPress($event)">
|
||||
<button mat-button [matMenuTriggerFor]="menu" class="adf-notification-history-menu_button"
|
||||
id="adf-notification-history-open-button">
|
||||
<mat-icon>mail_outline</mat-icon>
|
||||
<button mat-button
|
||||
[matMenuTriggerFor]="menu"
|
||||
class="adf-notification-history-menu_button"
|
||||
id="adf-notification-history-open-button"
|
||||
(menuOpened)="onMenuOpened()">
|
||||
<mat-icon matBadge="⁠"
|
||||
[matBadgeHidden]="!notifications.length"
|
||||
matBadgeColor="accent"
|
||||
matBadgeSize="small">notifications</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu" [xPosition]="menuPositionX" [yPosition]="menuPositionY"
|
||||
[overlapTrigger]="false" id="adf-notification-history-menu" class="adf-notification-history-menu">
|
||||
<mat-menu #menu="matMenu"
|
||||
[xPosition]="menuPositionX"
|
||||
[yPosition]="menuPositionY"
|
||||
id="adf-notification-history-menu"
|
||||
class="adf-notification-history-menu">
|
||||
|
||||
<div class="adf-notification-history-list"
|
||||
(click)="$event.stopPropagation()">
|
||||
<div mat-subheader>
|
||||
<span>{{ 'NOTIFICATIONS.TITLE' | translate }}</span>
|
||||
<button (click)="markAsRead()"
|
||||
id="adf-notification-history-mark-as-read"
|
||||
mat-button
|
||||
color="accent"
|
||||
*ngIf="notifications.length">
|
||||
{{ 'NOTIFICATIONS.MARK_AS_READ' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="adf-notification-history-list">
|
||||
<mat-list>
|
||||
<mat-list-item>
|
||||
<h6 mat-line>{{ 'NOTIFICATIONS.TITLE' | translate }}</h6>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<mat-list>
|
||||
<mat-list-item *ngFor="let notification of notifications">
|
||||
<mat-icon mat-list-icon>{{ notification | noticicationIcon }}</mat-icon>
|
||||
<h4 *ngFor="let message of notification.messages" mat-line>{{ message }}</h4>
|
||||
<p mat-line> {{notification.datetime | date}} </p>
|
||||
<p mat-line> {{notification.initiator.displayName | translate}} </p>
|
||||
</mat-list-item>
|
||||
<mat-list-item *ngIf="isEmptyNotification()" id="adf-notification-history-component-no-message">
|
||||
<h4 mat-line>{{ 'NOTIFICATIONS.NO_MESSAGE' | translate }}</h4>
|
||||
</mat-list-item>
|
||||
<mat-action-list *ngIf="!isEmptyNotification()" id="adf-notification-history-mark-as-read">
|
||||
<button mat-list-item (click)="markAsRead()">{{ 'NOTIFICATIONS.MARK_AS_READ' | translate }}
|
||||
</button>
|
||||
</mat-action-list>
|
||||
<ng-container *ngIf="notifications.length; else empty_list_template">
|
||||
<mat-list-item *ngFor="let notification of paginatedNotifications"
|
||||
class="adf-notification-history-menu-item"
|
||||
(click)="onNotificationClick(notification)">
|
||||
<div *ngIf="notification.initiator; else no_avatar"
|
||||
matListAvatar
|
||||
[outerHTML]="notification.initiator | usernameInitials:'adf-notification-initiator-pic'">
|
||||
</div>
|
||||
<ng-template #no_avatar>
|
||||
<mat-icon mat-list-icon
|
||||
class="adf-notification-history-menu-initiator">{{ notification | notificationIcon
|
||||
}}</mat-icon>
|
||||
</ng-template>
|
||||
<p class="adf-notification-history-menu-message"
|
||||
*ngFor="let message of notification.messages"
|
||||
mat-line>{{ message }}</p>
|
||||
<p class="adf-notification-history-menu-date"
|
||||
mat-line> {{notification.datetime | adfTimeAgo}} </p>
|
||||
</mat-list-item>
|
||||
</ng-container>
|
||||
<ng-template #empty_list_template>
|
||||
<mat-list-item id="adf-notification-history-component-no-message"
|
||||
class="adf-notification-history-menu-no-message">
|
||||
<h4 mat-line>{{ 'NOTIFICATIONS.NO_MESSAGE' | translate }}</h4>
|
||||
</mat-list-item>
|
||||
</ng-template>
|
||||
</mat-list>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div class="adf-notification-history-load-more"
|
||||
*ngIf="hasMoreNotifications()">
|
||||
<button mat-button
|
||||
(click)="loadMore()">
|
||||
{{ 'NOTIFICATIONS.LOAD_MORE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-menu>
|
||||
</div>
|
||||
|
@@ -1,23 +1,76 @@
|
||||
.adf {
|
||||
@mixin adf-notification-history-theme($theme) {
|
||||
$background: map-get($theme, background);
|
||||
$accent: map-get($theme, accent);
|
||||
|
||||
&-notification-history-menu_button.mat-button {
|
||||
margin-right: 0;
|
||||
border-radius: 90%;
|
||||
padding: 0;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
.adf {
|
||||
&-notification-history-menu_button.mat-button {
|
||||
margin-right: 0;
|
||||
border-radius: 90%;
|
||||
padding: 0;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
&-notification-history-list .mat-subheader {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-notification-history-menu {
|
||||
&-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-item:focus {
|
||||
outline: none;
|
||||
background: mat-color($background, 'hover');
|
||||
}
|
||||
|
||||
&-item:hover {
|
||||
background-color: mat-color($background, 'hover');
|
||||
}
|
||||
|
||||
&-message, &-no-message {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
&-date {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
&-initiator {
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&-notification-initiator-pic {
|
||||
min-width: 40px;
|
||||
background: mat-color($accent);
|
||||
display: inline-block;
|
||||
height: 40px;
|
||||
border-radius: 100px;
|
||||
text-align: center;
|
||||
font-weight: bolder;
|
||||
font-size: 18px;
|
||||
text-transform: uppercase;
|
||||
vertical-align: middle;
|
||||
line-height: 40px;
|
||||
color: mat-color($mat-grey, 50);
|
||||
}
|
||||
|
||||
&-notification-history-load-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-device-width: 480px) {
|
||||
.mat-menu-panel.adf-notification-history-menu {
|
||||
max-height: 450px;
|
||||
min-width: 450px;
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
min-width: 320px;
|
||||
max-height: 500px;
|
||||
|
||||
.mat-menu-content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-menu-panel.adf-notification-history-menu .mat-menu-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
@@ -23,10 +23,12 @@ import { OverlayContainer } from '@angular/cdk/overlay';
|
||||
import { NotificationService } from '../services/notification.service';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NotificationModel, NOTIFICATION_TYPE } from '../models/notification.model';
|
||||
|
||||
describe('Notification History Component', () => {
|
||||
|
||||
let fixture: ComponentFixture<NotificationHistoryComponent>;
|
||||
let component: NotificationHistoryComponent;
|
||||
let element: HTMLElement;
|
||||
let notificationService: NotificationService;
|
||||
let overlayContainerElement: HTMLElement;
|
||||
@@ -48,10 +50,12 @@ describe('Notification History Component', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(NotificationHistoryComponent);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.nativeElement;
|
||||
|
||||
storage = TestBed.inject(StorageService);
|
||||
notificationService = TestBed.inject(NotificationService);
|
||||
component.notifications = [];
|
||||
}));
|
||||
|
||||
beforeEach(inject([OverlayContainer], (oc: OverlayContainer) => {
|
||||
@@ -59,6 +63,7 @@ describe('Notification History Component', () => {
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
storage.removeItem('notifications');
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
@@ -73,27 +78,110 @@ describe('Notification History Component', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should message be present and empty message not be present when there are notifications in the history', (done) => {
|
||||
it('should remove notification when mark all as read is clicked', (done) => {
|
||||
fixture.detectChanges();
|
||||
notificationService.showInfo('Example Message Removed');
|
||||
openNotification();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.notifications.length).toBe(1);
|
||||
const markAllAsRead = <HTMLButtonElement> overlayContainerElement.querySelector('#adf-notification-history-mark-as-read');
|
||||
markAllAsRead.click();
|
||||
fixture.detectChanges();
|
||||
expect(storage.getItem('notifications')).toBeNull();
|
||||
expect(component.notifications.length).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should message be present and empty message not be present when there are notifications in the history', (done) => {
|
||||
fixture.detectChanges();
|
||||
notificationService.showInfo('Example Message');
|
||||
openNotification();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(overlayContainerElement.querySelector('#adf-notification-history-component-no-message')).toBeNull();
|
||||
expect(overlayContainerElement.querySelector('#adf-notification-history-list').innerHTML).toContain('Example Message');
|
||||
expect(overlayContainerElement.querySelector('.adf-notification-history-list').innerHTML).toContain('Example Message');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove notification from storage when mark all as read', (done) => {
|
||||
it('should show message when pushed directly to Notification History', (done) => {
|
||||
const callBackSpy = jasmine.createSpy('callBack');
|
||||
fixture.detectChanges();
|
||||
notificationService.pushToNotificationHistory(
|
||||
{
|
||||
clickCallBack: callBackSpy,
|
||||
messages: ['My new message'],
|
||||
datetime: new Date(),
|
||||
type: NOTIFICATION_TYPE.RECURSIVE
|
||||
|
||||
} as NotificationModel
|
||||
);
|
||||
openNotification();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
const markAllAsRead = <HTMLButtonElement> overlayContainerElement.querySelector('#adf-notification-history-mark-as-read button');
|
||||
markAllAsRead.click();
|
||||
fixture.detectChanges();
|
||||
expect(storage.getItem('notifications')).toBeNull();
|
||||
const notification = <HTMLButtonElement> overlayContainerElement.querySelector('.adf-notification-history-menu-item');
|
||||
notification.click();
|
||||
expect(callBackSpy).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show load more button when there are more notifications', (done) => {
|
||||
fixture.detectChanges();
|
||||
notificationService.showInfo('Example Message 1');
|
||||
notificationService.showInfo('Example Message 2');
|
||||
notificationService.showInfo('Example Message 3');
|
||||
notificationService.showInfo('Example Message 4');
|
||||
notificationService.showInfo('Example Message 5');
|
||||
notificationService.showInfo('Example Message 6');
|
||||
openNotification();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
const loadMoreButton = <HTMLButtonElement> overlayContainerElement.querySelector('.adf-notification-history-load-more');
|
||||
expect(component.paginatedNotifications.length).toBe(5);
|
||||
expect(loadMoreButton).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should read notifications from local storage', (done) => {
|
||||
storage.setItem('notifications', JSON.stringify([{
|
||||
messages: ['My new message'],
|
||||
datetime: new Date(),
|
||||
type: NOTIFICATION_TYPE.RECURSIVE
|
||||
|
||||
} as NotificationModel]));
|
||||
fixture.detectChanges();
|
||||
openNotification();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
const notification = <HTMLButtonElement> overlayContainerElement.querySelector('.adf-notification-history-menu-item');
|
||||
expect(notification).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to change the maximum number of notifications displayed', (done) => {
|
||||
component.maxNotifications = 10;
|
||||
fixture.detectChanges();
|
||||
notificationService.showInfo('Example Message 1');
|
||||
notificationService.showInfo('Example Message 2');
|
||||
notificationService.showInfo('Example Message 3');
|
||||
notificationService.showInfo('Example Message 4');
|
||||
notificationService.showInfo('Example Message 5');
|
||||
notificationService.showInfo('Example Message 6');
|
||||
openNotification();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
const notifications = overlayContainerElement.querySelectorAll('.adf-notification-history-menu-item');
|
||||
expect(notifications.length).toBe(6);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -15,26 +15,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, Input, ViewChild, OnDestroy } from '@angular/core';
|
||||
import { Component, Input, ViewChild, OnDestroy, OnInit, AfterViewInit, ChangeDetectorRef } from '@angular/core';
|
||||
import { NotificationService } from '../services/notification.service';
|
||||
import { NotificationModel } from '../models/notification.model';
|
||||
import { NotificationModel, NOTIFICATION_TYPE } from '../models/notification.model';
|
||||
import { MatMenuTrigger } from '@angular/material/menu';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { Subject } from 'rxjs';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { Pagination } from '@alfresco/js-api';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-notification-history',
|
||||
styleUrls: ['notification-history.component.scss'],
|
||||
templateUrl: 'notification-history.component.html'
|
||||
})
|
||||
export class NotificationHistoryComponent implements OnDestroy {
|
||||
export class NotificationHistoryComponent implements OnDestroy, OnInit, AfterViewInit {
|
||||
|
||||
onDestroy$ = new Subject<boolean>();
|
||||
|
||||
notifications: NotificationModel[] = [];
|
||||
|
||||
MAX_NOTIFICATION_STACK_LENGTH = 100;
|
||||
public static MAX_NOTIFICATION_STACK_LENGTH = 100;
|
||||
|
||||
@ViewChild(MatMenuTrigger, { static: true })
|
||||
trigger: MatMenuTrigger;
|
||||
@@ -47,33 +44,33 @@ export class NotificationHistoryComponent implements OnDestroy {
|
||||
@Input()
|
||||
menuPositionY: string = 'below';
|
||||
|
||||
/** Maximum number of notifications to display. The rest will remain hidden until load more is clicked */
|
||||
@Input()
|
||||
maxNotifications: number = 5;
|
||||
|
||||
onDestroy$ = new Subject<boolean>();
|
||||
notifications: NotificationModel[] = [];
|
||||
paginatedNotifications = [];
|
||||
pagination: Pagination;
|
||||
|
||||
constructor(
|
||||
private notificationService: NotificationService, public storageService: StorageService) {
|
||||
this.notifications = JSON.parse(storageService.getItem('notifications')) || [];
|
||||
private notificationService: NotificationService,
|
||||
public storageService: StorageService,
|
||||
public cd: ChangeDetectorRef) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.notifications = JSON.parse(this.storageService.getItem('notifications')) || [];
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.notificationService.notifications$
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe((message) => {
|
||||
this.notifications.push(message);
|
||||
|
||||
if (this.notifications.length > this.MAX_NOTIFICATION_STACK_LENGTH) {
|
||||
this.notifications.shift();
|
||||
}
|
||||
|
||||
storageService.setItem('notifications', JSON.stringify(this.notifications));
|
||||
});
|
||||
}
|
||||
|
||||
isEmptyNotification(): boolean {
|
||||
return (!this.notifications || this.notifications.length === 0);
|
||||
}
|
||||
|
||||
onKeyPress(event: KeyboardEvent) {
|
||||
this.closeUserModal(event);
|
||||
}
|
||||
|
||||
markAsRead() {
|
||||
this.storageService.removeItem('notifications');
|
||||
this.notifications = [];
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe((notification: NotificationModel) => {
|
||||
this.addNewNotification(notification);
|
||||
this.cd.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -81,9 +78,69 @@ export class NotificationHistoryComponent implements OnDestroy {
|
||||
this.onDestroy$.complete();
|
||||
}
|
||||
|
||||
addNewNotification(notification: NotificationModel) {
|
||||
this.notifications.unshift(notification);
|
||||
|
||||
if (this.notifications.length > NotificationHistoryComponent.MAX_NOTIFICATION_STACK_LENGTH) {
|
||||
this.notifications.shift();
|
||||
}
|
||||
|
||||
this.saveNotifications();
|
||||
this.createPagination();
|
||||
}
|
||||
|
||||
saveNotifications() {
|
||||
this.storageService.setItem('notifications', JSON.stringify(this.notifications.filter((notification) =>
|
||||
notification.type !== NOTIFICATION_TYPE.RECURSIVE
|
||||
)));
|
||||
}
|
||||
|
||||
onMenuOpened() {
|
||||
this.createPagination();
|
||||
}
|
||||
|
||||
onKeyPress(event: KeyboardEvent) {
|
||||
this.closeUserModal(event);
|
||||
}
|
||||
|
||||
private closeUserModal($event: KeyboardEvent) {
|
||||
if ($event.keyCode === 27) {
|
||||
this.trigger.closeMenu();
|
||||
}
|
||||
}
|
||||
|
||||
markAsRead() {
|
||||
this.notifications = [];
|
||||
this.paginatedNotifications = [];
|
||||
this.storageService.removeItem('notifications');
|
||||
this.createPagination();
|
||||
this.trigger.closeMenu();
|
||||
}
|
||||
|
||||
createPagination() {
|
||||
this.pagination = {
|
||||
skipCount: this.maxNotifications,
|
||||
maxItems: this.maxNotifications,
|
||||
totalItems: this.notifications.length,
|
||||
hasMoreItems: this.notifications.length > this.maxNotifications
|
||||
};
|
||||
this.paginatedNotifications = this.notifications.slice(0, this.pagination.skipCount);
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
this.pagination.skipCount = this.pagination.maxItems + this.pagination.skipCount;
|
||||
this.pagination.hasMoreItems = this.notifications.length > this.pagination.skipCount;
|
||||
this.paginatedNotifications = this.notifications.slice(0, this.pagination.skipCount);
|
||||
}
|
||||
|
||||
hasMoreNotifications(): boolean {
|
||||
return this.pagination?.hasMoreItems;
|
||||
}
|
||||
|
||||
onNotificationClick(notification: NotificationModel) {
|
||||
if (notification.clickCallBack) {
|
||||
notification.clickCallBack(notification.args);
|
||||
this.trigger.closeMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user