diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 198be6d4b1..db328429f4 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -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": { diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 4bda01037f..7c03be2d48 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -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", diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts index 957af35b5f..a30e40e717 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -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'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/components/backoffice-notification-container.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/components/backoffice-notification-container.element.ts index 31f51102ea..b8ac130310 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/components/backoffice-notification-container.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/components/backoffice-notification-container.element.ts @@ -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) => { + this._notificationService?.notifications.subscribe((notifications: Array) => { 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` - + ${repeat( this._notifications, - (notification) => notification.key, - (notification) => html` - + (notification: UmbNotificationHandler) => notification.key, + (notification) => html` + ${notification.element} ` )} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/components/node-editor.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/components/node-editor.element.ts index 90269bcfeb..c9f105489e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/components/node-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/components/node-editor.element.ts @@ -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 }); }); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/property-actions/property-action-copy.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/property-actions/property-action-copy.element.ts index 9648d93be2..dff163dfff 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/property-actions/property-action-copy.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/property-actions/property-action-copy.element.ts @@ -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 })); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/property-editors/property-editor-context-example.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/property-editors/property-editor-context-example.element.ts index 7a5689cbd9..d893fd0ddd 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/property-editors/property-editor-context-example.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/property-editors/property-editor-context-example.element.ts @@ -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() { diff --git a/src/Umbraco.Web.UI.Client/src/core/services/notification.service.ts b/src/Umbraco.Web.UI.Client/src/core/services/notification.service.ts deleted file mode 100644 index ee4373500f..0000000000 --- a/src/Umbraco.Web.UI.Client/src/core/services/notification.service.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { BehaviorSubject, Observable } from 'rxjs'; - -type TempNotificationObject = { - headline: string; - key: string; -}; - -export class UmbNotificationService { - private _notifications: BehaviorSubject> = new BehaviorSubject( - >[] - ); - public readonly notifications: Observable> = 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() }]); - } -} diff --git a/src/Umbraco.Web.UI.Client/src/core/services/notification/layouts/default/notification-layout-default.element.ts b/src/Umbraco.Web.UI.Client/src/core/services/notification/layouts/default/notification-layout-default.element.ts new file mode 100644 index 0000000000..dfee616771 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/services/notification/layouts/default/notification-layout-default.element.ts @@ -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` + +
${this.data.message}
+
+ `; + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/services/notification/notification-handler.ts b/src/Umbraco.Web.UI.Client/src/core/services/notification/notification-handler.ts new file mode 100644 index 0000000000..fafb3da629 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/services/notification/notification-handler.ts @@ -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; + 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) { + 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 { + return this._closePromise; + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/services/notification/notification.service.ts b/src/Umbraco.Web.UI.Client/src/core/services/notification/notification.service.ts new file mode 100644 index 0000000000..18eaf0895f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/services/notification/notification.service.ts @@ -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 { + color?: UmbNotificationColor; + duration?: number | null; + elementName?: string; + data?: UmbNotificationData; +} + +export type UmbNotificationColor = '' | 'default' | 'positive' | 'warning' | 'danger'; + +export class UmbNotificationService { + private _notifications: BehaviorSubject> = new BehaviorSubject(>[]); + public readonly notifications: Observable> = this._notifications.asObservable(); + + /** + * @private + * @param {UmbNotificationOptions} options + * @return {*} {UmbNotificationHandler} + * @memberof UmbNotificationService + */ + private _open (options: UmbNotificationOptions): 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} options + * @return {*} + * @memberof UmbNotificationService + */ + public peek(color: UmbNotificationColor, options: UmbNotificationOptions): 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} options + * @return {*} + * @memberof UmbNotificationService + */ + public stay(color: UmbNotificationColor, options: UmbNotificationOptions): UmbNotificationHandler { + return this._open({ ...options, color, duration: null }); + } +}