Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: reworking error toast notifications #18172

Merged
merged 16 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,8 @@ export default {
deleteLayout: 'You are deleting the layout',
deletingALayout:
'Modifying layout will result in loss of data for any existing content that is based on this configuration.',
seeErrorAction: 'Se fejlen',
seeErrorDialogHeadline: 'Fejl detaljer',
},
dictionary: {
noItems: 'Der er ingen ordbogselementer.',
Expand Down
2 changes: 2 additions & 0 deletions src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,8 @@ export default {
deleteLayout: 'You are deleting the layout',
deletingALayout:
'Modifying layout will result in loss of data for any existing content that is based on this configuration.',
seeErrorAction: 'See error',
seeErrorDialogHeadline: 'Error details',
},
dictionary: {
importDictionaryItemHelp:
Expand Down
2 changes: 2 additions & 0 deletions src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,8 @@ export default {
deletingALayout:
'Modifying layout will result in loss of data for any existing content that is based on this configuration.',
selectEditorConfiguration: 'Select configuration',
seeErrorAction: 'See error',
seeErrorDialogHeadline: 'Error details',
},
dictionary: {
importDictionaryItemHelp:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ export class UmbCodeBlockElement extends LitElement {
}

uui-scroll-container {
max-height: 500px;
overflow-y: auto;
overflow-wrap: anywhere;
}
Expand Down
2 changes: 2 additions & 0 deletions src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { manifests as iconRegistryManifests } from './icon-registry/manifests.js
import { manifests as localizationManifests } from './localization/manifests.js';
import { manifests as menuManifests } from './menu/manifests.js';
import { manifests as modalManifests } from './modal/manifests.js';
import { manifests as notificationManifests } from './notification/manifests.js';
import { manifests as pickerManifests } from './picker/manifests.js';
import { manifests as propertyActionManifests } from './property-action/manifests.js';
import { manifests as propertyEditorManifests } from './property-editor/manifests.js';
Expand Down Expand Up @@ -40,6 +41,7 @@ export const manifests: Array<UmbExtensionManifest | UmbExtensionManifestKind> =
...localizationManifests,
...menuManifests,
...modalManifests,
...notificationManifests,
...pickerManifests,
...propertyActionManifests,
...propertyEditorManifests,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './peek-error.controller.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { customElement, html, ifDefined, nothing, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbNotificationHandler } from '../../notification-handler.js';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import type { UmbPeekErrorArgs } from '../../types.js';
import { UMB_ERROR_VIEWER_MODAL } from '../../index.js';

@customElement('umb-peek-error-notification')
export class UmbPeekErrorNotificationElement extends UmbLitElement {
@property({ attribute: false })
public data?: UmbPeekErrorArgs;

public notificationHandler!: UmbNotificationHandler;

async #onClick() {
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);

modalManager.open(this, UMB_ERROR_VIEWER_MODAL, { data: this.data?.details });

this.notificationHandler.close();
}

protected override render() {
return this.data
? html`<uui-toast-notification-layout headline=${ifDefined(this.data.headline)}
>${this.data.message}${this.data.details
? html`<uui-button
slot="actions"
look="primary"
color="danger"
label=${this.localize.term('defaultdialogs_seeErrorAction')}
@click=${this.#onClick}></uui-button>`
: nothing}</uui-toast-notification-layout
>`
: nothing;
}
}

declare global {
interface HTMLElementTagNameMap {
'umb-peek-error-notification': UmbPeekErrorNotificationElement;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { UMB_NOTIFICATION_CONTEXT } from '../../notification.context.js';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbPeekErrorArgs } from '../../types.js';

import './peek-error-notification.element.js';

export class UmbPeekErrorController extends UmbControllerBase {
async open(args: UmbPeekErrorArgs): Promise<void> {
const context = await this.getContext(UMB_NOTIFICATION_CONTEXT);

context.peek('danger', {
elementName: 'umb-peek-error-notification',
data: args,
});

// This is a one time off, so we can destroy our selfs.
this.destroy();

return;
}
}

/**
*
* @param host {UmbControllerHost} - The host controller
* @param args {UmbPeekErrorArgs} - The data to pass to the notification
* @returns {UmbPeekErrorController} The notification peek controller instance
*/
export function umbPeekError(host: UmbControllerHost, args: UmbPeekErrorArgs) {
return new UmbPeekErrorController(host).open(args);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { UmbNotificationColor } from './notification.context.js';
import { EventMessageTypeModel } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbNotificationColor } from './types.js';

/**
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import './layouts/default/index.js';

export * from './notification.context.js';
export * from './controllers/peek-error/index.js';
export * from './extractUmbNotificationColor.function.js';
export * from './isUmbNotifications.function.js';
export * from './modals/error-viewer/index.js';
export * from './notification-handler.js';
export * from './notification.context.js';

export * from './isUmbNotifications.function.js';
export * from './extractUmbNotificationColor.function.js';
export type * from './types.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { manifest } from './modals/error-viewer/manifest.js';

export const manifests = [manifest];
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { UmbErrorViewerModalData, UmbErrorViewerModalValue } from './error-viewer-modal.token.js';
import { css, customElement, html, nothing, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';

@customElement('umb-error-viewer-modal')
export class UmbErrorViewerModalElement extends UmbModalBaseElement<UmbErrorViewerModalData, UmbErrorViewerModalValue> {
@state()
_displayError?: string;

@state()
_displayLang?: string;

// Code adapted from https://stackoverflow.com/a/57668208/12787
// Licensed under the permissions of the CC BY-SA 4.0 DEED
#stringify(obj: any): string {
let output = '{';
for (const key in obj) {
let value = obj[key];
if (typeof value === 'function') {
value = value.toString();
} else if (value instanceof Array) {
value = JSON.stringify(value);
} else if (typeof value === 'object') {
value = this.#stringify(value);
} else {
value = `"${value}"`;
}
output += `\n ${key}: ${value},`;
}
return output + '\n}';
}

public override set data(value: UmbErrorViewerModalData | undefined) {
super.data = value;
// is JSON:
if (typeof value === 'string') {
this._displayLang = 'String';
this._displayError = value;
} else {
this._displayLang = 'JSON';
this._displayError = this.#stringify(value);
}
}
public override get data(): UmbErrorViewerModalData | undefined {
return super.data;
}

override render() {
return html`
<umb-body-layout headline=${this.localize.term('defaultdialogs_seeErrorDialogHeadline')} main-no-padding>
${this.data
? html`<umb-code-block language=${this._displayLang ?? ''} copy>${this._displayError}</umb-code-block>`
: nothing}
<div slot="actions">
<uui-button label=${this.localize.term('general_close')} @click=${this._rejectModal}></uui-button>
</div>
</umb-body-layout>
`;
}

static override styles = [
css`
umb-code-block {
border: none;
height: 100%;
}
`,
];
}

export default UmbErrorViewerModalElement;

declare global {
interface HTMLElementTagNameMap {
'umb-error-viewer-modal': UmbErrorViewerModalElement;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
import type { UmbPeekErrorArgs } from '../../types.js';

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface UmbErrorViewerModalData extends UmbPeekErrorArgs {}

export type UmbErrorViewerModalValue = undefined;

export const UMB_ERROR_VIEWER_MODAL = new UmbModalToken<UmbErrorViewerModalData, UmbErrorViewerModalValue>(
'Umb.Modal.ErrorViewer',
{
modal: {
type: 'sidebar',
size: 'medium',
},
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './error-viewer-modal.token.js';
export * from './manifest.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ManifestModal } from '@umbraco-cms/backoffice/modal';

export const manifest: ManifestModal = {
type: 'modal',
alias: 'Umb.Modal.ErrorViewer',
name: 'Error Viewer Modal',
element: () => import('./error-viewer-modal.element.js'),
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { UmbNotificationHandler } from './notification-handler.js';
import type { UmbNotificationOptions } from './notification.context.js';
import type { UmbNotificationOptions } from './types.js';
import { assert, expect } from '@open-wc/testing';
import { UmbId } from '@umbraco-cms/backoffice/id';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import type {
UmbNotificationOptions,
UmbNotificationColor,
UmbNotificationDefaultData,
} from './notification.context.js';
import type { UmbNotificationOptions, UmbNotificationColor, UmbNotificationDefaultData } from './types.js';
import type { UUIToastNotificationElement } from '@umbraco-cms/backoffice/external/uui';
import { UmbId } from '@umbraco-cms/backoffice/id';

const DEFAULT_LAYOUT = 'umb-notification-layout-default';

/**
* @class UmbNotificationHandler
*/
Expand All @@ -17,10 +15,9 @@ export class UmbNotificationHandler {

private _defaultColor: UmbNotificationColor = 'default';
private _defaultDuration = 6000;
private _defaultLayout = 'umb-notification-layout-default';

public key: string;
public element: any;
public element!: UUIToastNotificationElement;
public color: UmbNotificationColor;
public duration: number | null;

Expand All @@ -34,23 +31,13 @@ export class UmbNotificationHandler {
this.color = options.color || this._defaultColor;
this.duration = options.duration !== undefined ? options.duration : this._defaultDuration;

this._elementName = options.elementName || this._defaultLayout;
this._elementName = options.elementName || DEFAULT_LAYOUT;
this._data = options.data;

this._closePromise = new Promise((res) => {
this._closeResolver = res;
});

this._createElement();
}

/**
* @private
* @memberof UmbNotificationHandler
*/
private _createElement() {
if (!this._elementName) return;

const notification: UUIToastNotificationElement = document.createElement('uui-toast-notification');

notification.color = this.color;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,7 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbBasicState } from '@umbraco-cms/backoffice/observable-api';

/**
* The default data of notifications
* @interface UmbNotificationDefaultData
*/
export interface UmbNotificationDefaultData {
message: string;
headline?: string;
structuredList?: Record<string, Array<unknown>>;
}

/**
* @interface UmbNotificationOptions
* @template UmbNotificationData
*/
export interface UmbNotificationOptions<UmbNotificationData = UmbNotificationDefaultData> {
color?: UmbNotificationColor;
duration?: number | null;
elementName?: string;
data?: UmbNotificationData;
}

export type UmbNotificationColor = '' | 'default' | 'positive' | 'warning' | 'danger';
import type { UmbNotificationColor, UmbNotificationOptions } from './types.js';

export class UmbNotificationContext extends UmbContextBase<UmbNotificationContext> {
// Notice this cannot use UniqueBehaviorSubject as it holds a HTML Element. which cannot be Serialized to JSON (it has some circular references)
Expand All @@ -42,9 +20,9 @@ export class UmbNotificationContext extends UmbContextBase<UmbNotificationContex
* @returns {*} {UmbNotificationHandler}
* @memberof UmbNotificationContext
*/
private _open(options: UmbNotificationOptions): UmbNotificationHandler {
#open<T extends UmbNotificationOptions = UmbNotificationOptions>(options: T): UmbNotificationHandler {
const notificationHandler = new UmbNotificationHandler(options);
notificationHandler.element.addEventListener('closed', () => this._handleClosed(notificationHandler));
notificationHandler.element?.addEventListener('closed', () => this._handleClosed(notificationHandler));

this._notifications.setValue([...this._notifications.getValue(), notificationHandler]);

Expand Down Expand Up @@ -78,8 +56,11 @@ export class UmbNotificationContext extends UmbContextBase<UmbNotificationContex
* @returns {*}
* @memberof UmbNotificationContext
*/
public peek(color: UmbNotificationColor, options: UmbNotificationOptions): UmbNotificationHandler {
return this._open({ color, ...options });
public peek<T extends UmbNotificationOptions = UmbNotificationOptions>(
color: UmbNotificationColor,
options: T,
): UmbNotificationHandler {
return this.#open({ color, ...options });
}

/**
Expand All @@ -89,8 +70,11 @@ export class UmbNotificationContext extends UmbContextBase<UmbNotificationContex
* @returns {*}
* @memberof UmbNotificationContext
*/
public stay(color: UmbNotificationColor, options: UmbNotificationOptions): UmbNotificationHandler {
return this._open({ ...options, color, duration: null });
public stay<T extends UmbNotificationOptions = UmbNotificationOptions>(
color: UmbNotificationColor,
options: T,
): UmbNotificationHandler {
return this.#open({ ...options, color, duration: null });
}
}

Expand Down
Loading
Loading