add support for custom layout and duration for notifications

This commit is contained in:
Mads Rasmussen
2022-08-02 16:21:00 +02:00
parent 03016b4170
commit 592702df82
11 changed files with 238 additions and 46 deletions

View File

@@ -14,7 +14,8 @@
"lit": "^2.2.8",
"openapi-typescript-fetch": "^1.1.3",
"router-slot": "^1.5.5",
"rxjs": "^7.5.6"
"rxjs": "^7.5.6",
"uuid": "^8.3.2"
},
"devDependencies": {
"@babel/core": "^7.18.9",
@@ -26,6 +27,7 @@
"@storybook/web-components": "^6.5.9",
"@types/chai": "^4.3.1",
"@types/mocha": "^9.1.1",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.31.0",
"@web/dev-server-esbuild": "^0.3.1",
@@ -5447,6 +5449,12 @@
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==",
"dev": true
},
"node_modules/@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true
},
"node_modules/@types/webpack": {
"version": "4.41.32",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz",
@@ -22929,13 +22937,11 @@
}
},
"node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"dev": true,
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "bin/uuid"
"uuid": "dist/bin/uuid"
}
},
"node_modules/uuid-browser": {
@@ -23618,6 +23624,16 @@
"node": ">= 6"
}
},
"node_modules/webpack-log/node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"dev": true,
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/webpack-sources": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz",
@@ -28434,6 +28450,12 @@
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==",
"dev": true
},
"@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true
},
"@types/webpack": {
"version": "4.41.32",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz",
@@ -42112,10 +42134,9 @@
"dev": true
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"uuid-browser": {
"version": "3.1.0",
@@ -43006,6 +43027,14 @@
"requires": {
"ansi-colors": "^3.0.0",
"uuid": "^3.3.2"
},
"dependencies": {
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
}
}
},
"webpack-sources": {

View File

@@ -39,7 +39,8 @@
"lit": "^2.2.8",
"openapi-typescript-fetch": "^1.1.3",
"router-slot": "^1.5.5",
"rxjs": "^7.5.6"
"rxjs": "^7.5.6",
"uuid": "^8.3.2"
},
"devDependencies": {
"@babel/core": "^7.18.9",
@@ -51,6 +52,7 @@
"@storybook/web-components": "^6.5.9",
"@types/chai": "^4.3.1",
"@types/mocha": "^9.1.1",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.31.0",
"@web/dev-server-esbuild": "^0.3.1",

View File

@@ -3,7 +3,7 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { UmbContextProviderMixin } from '../core/context';
import { UmbNotificationService } from '../core/services/notification.service';
import { UmbNotificationService } from '../core/services/notification/notification.service';
import { UmbDataTypeStore } from '../core/stores/data-type.store';
import { UmbNodeStore } from '../core/stores/node.store';

View File

@@ -4,7 +4,8 @@ import { customElement, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { Subscription } from 'rxjs';
import { UmbContextConsumerMixin } from '../../core/context';
import { UmbNotificationService } from '../../core/services/notification.service';
import { UmbNotificationHandler } from '../../core/services/notification/notification-handler';
import { UmbNotificationService } from '../../core/services/notification/notification.service';
@customElement('umb-backoffice-notification-container')
export class UmbBackofficeNotificationContainer extends UmbContextConsumerMixin(LitElement) {
@@ -24,7 +25,7 @@ export class UmbBackofficeNotificationContainer extends UmbContextConsumerMixin(
];
@state()
private _notifications: any[] = [];
private _notifications: UmbNotificationHandler[] = [];
private _notificationService?: UmbNotificationService;
private _notificationSubscription?: Subscription;
@@ -41,13 +42,15 @@ export class UmbBackofficeNotificationContainer extends UmbContextConsumerMixin(
private _useNotifications() {
this._notificationSubscription?.unsubscribe();
this._notificationService?.notifications.subscribe((notifications: Array<any>) => {
this._notificationService?.notifications.subscribe((notifications: Array<UmbNotificationHandler>) => {
this._notifications = notifications;
});
// TODO: listen to close event and remove notification from store.
}
private _closedNotificationHandler(notificationHandler: UmbNotificationHandler) {
notificationHandler.close();
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._notificationSubscription?.unsubscribe();
@@ -55,12 +58,12 @@ export class UmbBackofficeNotificationContainer extends UmbContextConsumerMixin(
render() {
return html`
<uui-toast-notification-container auto-close="7000" bottom-up id="notifications">
<uui-toast-notification-container bottom-up id="notifications">
${repeat(
this._notifications,
(notification) => notification.key,
(notification) => html` <uui-toast-notification color="positive">
<uui-toast-notification-layout .headline=${notification.headline}> </uui-toast-notification-layout>
(notification: UmbNotificationHandler) => notification.key,
(notification) => html`<uui-toast-notification color="${notification.color}" .autoClose="${notification.duration}" @closed="${() => this._closedNotificationHandler(notification)}">
${notification.element}
</uui-toast-notification>`
)}
</uui-toast-notification-container>

View File

@@ -5,7 +5,7 @@ import { UmbContextConsumerMixin } from '../../core/context';
import { UmbNodeStore } from '../../core/stores/node.store';
import { map, Subscription } from 'rxjs';
import { DocumentNode } from '../../mocks/data/content.data';
import { UmbNotificationService } from '../../core/services/notification.service';
import { UmbNotificationService } from '../../core/services/notification/notification.service';
import { UmbExtensionManifest, UmbExtensionManifestEditorView, UmbExtensionRegistry } from '../../core/extension';
import { IRoutingInfo, RouterSlot } from 'router-slot';
@@ -13,6 +13,7 @@ import { IRoutingInfo, RouterSlot } from 'router-slot';
// TODO: Make this dynamic, use load-extensions method to loop over extensions for this node.
import '../editor-views/editor-view-node-edit.element';
import '../editor-views/editor-view-node-info.element';
import { UmbNotificationDefaultData } from '../../core/services/notification/layouts/default/notification-layout-default.element';
@customElement('umb-node-editor')
export class UmbNodeEditor extends UmbContextConsumerMixin(LitElement) {
@@ -156,7 +157,8 @@ export class UmbNodeEditor extends UmbContextConsumerMixin(LitElement) {
// TODO: What if store is not present, what if node is not loaded....
if (this._node) {
this._nodeStore?.save([this._node]).then(() => {
this._notificationService?.peek('Document saved');
const data: UmbNotificationDefaultData = { message: 'Document Saved' };
this._notificationService?.peek('positive', { data });
});
}
}

View File

@@ -1,7 +1,8 @@
import { html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { UmbContextConsumerMixin } from '../../core/context';
import { UmbNotificationService } from '../../core/services/notification.service';
import { UmbNotificationDefaultData } from '../../core/services/notification/layouts/default/notification-layout-default.element';
import { UmbNotificationService } from '../../core/services/notification/notification.service';
import type { UmbPropertyAction } from './property-action/property-action.model';
@customElement('umb-property-action-copy')
@@ -26,7 +27,8 @@ export default class UmbPropertyActionCopyElement extends UmbContextConsumerMixi
}
private _handleLabelClick () {
this._notificationService?.peek('Copied to clipboard');
const data: UmbNotificationDefaultData = { message: 'Copied to clipboard' };
this._notificationService?.peek('positive', { data });
// TODO: how do we want to close the menu? Testing an event based approach
this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }));
}

View File

@@ -1,7 +1,8 @@
import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { UmbContextConsumerMixin } from '../../core/context';
import { UmbNotificationService } from '../../core/services/notification.service';
import { UmbNotificationDefaultData } from '../../core/services/notification/layouts/default/notification-layout-default.element';
import { UmbNotificationService } from '../../core/services/notification/notification.service';
@customElement('umb-property-editor-context-example')
export default class UmbPropertyEditorContextExample extends UmbContextConsumerMixin(LitElement) {
@@ -15,7 +16,8 @@ export default class UmbPropertyEditorContextExample extends UmbContextConsumerM
});
}
private _onClick = () => {
this._notificationService?.peek('Hello from property editor');
const data: UmbNotificationDefaultData = { message: 'Hello from property editor' };
this._notificationService?.peek('positive', { data });
};
render() {

View File

@@ -1,18 +0,0 @@
import { BehaviorSubject, Observable } from 'rxjs';
type TempNotificationObject = {
headline: string;
key: string;
};
export class UmbNotificationService {
private _notifications: BehaviorSubject<Array<TempNotificationObject>> = new BehaviorSubject(
<Array<TempNotificationObject>>[]
);
public readonly notifications: Observable<Array<TempNotificationObject>> = this._notifications.asObservable();
// TODO: this is just a quick solution to get notifications in POC. (suppose to get much more complex data set for this, enabling description, actions and event custom elements).
peek(headline: string) {
this._notifications.next([...this._notifications.getValue(), { headline: headline, key: Date.now().toString() }]);
}
}

View File

@@ -0,0 +1,31 @@
import { html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { UmbNotificationHandler } from '../../notification-handler';
import { ifDefined } from 'lit-html/directives/if-defined.js';
export interface UmbNotificationDefaultData {
message: string;
headline?: string;
}
@customElement('umb-notification-layout-default')
export class UmbNotificationLayoutDefaultElement extends LitElement {
static styles = [
UUITextStyles
];
@property({ attribute: false })
notificationHandler!: UmbNotificationHandler;
@property({ type: Object })
data!: UmbNotificationDefaultData;
render() {
return html`
<uui-toast-notification-layout headline="${ifDefined(this.data.headline)}" class="uui-text">
<div id="message">${this.data.message}</div>
</uui-toast-notification-layout>
`;
}
}

View File

@@ -0,0 +1,69 @@
import { v4 as uuidv4 } from 'uuid';
import { UmbNotificationOptions, UmbNotificationData, UmbNotificationColor } from './notification.service';
/**
* @export
* @class UmbNotificationHandler
*/
export class UmbNotificationHandler {
private _closeResolver: any;
private _closePromise: Promise<any>;
private _elementName?: string;
private _data: UmbNotificationData;
private _defaultColor: UmbNotificationColor = 'default';
private _defaultDuration = 6000;
private _defaultLayout = 'umb-notification-layout-default';
public key: string;
public element: any;
public color: UmbNotificationColor;
public duration: number | null;
/**
* Creates an instance of UmbNotificationHandler.
* @param {UmbNotificationOptions} options
* @memberof UmbNotificationHandler
*/
constructor (options: UmbNotificationOptions<UmbNotificationData>) {
this.key = uuidv4();
this.color = options.color || this._defaultColor;
this.duration = options.duration !== undefined ? options.duration : this._defaultDuration;
this._elementName = options.elementName || this._defaultLayout;
this._data = options.data;
this._closePromise = new Promise(res => {
this._closeResolver = res;
});
this._createLayoutElement();
}
/**
* @private
* @memberof UmbNotificationHandler
*/
private _createLayoutElement () {
if (!this._elementName) return;
this.element = document.createElement(this._elementName);
this.element.data = this._data;
this.element.notificationHandler = this;
}
/**
* @param {...any} args
* @memberof UmbNotificationHandler
*/
public close (...args: any) {
this._closeResolver(...args);
}
/**
* @return {*}
* @memberof UmbNotificationHandler
*/
public onClose (): Promise<any> {
return this._closePromise;
}
}

View File

@@ -0,0 +1,70 @@
import { BehaviorSubject, Observable } from 'rxjs';
import { UmbNotificationHandler } from './notification-handler';
import './layouts/default/notification-layout-default.element';
export type UmbNotificationData = any;
/**
* @export
* @interface UmbNotificationOptions
* @template UmbNotificationData
*/
export interface UmbNotificationOptions<UmbNotificationData> {
color?: UmbNotificationColor;
duration?: number | null;
elementName?: string;
data?: UmbNotificationData;
}
export type UmbNotificationColor = '' | 'default' | 'positive' | 'warning' | 'danger';
export class UmbNotificationService {
private _notifications: BehaviorSubject<Array<UmbNotificationHandler>> = new BehaviorSubject(<Array<UmbNotificationHandler>>[]);
public readonly notifications: Observable<Array<UmbNotificationHandler>> = this._notifications.asObservable();
/**
* @private
* @param {UmbNotificationOptions<UmbNotificationData>} options
* @return {*} {UmbNotificationHandler}
* @memberof UmbNotificationService
*/
private _open (options: UmbNotificationOptions<UmbNotificationData>): UmbNotificationHandler {
const notificationHandler = new UmbNotificationHandler(options);
notificationHandler.onClose().then(() => this._close(notificationHandler.key));
this._notifications.next([...this._notifications.getValue(), notificationHandler]);
return notificationHandler;
}
/**
* @private
* @param {string} key
* @memberof UmbNotificationService
*/
private _close (key: string) {
this._notifications.next(this._notifications.getValue().filter(notification => notification.key !== key));
}
/**
* Opens a notification that automatically goes away after 6 sek.
* @param {UmbNotificationColor} color
* @param {UmbNotificationOptions<UmbNotificationData>} options
* @return {*}
* @memberof UmbNotificationService
*/
public peek(color: UmbNotificationColor, options: UmbNotificationOptions<UmbNotificationData>): UmbNotificationHandler {
return this._open({ ...options, color });
}
/**
* Opens a notification that stays on the screen until dismissed by the user or custom code
* @param {UmbNotificationColor} color
* @param {UmbNotificationOptions<UmbNotificationData>} options
* @return {*}
* @memberof UmbNotificationService
*/
public stay(color: UmbNotificationColor, options: UmbNotificationOptions<UmbNotificationData>): UmbNotificationHandler {
return this._open({ ...options, color, duration: null });
}
}