Merge remote-tracking branch 'origin/main' into feature/slimmer-libs

This commit is contained in:
Jacob Overgaard
2023-03-13 10:20:17 +01:00
383 changed files with 18170 additions and 3333 deletions

View File

@@ -1,3 +1,4 @@
import { UMB_CONFIRM_MODAL_TOKEN } from '../../../../src/backoffice/shared/modals/confirm';
import { UmbEntityActionBase } from '@umbraco-cms/entity-action';
import { UmbContextConsumerController } from '@umbraco-cms/context-api';
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
@@ -24,17 +25,15 @@ export class UmbDeleteEntityAction<
if (data) {
const item = data[0];
const modalHandler = this.#modalContext.confirm({
const modalHandler = this.#modalContext.open(UMB_CONFIRM_MODAL_TOKEN, {
headline: `Delete ${item.name}`,
content: 'Are you sure you want to delete this item?',
color: 'danger',
confirmLabel: 'Delete',
});
const { confirmed } = await modalHandler.onClose();
if (confirmed) {
await this.repository?.delete(this.unique);
}
await modalHandler.onSubmit();
await this.repository?.delete(this.unique);
}
}
}

View File

@@ -1,3 +1,4 @@
import { UMB_CONFIRM_MODAL_TOKEN } from '../../../../src/backoffice/shared/modals/confirm';
import { UmbEntityActionBase } from '@umbraco-cms/entity-action';
import { UmbContextConsumerController } from '@umbraco-cms/context-api';
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
@@ -24,17 +25,15 @@ export class UmbTrashEntityAction<
if (data) {
const item = data[0];
const modalHandler = this.#modalContext?.confirm({
const modalHandler = this.#modalContext?.open(UMB_CONFIRM_MODAL_TOKEN, {
headline: `Trash ${item.name}`,
content: 'Are you sure you want to move this item to the recycle bin?',
color: 'danger',
confirmLabel: 'Trash',
});
modalHandler?.onClose().then(({ confirmed }) => {
if (confirmed) {
this.repository?.trash([this.unique]);
}
modalHandler?.onSubmit().then(() => {
this.repository?.trash([this.unique]);
});
}
}

View File

@@ -1,9 +1,9 @@
import { BehaviorSubject, map, Observable } from 'rxjs';
import { UmbContextToken } from "@umbraco-cms/context-api";
import type { UmbControllerHostInterface } from "@umbraco-cms/controller";
import type { ManifestTypes, ManifestTypeMap, ManifestBase, ManifestEntrypoint } from '../../models';
import { loadExtension } from '../load-extension.function';
import { hasInitExport } from "../has-init-export.function";
import type { UmbControllerHostInterface } from "@umbraco-cms/controller";
import { UmbContextToken } from "@umbraco-cms/context-api";
type SpecificManifestTypeOrManifestBase<T extends keyof ManifestTypeMap | string> = T extends keyof ManifestTypeMap
? ManifestTypeMap[T]

View File

