Merge branch 'main' into feature/content-picker
This commit is contained in:
@@ -2,6 +2,9 @@ module.exports = {
|
||||
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-a11y'],
|
||||
framework: '@storybook/web-components',
|
||||
features: {
|
||||
previewMdx2: true,
|
||||
},
|
||||
core: {
|
||||
builder: '@storybook/builder-vite',
|
||||
},
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
body {
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
background-color: var(--uui-color-background);
|
||||
padding: 1.5em;
|
||||
border-radius: 3px;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
4302
src/Umbraco.Web.UI.Client/package-lock.json
generated
4302
src/Umbraco.Web.UI.Client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,7 @@
|
||||
"openapi-typescript-fetch": "^1.1.3",
|
||||
"router-slot": "^1.5.5",
|
||||
"rxjs": "^7.5.6",
|
||||
"uuid": "^8.3.2"
|
||||
"@umbraco-ui/uui-modal": "file:umbraco-ui-uui-modal-0.0.0.tgz",
|
||||
"@umbraco-ui/uui-modal-container": "file:umbraco-ui-uui-modal-container-0.0.0.tgz",
|
||||
"@umbraco-ui/uui-modal-dialog": "file:umbraco-ui-uui-modal-dialog-0.0.0.tgz",
|
||||
@@ -47,15 +48,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.10",
|
||||
"@mdx-js/react": "^2.1.2",
|
||||
"@open-wc/testing": "^3.1.6",
|
||||
"@storybook/addon-a11y": "^6.5.9",
|
||||
"@storybook/addon-actions": "^6.5.9",
|
||||
"@storybook/addon-essentials": "^6.5.9",
|
||||
"@storybook/addon-links": "^6.5.9",
|
||||
"@storybook/builder-vite": "^0.2.2",
|
||||
"@storybook/mdx2-csf": "^0.0.3",
|
||||
"@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.32.0",
|
||||
"@typescript-eslint/parser": "^5.32.0",
|
||||
"@web/dev-server-esbuild": "^0.3.1",
|
||||
|
||||
@@ -3,8 +3,8 @@ 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';
|
||||
import { UmbModalService } from '../core/services/modal/modal.service';
|
||||
import { UmbNotificationService } from '../core/services/notification.service';
|
||||
import { UmbDataTypeStore } from '../core/stores/data-type.store';
|
||||
import { UmbNodeStore } from '../core/stores/node.store';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 type { UmbNotificationService, UmbNotificationHandler } from '../../core/services/notification';
|
||||
|
||||
@customElement('umb-backoffice-notification-container')
|
||||
export class UmbBackofficeNotificationContainer extends UmbContextConsumerMixin(LitElement) {
|
||||
@@ -24,7 +24,7 @@ export class UmbBackofficeNotificationContainer extends UmbContextConsumerMixin(
|
||||
];
|
||||
|
||||
@state()
|
||||
private _notifications: any[] = [];
|
||||
private _notifications: UmbNotificationHandler[] = [];
|
||||
|
||||
private _notificationService?: UmbNotificationService;
|
||||
private _notificationSubscription?: Subscription;
|
||||
@@ -41,11 +41,13 @@ 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 {
|
||||
@@ -55,12 +57,15 @@ 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>
|
||||
|
||||
@@ -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 type { UmbNotificationService } from '../../core/services/notification';
|
||||
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';
|
||||
|
||||
@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 });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 type { UmbNotificationDefaultData } from '../../core/services/notification/layouts/default';
|
||||
import type { UmbNotificationService } from '../../core/services/notification';
|
||||
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 }));
|
||||
}
|
||||
|
||||
@@ -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 type { UmbNotificationDefaultData } from '../../core/services/notification/layouts/default';
|
||||
import type { UmbNotificationService } from '../../core/services/notification';
|
||||
|
||||
@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() {
|
||||
|
||||
@@ -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() }]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './notification.service';
|
||||
export * from './notification-handler';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './notification-layout-default.element';
|
||||
@@ -0,0 +1,29 @@
|
||||
import { html, LitElement } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import type { UmbNotificationHandler } from '../../';
|
||||
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 id="layout" headline="${ifDefined(this.data.headline)}" class="uui-text">
|
||||
<div id="message">${this.data.message}</div>
|
||||
</uui-toast-notification-layout>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Meta, Story } from '@storybook/web-components';
|
||||
import { html } from 'lit';
|
||||
import { UmbNotificationLayoutDefaultElement, UmbNotificationDefaultData } from '.';
|
||||
|
||||
export default {
|
||||
title: 'API/Notifications/Layouts/Default',
|
||||
component: 'umb-notification-layout-default',
|
||||
id: 'notification-layout-default',
|
||||
} as Meta;
|
||||
|
||||
const data: UmbNotificationDefaultData = {
|
||||
headline: 'Headline',
|
||||
message: 'This is a default notification',
|
||||
};
|
||||
|
||||
const Template: Story<UmbNotificationLayoutDefaultElement> = () => html`
|
||||
<uui-toast-notification .open=${true}>
|
||||
<umb-notification-layout-default .data=${data}></umb-notification-layout-default>
|
||||
</uui-toast-notification>
|
||||
`;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { fixture, expect, html } from '@open-wc/testing';
|
||||
import { UmbNotificationHandler } from '../../';
|
||||
import type { UmbNotificationLayoutDefaultElement, UmbNotificationDefaultData } from '.';
|
||||
import '.';
|
||||
import { UUIToastNotificationLayoutElement } from '@umbraco-ui/uui';
|
||||
|
||||
describe('UmbNotificationLayoutDefault', () => {
|
||||
let element: UmbNotificationLayoutDefaultElement;
|
||||
|
||||
const data: UmbNotificationDefaultData = {
|
||||
headline: 'Notification Headline',
|
||||
message: 'Notification message',
|
||||
};
|
||||
|
||||
const options = { elementName: 'umb-notification-layout-default', data };
|
||||
|
||||
let notificationHandler: UmbNotificationHandler;
|
||||
|
||||
beforeEach(async () => {
|
||||
notificationHandler = new UmbNotificationHandler(options);
|
||||
element = await fixture(
|
||||
html`<umb-notification-layout-default
|
||||
.notificationHandler=${notificationHandler}
|
||||
.data=${options.data}></umb-notification-layout-default>`
|
||||
);
|
||||
});
|
||||
|
||||
describe('Public API', () => {
|
||||
describe('properties', () => {
|
||||
it('has a notificationHandler property', () => {
|
||||
expect(element).to.have.property('notificationHandler');
|
||||
});
|
||||
|
||||
it('has a data property', () => {
|
||||
expect(element).to.have.property('data');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Data options', () => {
|
||||
describe('Headline', () => {
|
||||
it('sets headline on uui notification layout', () => {
|
||||
const uuiNotificationLayout: UUIToastNotificationLayoutElement | null =
|
||||
element.renderRoot.querySelector('#layout');
|
||||
expect(uuiNotificationLayout?.getAttribute('headline')).to.equal('Notification Headline');
|
||||
});
|
||||
});
|
||||
|
||||
describe('message', () => {
|
||||
it('renders the message', () => {
|
||||
const messageElement: HTMLElement | null = element.renderRoot.querySelector('#message');
|
||||
expect(messageElement?.innerText).to.equal('Notification message');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import { assert, expect } from '@open-wc/testing';
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
import { UmbNotificationHandler } from './notification-handler';
|
||||
import type { UmbNotificationDefaultData } from './layouts/default';
|
||||
import type { UmbNotificationOptions } from './';
|
||||
|
||||
describe('UCPNotificationHandler', () => {
|
||||
let notificationHandler: UmbNotificationHandler;
|
||||
|
||||
beforeEach(async () => {
|
||||
const options: UmbNotificationOptions<UmbNotificationDefaultData> = {};
|
||||
notificationHandler = new UmbNotificationHandler(options);
|
||||
});
|
||||
|
||||
describe('Public API', () => {
|
||||
describe('properties', () => {
|
||||
it('has a key property', () => {
|
||||
expect(notificationHandler).to.have.property('key');
|
||||
expect(uuidValidate(notificationHandler.key)).to.be.true;
|
||||
});
|
||||
|
||||
it('has an element property', () => {
|
||||
expect(notificationHandler).to.have.property('element');
|
||||
});
|
||||
|
||||
it('has an color property', () => {
|
||||
expect(notificationHandler).to.have.property('color');
|
||||
});
|
||||
|
||||
it('has an duration property', () => {
|
||||
expect(notificationHandler).to.have.property('duration');
|
||||
});
|
||||
|
||||
it('sets a default duration to 6000 ms', () => {
|
||||
expect(notificationHandler.duration).to.equal(6000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
it('has a close method', () => {
|
||||
expect(notificationHandler).to.have.property('close').that.is.a('function');
|
||||
});
|
||||
|
||||
it('has a onClose method', () => {
|
||||
expect(notificationHandler).to.have.property('onClose').that.is.a('function');
|
||||
});
|
||||
|
||||
it('returns result from close method in onClose promise', () => {
|
||||
notificationHandler.close('result value');
|
||||
|
||||
const onClose = notificationHandler.onClose();
|
||||
expect(onClose).that.is.a('promise');
|
||||
|
||||
expect(onClose).to.be.not.null;
|
||||
|
||||
if (onClose !== null) {
|
||||
return onClose.then((result) => {
|
||||
expect(result).to.equal('result value');
|
||||
});
|
||||
}
|
||||
|
||||
return assert.fail('onClose should not have returned ´null´');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Layout', () => {
|
||||
describe('Default Layout', () => {
|
||||
let defaultLayoutNotification: UmbNotificationHandler;
|
||||
|
||||
beforeEach(async () => {
|
||||
const options: UmbNotificationOptions<UmbNotificationDefaultData> = {
|
||||
color: 'positive',
|
||||
data: {
|
||||
message: 'Notification default layout message',
|
||||
},
|
||||
};
|
||||
defaultLayoutNotification = new UmbNotificationHandler(options);
|
||||
});
|
||||
|
||||
it('creates a default layout if a custom element name havnt been specified', () => {
|
||||
expect(defaultLayoutNotification.element.tagName).to.equal('UMB-NOTIFICATION-LAYOUT-DEFAULT');
|
||||
});
|
||||
|
||||
it('it sets notificationHandler on custom element', () => {
|
||||
expect(defaultLayoutNotification.element.notificationHandler).to.equal(defaultLayoutNotification);
|
||||
});
|
||||
|
||||
it('it sets data on custom element', () => {
|
||||
expect(defaultLayoutNotification.element.data.message).to.equal('Notification default layout message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom Layout', () => {
|
||||
let customLayoutNotification: UmbNotificationHandler;
|
||||
|
||||
beforeEach(async () => {
|
||||
const options: UmbNotificationOptions<UmbNotificationDefaultData> = {
|
||||
elementName: 'umb-notification-test-element',
|
||||
color: 'positive',
|
||||
data: {
|
||||
message: 'Notification custom layout message',
|
||||
},
|
||||
};
|
||||
customLayoutNotification = new UmbNotificationHandler(options);
|
||||
});
|
||||
|
||||
it('creates a custom element', () => {
|
||||
expect(customLayoutNotification.element.tagName).to.equal('UMB-NOTIFICATION-TEST-ELEMENT');
|
||||
});
|
||||
|
||||
it('it sets notificationHandler on custom element', () => {
|
||||
expect(customLayoutNotification.element.notificationHandler).to.equal(customLayoutNotification);
|
||||
});
|
||||
|
||||
it('it sets data on custom element', () => {
|
||||
expect(customLayoutNotification.element.data.message).to.equal('Notification custom layout message');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { UmbNotificationOptions, UmbNotificationData, UmbNotificationColor } from './';
|
||||
|
||||
import './layouts/default';
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { UmbNotificationHandler } from './';
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="API/Notifications/Intro" />
|
||||
|
||||
# Notifications
|
||||
|
||||
Notifications appear in the bottom right corner of the Backoffice. There are two types of notifications: "Peek" and "Stay".
|
||||
|
||||
**Peek notifications**
|
||||
Goes away automatically and should be used as feedback on user actions.
|
||||
|
||||
**Stay notifications**
|
||||
Stays on the screen until dismissed by the user or custom code. Stay notification should be used when you need user feedback or want to control when the notification disappears.
|
||||
|
||||
## Basic usage
|
||||
|
||||
### Consume UmbNotificationService from an element
|
||||
|
||||
The UmbNotification service can be used to open notifications.
|
||||
|
||||
```ts
|
||||
import { html, LitElement } from 'lit';
|
||||
import { UmbContextConsumerMixin } from './core/context';
|
||||
import type { UmbNotificationService } from './core/services/notification';
|
||||
|
||||
class MyElement extends UmbContextConsumerMixin(LitElement) {
|
||||
|
||||
private _notificationService?: UmbNotificationService;
|
||||
|
||||
constructor () {
|
||||
super();
|
||||
|
||||
this.consumeContext('umbNotificationService', (notificationService) => {
|
||||
this._notificationService = notificationService;
|
||||
// notificationService is now ready to be used
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Open a notification
|
||||
|
||||
A notification is opened by calling one of the helper methods on the UmbNotificationService. The methods will return an instance of UmbNotificationHandler.
|
||||
|
||||
```ts
|
||||
import { html, LitElement } from 'lit';
|
||||
import { state } from 'lit/decorators.js';
|
||||
import { UmbContextConsumerMixin } from './core/context';
|
||||
import type { UmbNotificationService, UmbNotificationDefaultData } from './core/services/notification';
|
||||
|
||||
class MyElement extends UmbContextConsumerMixin(LitElement) {
|
||||
|
||||
private _notificationService?: UmbNotificationService;
|
||||
|
||||
constructor () {
|
||||
super();
|
||||
|
||||
this.consumeContext('umbNotificationService', (notificationService) => {
|
||||
this._notificationService = notificationService;
|
||||
// notificationService is now ready to be used
|
||||
});
|
||||
}
|
||||
|
||||
private _handleClick () {
|
||||
const data: UmbNotificationDefaultData = { headline: 'Look at this', message: 'Something good happened' };
|
||||
const notificationHandler = this._notificationService?.peek('positive', { data });
|
||||
|
||||
notificationHandler.onClose().then(() => {
|
||||
// if you need any logic when the notification is closed you can run it here
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<button @click="${this._handleClick}">Open Notification</button>`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced usage: creating custom layouts
|
||||
|
||||
The default layout will cover most cases, but there might be situations where we want a more complex layout. You can create a new Custom Element to use as the layout.
|
||||
|
||||
### Custom layout element
|
||||
|
||||
```ts
|
||||
import { html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import type { UmbNotificationHandler } from './core/services/notification';
|
||||
|
||||
export interface UmbNotificationCustomData {
|
||||
headline: string;
|
||||
user: {
|
||||
name: string;
|
||||
}
|
||||
}
|
||||
|
||||
export class UmbNotificationLayoutCustom extends LitElement {
|
||||
static styles = [
|
||||
UUITextStyles
|
||||
];
|
||||
|
||||
@property({ attribute: false })
|
||||
public notificationHandler: UmbNotificationHandler;
|
||||
|
||||
@property({ type: Object })
|
||||
public data: UmbNotificationCustomData;
|
||||
|
||||
private _handleConfirm () {
|
||||
this.notificationHandler.close(true);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-toast-notification-layout headline="${this.data.headline}" class="uui-text">
|
||||
${this.data.user.name}
|
||||
<uui-button slot="actions" @click="${this._handleConfirm}" label="Confirm">Confirm</uui-button>
|
||||
</uui-toast-notification-layout>
|
||||
`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Open notification with custom layout
|
||||
|
||||
```ts
|
||||
import { html, LitElement } from 'lit';
|
||||
import { UmbContextInjectMixin } from './core/context';
|
||||
import type { UmbNotificationService, UmbNotificationOptions } from './core/services/notification';
|
||||
import type { UmbNotificationCustomData } from './custom-notification-layout';
|
||||
|
||||
class MyElement extends LitElement {
|
||||
|
||||
private _notificationService?: UmbNotificationService;
|
||||
|
||||
constructor () {
|
||||
super();
|
||||
|
||||
this.consumeContext('umbNotificationService', (notificationService) => {
|
||||
this._notificationService = notificationService;
|
||||
// notificationService is now ready to be used
|
||||
});
|
||||
}
|
||||
|
||||
private _handleClick () {
|
||||
const options: UmbNotificationOptions<UmbNotificationCustomData> = {
|
||||
elementName: 'umb-notification-layout-custom',
|
||||
data: {
|
||||
headline: 'Attention',
|
||||
user: { name: 'Peter Parker' }
|
||||
}
|
||||
};
|
||||
|
||||
const notificationHandler = this._notificationService?.stay('default', options);
|
||||
|
||||
notificationHandler.onClose().then((result) => {
|
||||
if (result) {
|
||||
console.log('He agreed!');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<button @click="${this._handleClick}">Open Notification</button>`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best practices
|
||||
* Keep messages in notifications short and friendly.
|
||||
* Only use headlines when you need extra attention to the notification
|
||||
* If a custom notification layout is only used in one module keep the files layout files local to that module.
|
||||
* If a custom notification will be used across the project. Create it as a layout in the notification folder and add a helper method to the UmbNotificationService.
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Meta, Story } from '@storybook/web-components';
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { UmbNotificationService, UmbNotificationOptions, UmbNotificationColor } from '.';
|
||||
import type { UmbNotificationDefaultData } from './layouts/default';
|
||||
import { UmbContextConsumerMixin } from '../../context';
|
||||
|
||||
import '../../context/context-provider.element';
|
||||
import '../../../backoffice/components/backoffice-notification-container.element';
|
||||
import './layouts/default';
|
||||
|
||||
export default {
|
||||
title: 'API/Notifications/Overview',
|
||||
component: 'ucp-notification-layout-default',
|
||||
decorators: [
|
||||
(story) =>
|
||||
html`<umb-context-provider key="umbNotificationService" .value=${new UmbNotificationService()}>
|
||||
${story()}
|
||||
</umb-context-provider>`,
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
@customElement('story-notification-default-example')
|
||||
class StoryNotificationDefaultExampleElement extends UmbContextConsumerMixin(LitElement) {
|
||||
private _notificationService?: UmbNotificationService;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.consumeContext('umbNotificationService', (notificationService) => {
|
||||
this._notificationService = notificationService;
|
||||
console.log('notification service ready');
|
||||
});
|
||||
}
|
||||
|
||||
private _handleNotification = (color: UmbNotificationColor) => {
|
||||
const options: UmbNotificationOptions<UmbNotificationDefaultData> = {
|
||||
data: {
|
||||
headline: 'Headline',
|
||||
message: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
|
||||
},
|
||||
};
|
||||
this._notificationService?.peek(color, options);
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-button @click="${() => this._handleNotification('default')}" label="Default"></uui-button>
|
||||
<uui-button
|
||||
@click="${() => this._handleNotification('positive')}"
|
||||
label="Positive"
|
||||
look="primary"
|
||||
color="positive"></uui-button>
|
||||
<uui-button
|
||||
@click="${() => this._handleNotification('warning')}"
|
||||
label="Warning"
|
||||
look="primary"
|
||||
color="warning"></uui-button>
|
||||
<uui-button
|
||||
@click="${() => this._handleNotification('danger')}"
|
||||
label="Danger"
|
||||
look="primary"
|
||||
color="danger"></uui-button>
|
||||
|
||||
<umb-backoffice-notification-container></umb-backoffice-notification-container>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const Template: Story = () => html`<story-notification-default-example></story-notification-default-example>`;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.parameters = {
|
||||
docs: {
|
||||
source: {
|
||||
language: 'js',
|
||||
code: `
|
||||
const options: UmbNotificationOptions<UmbNotificationDefaultData> = {
|
||||
data: {
|
||||
headline: 'Headline',
|
||||
message: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
|
||||
}
|
||||
};
|
||||
|
||||
this._notificationService?.peek('positive', options);
|
||||
`,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { expect } from '@open-wc/testing';
|
||||
import { UmbNotificationService, UmbNotificationHandler } from './';
|
||||
|
||||
describe('UCPNotificationService', () => {
|
||||
let notificationService: UmbNotificationService;
|
||||
|
||||
beforeEach(async () => {
|
||||
notificationService = new UmbNotificationService();
|
||||
});
|
||||
|
||||
describe('Public API', () => {
|
||||
describe('properties', () => {
|
||||
it('has a dialog property', () => {
|
||||
expect(notificationService).to.have.property('notifications');
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
it('has a peek method', () => {
|
||||
expect(notificationService).to.have.property('peek').that.is.a('function');
|
||||
});
|
||||
|
||||
it('has a stay method', () => {
|
||||
expect(notificationService).to.have.property('stay').that.is.a('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('peek', () => {
|
||||
let peekNotificationHandler: UmbNotificationHandler | undefined = undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
const peekOptions = {
|
||||
data: { headline: 'Peek notification headline', message: 'Peek notification message' },
|
||||
};
|
||||
|
||||
peekNotificationHandler = notificationService.peek('positive', peekOptions);
|
||||
});
|
||||
|
||||
it('it sets notification color', () => {
|
||||
expect(peekNotificationHandler?.color).to.equal('positive');
|
||||
});
|
||||
|
||||
it('should set peek data on the notification element', () => {
|
||||
const data = peekNotificationHandler?.element.data;
|
||||
expect(data.headline).to.equal('Peek notification headline');
|
||||
expect(data.message).to.equal('Peek notification message');
|
||||
});
|
||||
|
||||
it('it sets duration to 6000 ms', () => {
|
||||
expect(peekNotificationHandler?.duration).to.equal(6000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stay', () => {
|
||||
let stayNotificationHandler: UmbNotificationHandler | undefined = undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
const stayOptions = {
|
||||
data: { headline: 'Stay notification headline', message: 'Stay notification message' },
|
||||
};
|
||||
|
||||
stayNotificationHandler = notificationService.stay('danger', stayOptions);
|
||||
});
|
||||
|
||||
it('it sets notification color', () => {
|
||||
expect(stayNotificationHandler?.color).to.equal('danger');
|
||||
});
|
||||
|
||||
it('should set stay data on the notification element', () => {
|
||||
const data = stayNotificationHandler?.element.data;
|
||||
expect(data.headline).to.equal('Stay notification headline');
|
||||
expect(data.message).to.equal('Stay notification message');
|
||||
});
|
||||
|
||||
it('it sets the duration to null', () => {
|
||||
expect(stayNotificationHandler?.duration).to.equal(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user