@@ -1,5 +1,5 @@
import type { UmbControllerHostInterface } from "@umbraco-cms/controller";
import type { UmbExtensionRegistry } from "./registry/extension.registry";
import type { UmbControllerHostInterface } from "@umbraco-cms/controller";
/**
* Interface containing supported life-cycle functions for ESModule entrypoints

View File

@@ -0,0 +1,5 @@
import type { ManifestElement } from './models';
export interface ManifestModal extends ManifestElement {
type: 'modal';
}

View File

@@ -22,6 +22,8 @@ import type { ManifestWorkspaceAction } from './workspace-action.models';
import type { ManifestWorkspaceView } from './workspace-view.models';
import type { ManifestWorkspaceViewCollection } from './workspace-view-collection.models';
import type { ManifestRepository } from './repository.models';
import type { ManifestModal } from './modal.models';
import type { ManifestStore, ManifestTreeStore } from './store.models';
import type { ClassConstructor } from '@umbraco-cms/models';
export * from './collection-view.models';
@@ -47,7 +49,9 @@ export * from './workspace-action.models';
export * from './workspace-view-collection.models';
export * from './workspace-view.models';
export * from './repository.models';
export * from './store.models';
export * from './workspace.models';
export * from './modal.models';
export type ManifestTypes =
| ManifestCollectionView
@@ -78,6 +82,9 @@ export type ManifestTypes =
| ManifestWorkspaceAction
| ManifestWorkspaceView
| ManifestWorkspaceViewCollection
| ManifestModal
| ManifestStore
| ManifestTreeStore
| ManifestBase;
export type ManifestStandardTypes = ManifestTypes['type'];
@@ -97,11 +104,11 @@ export interface ManifestWithLoader<LoaderReturnType> extends ManifestBase {
loader?: () => Promise<LoaderReturnType>;
}
export interface ManifestClass extends ManifestWithLoader<object> {
export interface ManifestClass<T = unknown> extends ManifestWithLoader<object> {
type: ManifestStandardTypes;
js?: string;
className?: string;
class?: ClassConstructor<unknown>;
class?: ClassConstructor<T>;
//loader?: () => Promise<object | HTMLElement>;
}

View File

@@ -0,0 +1,10 @@
import type { ManifestClass } from './models';
import { UmbStoreBase, UmbTreeStoreBase } from '@umbraco-cms/store';
export interface ManifestStore extends ManifestClass<UmbStoreBase> {
type: 'store';
}
export interface ManifestTreeStore extends ManifestClass<UmbTreeStoreBase> {
type: 'treeStore';
}

View File

@@ -0,0 +1,58 @@
import { property } from 'lit/decorators.js';
import { UmbModalBaseElement } from '..';
import './modal-element.element';
export interface UmbPickerModalData<T> {
multiple: boolean;
selection: Array<string>;
filter?: (language: T) => boolean;
}
export interface UmbPickerModalResult<T> {
selection: Array<string>;
}
// TODO: we should consider moving this into a class/context instead of an element.
// So we don't have to extend an element to get basic picker/selection logic
export class UmbModalElementPickerBase<T> extends UmbModalBaseElement<UmbPickerModalData<T>, UmbPickerModalResult<T>> {
@property()
selection: Array<string> = [];
connectedCallback(): void {
super.connectedCallback();
this.selection = this.data?.selection || [];
}
submit() {
this.modalHandler?.submit({ selection: this.selection });
}
close() {
this.modalHandler?.reject();
}
protected _handleKeydown(e: KeyboardEvent, key: string) {
if (e.key === 'Enter') {
this.handleSelection(key);
}
}
/* TODO: Write test for this select/deselect method. */
handleSelection(key: string) {
if (this.data?.multiple) {
if (this.isSelected(key)) {
this.selection = this.selection.filter((selectedKey) => selectedKey !== key);
} else {
this.selection.push(key);
}
} else {
this.selection = [key];
}
this.requestUpdate('_selection');
}
isSelected(key: string): boolean {
return this.selection.includes(key);
}
}

View File

@@ -0,0 +1,18 @@
import { customElement, property } from 'lit/decorators.js';
import { UmbModalHandler } from '..';
import { UmbLitElement } from '@umbraco-cms/element';
@customElement('umb-modal-element')
export class UmbModalBaseElement<UmbModalData = void, UmbModalResult = void> extends UmbLitElement {
@property({ attribute: false })
modalHandler?: UmbModalHandler<UmbModalData, UmbModalResult>;
@property({ type: Object, attribute: false })
data?: UmbModalData;
}
declare global {
interface HTMLElementTagNameMap {
'umb-modal-element': UmbModalBaseElement<unknown>;
}
}

View File

@@ -0,0 +1,5 @@
export * from './modal.context';
export * from './modal-handler';
export * from './elements/modal-element.element';
export * from './elements/modal-element-picker-base';
export * from './token/modal-token';

View File

@@ -0,0 +1,159 @@
import type { UUIDialogElement } from '@umbraco-ui/uui';
import type { UUIModalDialogElement } from '@umbraco-ui/uui-modal-dialog';
import { UUIModalSidebarElement, UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar';
import { v4 as uuidv4 } from 'uuid';
import { BehaviorSubject } from 'rxjs';
import { UmbModalConfig, UmbModalType } from './modal.context';
import { UmbModalToken } from './token/modal-token';
import { createExtensionElement, umbExtensionsRegistry } from '@umbraco-cms/extensions-api';
import { UmbObserverController } from '@umbraco-cms/observable-api';
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
import { ManifestModal } from '@umbraco-cms/extensions-registry';
/**
* Type which omits the real submit method, and replaces it with a submit method which accepts an optional argument depending on the generic type.
*/
export type UmbModalHandler<ModalData = unknown, ModalResult = unknown> = Omit<
UmbModalHandlerClass<ModalData, ModalResult>,
'submit'
> &
OptionalSubmitArgumentIfUndefined<ModalResult>;
// If Type is undefined we don't accept an argument,
// If type is unknown, we accept an option argument.
// If type is anything else, we require an argument of that type.
type OptionalSubmitArgumentIfUndefined<T> = T extends undefined
? {
submit: () => void;
}
: T extends unknown
? {
submit: (arg?: T) => void;
}
: {
submit: (arg: T) => void;
};
//TODO consider splitting this into two separate handlers
export class UmbModalHandlerClass<ModalData, ModalResult> {
private _submitPromise: Promise<ModalResult>;
private _submitResolver?: (value: ModalResult) => void;
private _submitRejecter?: () => void;
#host: UmbControllerHostInterface;
public modalElement: UUIModalDialogElement | UUIModalSidebarElement;
#innerElement = new BehaviorSubject<any | undefined>(undefined);
public readonly innerElement = this.#innerElement.asObservable();
#modalElement?: UUIModalSidebarElement | UUIDialogElement;
public key: string;
public type: UmbModalType = 'dialog';
public size: UUIModalSidebarSize = 'small';
constructor(
host: UmbControllerHostInterface,
modalAlias: string | UmbModalToken<ModalData, ModalResult>,
data?: ModalData,
config?: UmbModalConfig
) {
this.#host = host;
this.key = config?.key || uuidv4();
if (modalAlias instanceof UmbModalToken) {
this.type = modalAlias.getDefaultConfig()?.type || this.type;
this.size = modalAlias.getDefaultConfig()?.size || this.size;
}
this.type = config?.type || this.type;
this.size = config?.size || this.size;
// TODO: Consider if its right to use Promises, or use another event based system? Would we need to be able to cancel an event, to then prevent the closing..?
this._submitPromise = new Promise((resolve, reject) => {
this._submitResolver = resolve;
this._submitRejecter = reject;
});
this.modalElement = this.#createContainerElement();
this.#observeModal(modalAlias.toString(), data);
}
#createContainerElement() {
return this.type === 'sidebar' ? this.#createSidebarElement() : this.#createDialogElement();
}
#createSidebarElement() {
const sidebarElement = document.createElement('uui-modal-sidebar');
this.#modalElement = sidebarElement;
sidebarElement.size = this.size;
return sidebarElement;
}
#createDialogElement() {
const modalDialogElement = document.createElement('uui-modal-dialog');
const dialogElement: UUIDialogElement = document.createElement('uui-dialog');
this.#modalElement = dialogElement;
modalDialogElement.appendChild(dialogElement);
return modalDialogElement;
}
async #createInnerElement(manifest: ManifestModal, data?: ModalData) {
// TODO: add inner fallback element if no extension element is found
const innerElement = (await createExtensionElement(manifest)) as any;
if (innerElement) {
innerElement.data = data; //
//innerElement.observable = this.#dataObservable;
innerElement.modalHandler = this;
}
return innerElement;
}
// note, this methods argument is not defined correctly here, but requires to be fix by appending the OptionalSubmitArgumentIfUndefined type when newing up this class.
private submit(result?: ModalResult) {
this._submitResolver?.(result as ModalResult);
this.modalElement.close();
}
public reject() {
this._submitRejecter?.();
this.modalElement.close();
}
public onSubmit(): Promise<ModalResult> {
return this._submitPromise;
}
/* TODO: modals being part of the extension registry now means that a modal element can change over time.
It makes this code a bit more complex. The main idea is to have the element as part of the modalHandler so it is possible to dispatch events from within the modal element to the one that opened it.
Now when the element is an observable it makes it more complex because this host needs to subscribe to updates to the element, instead of just having a reference to it.
If we find a better generic solution to communicate between the modal and the implementor, then we can remove the element as part of the modalHandler. */
#observeModal(modalAlias: string, data?: ModalData) {
new UmbObserverController(
this.#host,
umbExtensionsRegistry.getByTypeAndAlias('modal', modalAlias),
async (manifest) => {
if (manifest) {
const innerElement = await this.#createInnerElement(manifest, data);
this.#appendInnerElement(innerElement);
} else {
this.#removeInnerElement();
}
}
);
}
#appendInnerElement(element: any) {
this.#modalElement?.appendChild(element);
this.#innerElement.next(element);
}
#removeInnerElement() {
if (this.#innerElement.getValue()) {
this.#modalElement?.removeChild(this.#innerElement.getValue());
this.#innerElement.next(undefined);
}
}
}

View File

@@ -0,0 +1,124 @@
// TODO: remove this import when the search hack is removed
import '../../src/backoffice/search/modals/search/search-modal.element';
import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar';
import { BehaviorSubject } from 'rxjs';
import type { UUIModalDialogElement } from '@umbraco-ui/uui-modal-dialog';
import { UmbModalHandler, UmbModalHandlerClass } from './modal-handler';
import type { UmbModalToken } from './token/modal-token';
import { UmbContextToken } from '@umbraco-cms/context-api';
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
export type UmbModalType = 'dialog' | 'sidebar';
export interface UmbModalConfig {
key?: string;
type?: UmbModalType;
size?: UUIModalSidebarSize;
}
// TODO: we should find a way to easily open a modal without adding custom methods to this context. It would result in a better separation of concerns.
// TODO: move all layouts into their correct "silo" folders. User picker should live with users etc.
export class UmbModalContext {
host: UmbControllerHostInterface;
// TODO: Investigate if we can get rid of HTML elements in our store, so we can use one of our states.
#modals = new BehaviorSubject(<Array<UmbModalHandler<any, any>>>[]);
public readonly modals = this.#modals.asObservable();
constructor(host: UmbControllerHostInterface) {
this.host = host;
}
// TODO: Remove this when the modal system is more flexible
public search() {
const modalHandler = new UmbModalHandlerClass(this.host, 'Umb.Modal.Search') as unknown as UmbModalHandler<
any,
any
>;
//TODO START: This is a hack to get the search modal layout to look like i want it to.
//TODO: Remove from here to END when the modal system is more flexible
const topDistance = '50%';
const margin = '16px';
const maxHeight = '600px';
const maxWidth = '500px';
const dialog = document.createElement('dialog') as HTMLDialogElement;
dialog.style.top = `max(${margin}, calc(${topDistance} - ${maxHeight} / 2))`;
dialog.style.margin = '0 auto';
dialog.style.transform = `translateY(${-maxHeight})`;
dialog.style.maxHeight = `min(${maxHeight}, calc(100% - ${margin}px * 2))`;
dialog.style.width = `min(${maxWidth}, calc(100vw - ${margin}))`;
dialog.style.boxSizing = 'border-box';
dialog.style.background = 'none';
dialog.style.border = 'none';
dialog.style.padding = '0';
dialog.style.boxShadow = 'var(--uui-shadow-depth-5)';
dialog.style.borderRadius = '9px';
const search = document.createElement('umb-search-modal');
dialog.appendChild(search);
requestAnimationFrame(() => {
dialog.showModal();
});
modalHandler.modalElement = dialog as unknown as UUIModalDialogElement;
//TODO END
modalHandler.modalElement.addEventListener('close-end', () => this.#onCloseEnd(modalHandler));
this.#modals.next([...this.#modals.getValue(), modalHandler]);
return modalHandler;
}
/**
* Opens a modal or sidebar modal
* @public
* @param {(string | HTMLElement)} element
* @param {UmbModalOptions<unknown>} [options]
* @return {*} {UmbModalHandler}
* @memberof UmbModalContext
*/
public open<ModalData = unknown, ModalResult = unknown>(
modalAlias: string | UmbModalToken<ModalData, ModalResult>,
data?: ModalData,
config?: UmbModalConfig
) {
const modalHandler = new UmbModalHandlerClass(this.host, modalAlias, data, config) as unknown as UmbModalHandler<
ModalData,
ModalResult
>;
modalHandler.modalElement.addEventListener('close-end', () => this.#onCloseEnd(modalHandler));
this.#modals.next([...this.#modals.getValue(), modalHandler]);
return modalHandler;
}
/**
* Closes a modal or sidebar modal
* @private
* @param {string} key
* @memberof UmbModalContext
*/
public close(key: string) {
const modal = this.#modals.getValue().find((modal) => modal.key === key);
if (modal) {
modal.reject();
}
}
#remove(key: string) {
this.#modals.next(this.#modals.getValue().filter((modal) => modal.key !== key));
}
/**
* Handles the close-end event
* @private
* @param {UmbModalHandler} modalHandler
* @memberof UmbModalContext
*/
#onCloseEnd(modalHandler: UmbModalHandler<any, any>) {
modalHandler.modalElement.removeEventListener('close-end', () => this.#onCloseEnd(modalHandler));
this.#remove(modalHandler.key);
}
}
export const UMB_MODAL_CONTEXT_TOKEN = new UmbContextToken<UmbModalContext>('UmbModalContext');

View File

@@ -0,0 +1,145 @@
import { Meta } from '@storybook/blocks';
<Meta title="API/Modals/Intro" />
# Modals
A modal is a popup that darkens the background and has focus lock. There are two types of modals: "dialog" and "sidebar".
**Dialog modals** appears in the middle of the screen.
| option | values |
|:------:|:--------------------------:|
| No options yet | |
**Sidebar modals** slides in from the right.
| option | values |
|:------:|:--------------------------:|
| size | small, medium, large, full |
## Basic Usage
### Consume UmbModalContext from an element
The UmbModal context can be used to open modals.
```ts
import { LitElement } from 'lit';
import { UmbElementMixin } from '@umbraco-cms/element';
import { UmbModalContext, UMB_MODAL_CONTEXT_ALIAS } from '@umbraco-cms/modal';
class MyElement extends UmbElementMixin(LitElement) {
#modalContext?: UmbModalContext;
constructor() {
super();
this.consumeContext(UMB_MODAL_CONTEXT_ALIAS, (instance) => {
this.#modalContext = instance;
// modalContext is now ready to be used.
});
}
}
```
### Open a modal
A modal is opened by calling the open method on the UmbModalContext. The methods will accept a modal token (or extension alias), an optional dataset, and optional modal options .It returns an instance of UmbModalHandler.
```ts
import { html, LitElement } from 'lit';
import { UmbElementMixin } from '@umbraco-cms/element';
import { UmbModalContext, UMB_MODAL_CONTEXT_ALIAS } from '@umbraco-cms/modal';
class MyElement extends UmbElementMixin(LitElement) {
#modalContext?: UmbModalContext;
constructor() {
super();
this.consumeContext(UMB_MODAL_CONTEXT_ALIAS, (instance) => {
this.#modalContext = instance;
// modalContext is now ready to be used
});
}
#onClick() {
const data = {'data goes here'};
const options {'options go here'};
const modalHandler = this.#modalContext?.open(SOME_MODAL_TOKEN), data, options);
modalHandler?.onSubmit().then((data) => {
// if modal submitted, then data is supplied here.
});
}
render() {
return html`<button @click=${this.#onClick}>Open modal</button>`;
}
}
```
## Create a custom modal
### Register in the extension registry
The manifest
```json
{
"type": "modal",
"alias": "My.Modal",
"name": "My Modal",
"js": "../path/to/my-modal.element.js"
}
```
### Create a modal token
A modal token is a string that identifies a modal. It should be the modal extension alias. It is used to open a modal and is also to set default options for the modal.
```ts
interface MyModalData = {
headline: string;
content: string;
}
interface MyModalResult = {
myReturnData: string;
}
const MY_MODAL_TOKEN = new ModalToken<MyModalData, MyModalResult>('My.Modal', {
type: 'sidebar',
size: 'small'
});
```
The Modal element
```ts
import { html, LitElement } from 'lit';
import { UmbElementMixin } from '@umbraco-cms/element';
import type { UmbModalHandler } from '@umbraco-cms/modal';
class MyDialog extends UmbElementMixin(LitElement) {
// the modal handler will be injected into the element when the modal is opened.
@property({ attribute: false })
modalHandler?: UmbModalHandler<MyModalData, MyModalResult>;
private _handleCancel() {
this._modalHandler?.close();
}
private _handleSubmit() {
/* Optional data of any type can be applied to the submit method to pass it
to the modal parent through the onSubmit promise. */
this._modalHandler?.submit({ myReturnData: 'hello world' });
}
render() {
return html`
<div>
<h1>My Modal</h1>
<button @click=${this._handleCancel}>Cancel</button>
<button @click=${this._handleSubmit}>Submit</button>
</div>
`;
}
}
```

View File

@@ -0,0 +1,22 @@
import { Meta, Story } from '@storybook/web-components';
import { html } from 'lit-html';
export default {
title: 'API/Modals',
id: 'umb-modal-context',
argTypes: {
modalLayout: {
control: 'select',
//options: ['Confirm', 'Content Picker', 'Property Editor UI Picker', 'Icon Picker'],
},
},
} as Meta;
const Template: Story = (props) => {
return html`
Under construction
<story-modal-context-example .modalLayout=${props.modalLayout}></story-modal-context-example>
`;
};
export const Overview = Template.bind({});

View File

@@ -0,0 +1,53 @@
import { html } from 'lit-html';
import { customElement, property, state } from 'lit/decorators.js';
import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '..';
import { UmbLitElement } from '@umbraco-cms/element';
@customElement('story-modal-context-example')
export class StoryModalContextExampleElement extends UmbLitElement {
@property()
modalLayout = 'confirm';
@state()
value = '';
private _modalContext?: UmbModalContext;
constructor() {
super();
this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => {
this._modalContext = instance;
});
}
private _open() {
// TODO: use the extension registry to get all modals
/*
switch (this.modalLayout) {
case 'Content Picker':
this._modalContext?.documentPicker();
break;
case 'Property Editor UI Picker':
this._modalContext?.propertyEditorUIPicker();
break;
case 'Icon Picker':
this._modalContext?.iconPicker();
break;
default:
this._modalContext?.confirm({
headline: 'Headline',
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
});
break;
}
*/
}
render() {
return html`
<uui-button label="open-dialog" look="primary" @click=${() => this._open()} style="margin-right: 9px;"
>Open modal</uui-button
>
`;
}
}

View File

@@ -0,0 +1,48 @@
import { UmbModalConfig } from '../modal.context';
export class UmbModalToken<Data = unknown, Result = unknown> {
/**
* Get the data type of the token's data.
*
* @public
* @type {Data}
* @memberOf UmbModalToken
* @example `typeof MyModal.TYPE`
* @returns undefined
*/
readonly DATA: Data = undefined as never;
/**
* Get the result type of the token
*
* @public
* @type {Result}
* @memberOf UmbModalToken
* @example `typeof MyModal.RESULT`
* @returns undefined
*/
readonly RESULT: Result = undefined as never;
/**
* @param alias Unique identifier for the token,
* @param defaultConfig Default configuration for the modal,
* @param _desc Description for the token,
* used only for debugging purposes,
* it should but does not need to be unique
*/
constructor(protected alias: string, protected defaultConfig?: UmbModalConfig, protected _desc?: string) {}
/**
* This method must always return the unique alias of the token since that
* will be used to look up the token in the injector.
*
* @returns the unique alias of the token
*/
toString(): string {
return this.alias;
}
public getDefaultConfig(): UmbModalConfig | undefined {
return this.defaultConfig;
}
}

View File

@@ -72,10 +72,7 @@ export class UmbNotificationContext {
* @return {*}
* @memberof UmbNotificationContext
*/
public peek(
color: UmbNotificationColor,
options: UmbNotificationOptions
): UmbNotificationHandler {
public peek(color: UmbNotificationColor, options: UmbNotificationOptions): UmbNotificationHandler {
return this._open({ color, ...options });
}
@@ -86,10 +83,7 @@ export class UmbNotificationContext {
* @return {*}
* @memberof UmbNotificationContext
*/
public stay(
color: UmbNotificationColor,
options: UmbNotificationOptions
): UmbNotificationHandler {
public stay(color: UmbNotificationColor, options: UmbNotificationOptions): UmbNotificationHandler {
return this._open({ ...options, color, duration: null });
}
}

View File

@@ -1,4 +1,4 @@
import { Meta } from '@storybook/addon-docs';
import { Meta } from '@storybook/blocks';
<Meta title="API/Notifications/Intro" />
@@ -18,7 +18,7 @@ Stays on the screen until dismissed by the user or custom code. Stay notificatio
The UmbNotification context can be used to open notifications.
```typescript
```ts
import { html, LitElement } from 'lit';
import { UmbLitElement } from '@umbraco-cms/element';
import type { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_ALIAS } from '@umbraco-cms/notification';
@@ -41,7 +41,7 @@ class MyElement extends UmbLitElement {
A notification is opened by calling one of the helper methods on the UmbNotificationContext. The methods will return an instance of UmbNotificationHandler.
```typescript
```ts
import { html, LitElement } from 'lit';
import { state } from 'lit/decorators.js';
import { UmbLitElement } from '@umbraco-cms/element';
@@ -84,7 +84,7 @@ The default layout will cover most cases, but there might be situations where we
### Custom layout element
```typescript
```ts
import { html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import { UUITextStyles } from '@umbraco-ui/uui-css';
@@ -123,7 +123,7 @@ export class UmbNotificationLayoutCustom extends LitElement {
### Open notification with custom layout
```typescript
```ts
import { html, LitElement } from 'lit';
import { UmbContextInjectMixin } from '@umbraco-cms/context-api';
import type {

View File

@@ -10,7 +10,6 @@ import { pushToUniqueArray } from './push-to-unique-array.function';
*
* The ArrayState provides methods to append data when the data is an Object.
*/
export class ArrayState<T> extends DeepState<T[]> {
constructor(initialData: T[], private _getUnique?: (entry: T) => unknown) {
super(initialData);

View File

@@ -0,0 +1,13 @@
import { BasicState } from './basic-state';
/**
* @export
* @class BooleanState
* @extends {BehaviorSubject<T>}
* @description - A RxJS BehaviorSubject this Subject ensures the data is unique, not updating any Observes unless there is an actual change of the value.
*/
export class BooleanState<T> extends BasicState<T | boolean> {
constructor(initialData: T | boolean) {
super(initialData);
}
}

View File

@@ -1,5 +1,6 @@
export * from './observer.controller';
export * from './observer';
export * from './boolean-state';
export * from './number-state';
export * from './string-state';
export * from './class-state';