diff --git a/src/Umbraco.Web.UI.Client/.storybook/main.ts b/src/Umbraco.Web.UI.Client/.storybook/main.ts index 1904aec0d2..58d9e365e4 100644 --- a/src/Umbraco.Web.UI.Client/.storybook/main.ts +++ b/src/Umbraco.Web.UI.Client/.storybook/main.ts @@ -12,7 +12,7 @@ const config: StorybookConfig = { check: true, }, docs: { - autodocs: 'tag' + autodocs: true, }, }; export default config; diff --git a/src/Umbraco.Web.UI.Client/.storybook/preview.js b/src/Umbraco.Web.UI.Client/.storybook/preview.js index 943845e252..ae238d9981 100644 --- a/src/Umbraco.Web.UI.Client/.storybook/preview.js +++ b/src/Umbraco.Web.UI.Client/.storybook/preview.js @@ -13,32 +13,32 @@ import { setCustomElements } from '@storybook/web-components'; import { UmbDataTypeStore } from '../src/backoffice/settings/data-types/repository/data-type.store.ts'; import { UmbDocumentTypeStore } from '../src/backoffice/documents/document-types/repository/document-type.store.ts'; +import { UmbDocumentStore } from '../src/backoffice/documents/documents/repository/document.store.ts'; +import { UmbDocumentTreeStore } from '../src/backoffice/documents/documents/repository/document.tree.store.ts'; import customElementManifests from '../custom-elements.json'; import { UmbIconStore } from '../libs/store/icon/icon.store'; import { onUnhandledRequest } from '../src/core/mocks/browser'; import { handlers } from '../src/core/mocks/browser-handlers'; -import { LitElement } from 'lit'; -import { UMB_MODAL_CONTEXT_TOKEN, UmbModalContext } from '../src/core/modal'; - -// TODO: Fix storybook manifest registrations. +import { UMB_MODAL_CONTEXT_TOKEN, UmbModalContext } from '../libs/modal'; +import { UmbLitElement } from '../libs/element'; import { umbExtensionsRegistry } from '../libs/extensions-api'; import '../src/core/context-provider/context-provider.element'; import '../src/core/controller-host/controller-host-test.element'; -import '../src/backoffice/shared/components/backoffice-frame/backoffice-notification-container.element'; -import '../src/backoffice/shared/components/backoffice-frame/backoffice-modal-container.element'; -import '../src/backoffice/shared/components/code-block/code-block.element'; -import '../src/backoffice/shared/components/workspace/workspace-layout/workspace-layout.element'; import '../src/backoffice/shared/components'; -class UmbStoryBookElement extends LitElement { +import { manifests as documentManifests } from '../src/backoffice/documents'; + +class UmbStoryBookElement extends UmbLitElement { _umbIconStore = new UmbIconStore(); constructor() { super(); this._umbIconStore.attach(this); + this._registerExtensions(documentManifests); + this.provideContext(UMB_MODAL_CONTEXT_TOKEN, new UmbModalContext(this)); } _registerExtensions(manifests) { @@ -49,7 +49,8 @@ class UmbStoryBookElement extends LitElement { } render() { - return html``; + return html` + `; } } @@ -57,7 +58,6 @@ customElements.define('umb-storybook', UmbStoryBookElement); const storybookProvider = (story) => html` ${story()} `; -// TODO: Stop using this context provider element. If we need to continue this path, then we should make a new element which just has a create method that can be used to spin up code. This is because our ContextAPIs provide them self. so no need for a provider element. just a element. const dataTypeStoreProvider = (story) => html` new UmbDataTypeStore(host)}>${story()} `; @@ -66,14 +66,12 @@ const documentTypeStoreProvider = (story) => html` new UmbDocumentTypeStore(host)}>${story()} `; -const modalContextProvider = (story) => html` - - ${story()} - - +const documentStoreProvider = (story) => html` + new UmbDocumentStore(host)}>${story()} +`; + +const documentTreeStoreProvider = (story) => html` + new UmbDocumentTreeStore(host)}>${story()} `; // Initialize MSW @@ -83,9 +81,10 @@ initialize({ onUnhandledRequest }); export const decorators = [ mswDecorator, storybookProvider, + documentStoreProvider, + documentTreeStoreProvider, dataTypeStoreProvider, documentTypeStoreProvider, - modalContextProvider, ]; export const parameters = { @@ -93,7 +92,28 @@ export const parameters = { storySort: { method: 'alphabetical', includeNames: true, - order: ['Guides', ['Getting Started'], '*'], + order: [ + 'Guides', + [ + 'Getting started', + 'Extending the Backoffice', + [ + 'Intro', + 'Registration', + 'Header Apps', + 'Sections', + ['Intro', 'Sidebar', 'Views', '*'], + 'Entity Actions', + 'Workspaces', + ['Intro', 'Views', 'Actions', '*'], + 'Property Editors', + 'Repositories', + '*', + ], + '*', + ], + '*', + ], }, }, actions: { argTypesRegex: '^on.*' }, diff --git a/src/Umbraco.Web.UI.Client/.vscode/settings.json b/src/Umbraco.Web.UI.Client/.vscode/settings.json index a33b02ae5b..196b1d2b83 100644 --- a/src/Umbraco.Web.UI.Client/.vscode/settings.json +++ b/src/Umbraco.Web.UI.Client/.vscode/settings.json @@ -1,4 +1,6 @@ { "cssVariables.lookupFiles": ["node_modules/@umbraco-ui/uui-css/dist/custom-properties.css"], - "cSpell.words": ["combobox", "variantable"] + "cSpell.words": ["combobox", "variantable"], + "exportall.config.folderListener": [], + "exportall.config.relExclusion": [] } diff --git a/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete/delete.action.ts b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete/delete.action.ts index bba0636c12..e6d9deb818 100644 --- a/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete/delete.action.ts +++ b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete/delete.action.ts @@ -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); } } } diff --git a/src/Umbraco.Web.UI.Client/libs/entity-action/actions/trash/trash.action.ts b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/trash/trash.action.ts index a72b543ac8..36ade2fef0 100644 --- a/src/Umbraco.Web.UI.Client/libs/entity-action/actions/trash/trash.action.ts +++ b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/trash/trash.action.ts @@ -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]); }); } } diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-api/registry/extension.registry.ts b/src/Umbraco.Web.UI.Client/libs/extensions-api/registry/extension.registry.ts index c5e056f35e..14dfe2cc9e 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-api/registry/extension.registry.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-api/registry/extension.registry.ts @@ -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 ? ManifestTypeMap[T] diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-api/umb-lifecycle.interface.ts b/src/Umbraco.Web.UI.Client/libs/extensions-api/umb-lifecycle.interface.ts index 8486453312..c90a871320 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-api/umb-lifecycle.interface.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-api/umb-lifecycle.interface.ts @@ -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 diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/modal.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/modal.models.ts new file mode 100644 index 0000000000..901a998bf5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/modal.models.ts @@ -0,0 +1,5 @@ +import type { ManifestElement } from './models'; + +export interface ManifestModal extends ManifestElement { + type: 'modal'; +} diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts index 4e9bd27adf..b77b89de5e 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts @@ -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 extends ManifestBase { loader?: () => Promise; } -export interface ManifestClass extends ManifestWithLoader { +export interface ManifestClass extends ManifestWithLoader { type: ManifestStandardTypes; js?: string; className?: string; - class?: ClassConstructor; + class?: ClassConstructor; //loader?: () => Promise; } diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/store.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/store.models.ts new file mode 100644 index 0000000000..35a156c466 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/store.models.ts @@ -0,0 +1,10 @@ +import type { ManifestClass } from './models'; +import { UmbStoreBase, UmbTreeStoreBase } from '@umbraco-cms/store'; + +export interface ManifestStore extends ManifestClass { + type: 'store'; +} + +export interface ManifestTreeStore extends ManifestClass { + type: 'treeStore'; +} diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout-picker-base.ts b/src/Umbraco.Web.UI.Client/libs/modal/elements/modal-element-picker-base.ts similarity index 75% rename from src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout-picker-base.ts rename to src/Umbraco.Web.UI.Client/libs/modal/elements/modal-element-picker-base.ts index 7c84bd0a43..8e7ff3d404 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout-picker-base.ts +++ b/src/Umbraco.Web.UI.Client/libs/modal/elements/modal-element-picker-base.ts @@ -1,5 +1,6 @@ import { property } from 'lit/decorators.js'; -import { UmbModalLayoutElement } from '..'; +import { UmbModalBaseElement } from '..'; +import './modal-element.element'; export interface UmbPickerModalData { multiple: boolean; @@ -7,9 +8,13 @@ export interface UmbPickerModalData { filter?: (language: T) => boolean; } +export interface UmbPickerModalResult { + selection: Array; +} + // 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 UmbModalLayoutPickerBase extends UmbModalLayoutElement> { +export class UmbModalElementPickerBase extends UmbModalBaseElement, UmbPickerModalResult> { @property() selection: Array = []; @@ -19,11 +24,11 @@ export class UmbModalLayoutPickerBase extends UmbModalLayoutElement extends UmbLitElement { + @property({ attribute: false }) + modalHandler?: UmbModalHandler; + + @property({ type: Object, attribute: false }) + data?: UmbModalData; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-modal-element': UmbModalBaseElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/libs/modal/index.ts b/src/Umbraco.Web.UI.Client/libs/modal/index.ts new file mode 100644 index 0000000000..5882bda117 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/modal/index.ts @@ -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'; diff --git a/src/Umbraco.Web.UI.Client/libs/modal/modal-handler.ts b/src/Umbraco.Web.UI.Client/libs/modal/modal-handler.ts new file mode 100644 index 0000000000..bf46856f13 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/modal/modal-handler.ts @@ -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 = Omit< + UmbModalHandlerClass, + 'submit' +> & + OptionalSubmitArgumentIfUndefined; + +// 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 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 { + private _submitPromise: Promise; + private _submitResolver?: (value: ModalResult) => void; + private _submitRejecter?: () => void; + #host: UmbControllerHostInterface; + + public modalElement: UUIModalDialogElement | UUIModalSidebarElement; + + #innerElement = new BehaviorSubject(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, + 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 { + 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); + } + } +} diff --git a/src/Umbraco.Web.UI.Client/libs/modal/modal.context.ts b/src/Umbraco.Web.UI.Client/libs/modal/modal.context.ts new file mode 100644 index 0000000000..aacb0f1646 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/modal/modal.context.ts @@ -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(>>[]); + 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} [options] + * @return {*} {UmbModalHandler} + * @memberof UmbModalContext + */ + public open( + modalAlias: string | UmbModalToken, + 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) { + modalHandler.modalElement.removeEventListener('close-end', () => this.#onCloseEnd(modalHandler)); + this.#remove(modalHandler.key); + } +} + +export const UMB_MODAL_CONTEXT_TOKEN = new UmbContextToken('UmbModalContext'); diff --git a/src/Umbraco.Web.UI.Client/libs/modal/stories/modal.mdx b/src/Umbraco.Web.UI.Client/libs/modal/stories/modal.mdx new file mode 100644 index 0000000000..9cb19bc473 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/modal/stories/modal.mdx @@ -0,0 +1,145 @@ +import { Meta } from '@storybook/blocks'; + + + +# 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``; + } +} +``` + +## 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('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; + + 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` +
+

My Modal

+ + +
+ `; + } +} +``` diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/stories/modal.stories.ts b/src/Umbraco.Web.UI.Client/libs/modal/stories/modal.stories.ts similarity index 58% rename from src/Umbraco.Web.UI.Client/src/core/modal/stories/modal.stories.ts rename to src/Umbraco.Web.UI.Client/libs/modal/stories/modal.stories.ts index e03fe93493..a1002bf1cf 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/stories/modal.stories.ts +++ b/src/Umbraco.Web.UI.Client/libs/modal/stories/modal.stories.ts @@ -7,13 +7,16 @@ export default { argTypes: { modalLayout: { control: 'select', - options: ['Confirm', 'Content Picker', 'Property Editor UI Picker', 'Icon Picker'], + //options: ['Confirm', 'Content Picker', 'Property Editor UI Picker', 'Icon Picker'], }, }, } as Meta; const Template: Story = (props) => { - return html` `; + return html` + Under construction + + `; }; export const Overview = Template.bind({}); diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/stories/story-modal-service-example.element.ts b/src/Umbraco.Web.UI.Client/libs/modal/stories/story-modal-service-example.element.ts similarity index 91% rename from src/Umbraco.Web.UI.Client/src/core/modal/stories/story-modal-service-example.element.ts rename to src/Umbraco.Web.UI.Client/libs/modal/stories/story-modal-service-example.element.ts index bd33181774..3a84115b8d 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/stories/story-modal-service-example.element.ts +++ b/src/Umbraco.Web.UI.Client/libs/modal/stories/story-modal-service-example.element.ts @@ -21,9 +21,11 @@ export class StoryModalContextExampleElement extends UmbLitElement { } private _open() { + // TODO: use the extension registry to get all modals + /* switch (this.modalLayout) { case 'Content Picker': - this._modalContext?.contentPicker(); + this._modalContext?.documentPicker(); break; case 'Property Editor UI Picker': this._modalContext?.propertyEditorUIPicker(); @@ -38,6 +40,7 @@ export class StoryModalContextExampleElement extends UmbLitElement { }); break; } + */ } render() { diff --git a/src/Umbraco.Web.UI.Client/libs/modal/token/modal-token.ts b/src/Umbraco.Web.UI.Client/libs/modal/token/modal-token.ts new file mode 100644 index 0000000000..ab466d34d7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/modal/token/modal-token.ts @@ -0,0 +1,48 @@ +import { UmbModalConfig } from '../modal.context'; + +export class UmbModalToken { + /** + * 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; + } +} diff --git a/src/Umbraco.Web.UI.Client/libs/notification/notification.context.ts b/src/Umbraco.Web.UI.Client/libs/notification/notification.context.ts index d48b21678a..e3aad5ccd4 100644 --- a/src/Umbraco.Web.UI.Client/libs/notification/notification.context.ts +++ b/src/Umbraco.Web.UI.Client/libs/notification/notification.context.ts @@ -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 }); } } diff --git a/src/Umbraco.Web.UI.Client/libs/notification/stories/notification.mdx b/src/Umbraco.Web.UI.Client/libs/notification/stories/notification.mdx index 5d60846fc4..0755521f77 100644 --- a/src/Umbraco.Web.UI.Client/libs/notification/stories/notification.mdx +++ b/src/Umbraco.Web.UI.Client/libs/notification/stories/notification.mdx @@ -1,4 +1,4 @@ -import { Meta } from '@storybook/addon-docs'; +import { Meta } from '@storybook/blocks'; @@ -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 { diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/array-state.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/array-state.ts index 81ede2a7ea..d421e4d6b5 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/array-state.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/array-state.ts @@ -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 extends DeepState { constructor(initialData: T[], private _getUnique?: (entry: T) => unknown) { super(initialData); diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/boolean-state.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/boolean-state.ts new file mode 100644 index 0000000000..5404d43651 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/boolean-state.ts @@ -0,0 +1,13 @@ +import { BasicState } from './basic-state'; + +/** + * @export + * @class BooleanState + * @extends {BehaviorSubject} + * @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 extends BasicState { + constructor(initialData: T | boolean) { + super(initialData); + } +} diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/index.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/index.ts index 5ed8fc803a..dbfa7cf4b5 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/index.ts @@ -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'; diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 3698a72442..9228699b0d 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -2018,70 +2018,6 @@ "react": ">=16.8.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz", - "integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz", - "integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz", - "integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz", - "integrity": "sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/darwin-x64": { "version": "0.16.17", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz", @@ -2098,278 +2034,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz", - "integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz", - "integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz", - "integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz", - "integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz", - "integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz", - "integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz", - "integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz", - "integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz", - "integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz", - "integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz", - "integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz", - "integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz", - "integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz", - "integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz", - "integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz", - "integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz", - "integrity": "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint/eslintrc": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.0.tgz", @@ -6987,22 +6651,6 @@ "node": ">=10.0.0" } }, - "node_modules/@web/dev-server-esbuild/node_modules/@esbuild/linux-loong64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", - "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@web/dev-server-esbuild/node_modules/esbuild": { "version": "0.14.54", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", @@ -9619,38 +9267,6 @@ "@esbuild/win32-x64": "0.16.17" } }, - "node_modules/esbuild-android-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", - "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-android-arm64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", - "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/esbuild-darwin-64": { "version": "0.14.54", "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", @@ -9667,214 +9283,6 @@ "node": ">=12" } }, - "node_modules/esbuild-darwin-arm64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", - "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-freebsd-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", - "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-freebsd-arm64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", - "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-32": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", - "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", - "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-arm": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", - "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-arm64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", - "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-mips64le": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", - "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-ppc64le": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", - "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-riscv64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", - "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-s390x": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", - "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-netbsd-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", - "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-openbsd-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", - "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/esbuild-plugin-alias": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/esbuild-plugin-alias/-/esbuild-plugin-alias-0.2.1.tgz", @@ -9893,70 +9301,6 @@ "esbuild": ">=0.12 <1" } }, - "node_modules/esbuild-sunos-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", - "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-windows-32": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", - "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-windows-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", - "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-windows-arm64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", - "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -20065,34 +19409,6 @@ "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==", "dev": true }, - "@esbuild/android-arm": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz", - "integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz", - "integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz", - "integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz", - "integrity": "sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==", - "dev": true, - "optional": true - }, "@esbuild/darwin-x64": { "version": "0.16.17", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz", @@ -20100,125 +19416,6 @@ "dev": true, "optional": true }, - "@esbuild/freebsd-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz", - "integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz", - "integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz", - "integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz", - "integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz", - "integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz", - "integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz", - "integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz", - "integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz", - "integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz", - "integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz", - "integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz", - "integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz", - "integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz", - "integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz", - "integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz", - "integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz", - "integrity": "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==", - "dev": true, - "optional": true - }, "@eslint/eslintrc": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.0.tgz", @@ -23952,13 +23149,6 @@ "ua-parser-js": "^1.0.2" }, "dependencies": { - "@esbuild/linux-loong64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", - "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", - "dev": true, - "optional": true - }, "esbuild": { "version": "0.14.54", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", @@ -25981,20 +25171,6 @@ "@esbuild/win32-x64": "0.16.17" } }, - "esbuild-android-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", - "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", - "dev": true, - "optional": true - }, - "esbuild-android-arm64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", - "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", - "dev": true, - "optional": true - }, "esbuild-darwin-64": { "version": "0.14.54", "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", @@ -26002,97 +25178,6 @@ "dev": true, "optional": true }, - "esbuild-darwin-arm64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", - "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", - "dev": true, - "optional": true - }, - "esbuild-freebsd-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", - "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", - "dev": true, - "optional": true - }, - "esbuild-freebsd-arm64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", - "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", - "dev": true, - "optional": true - }, - "esbuild-linux-32": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", - "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", - "dev": true, - "optional": true - }, - "esbuild-linux-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", - "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", - "dev": true, - "optional": true - }, - "esbuild-linux-arm": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", - "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", - "dev": true, - "optional": true - }, - "esbuild-linux-arm64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", - "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", - "dev": true, - "optional": true - }, - "esbuild-linux-mips64le": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", - "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", - "dev": true, - "optional": true - }, - "esbuild-linux-ppc64le": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", - "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", - "dev": true, - "optional": true - }, - "esbuild-linux-riscv64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", - "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", - "dev": true, - "optional": true - }, - "esbuild-linux-s390x": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", - "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", - "dev": true, - "optional": true - }, - "esbuild-netbsd-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", - "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", - "dev": true, - "optional": true - }, - "esbuild-openbsd-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", - "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", - "dev": true, - "optional": true - }, "esbuild-plugin-alias": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/esbuild-plugin-alias/-/esbuild-plugin-alias-0.2.1.tgz", @@ -26108,34 +25193,6 @@ "debug": "^4.3.4" } }, - "esbuild-sunos-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", - "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", - "dev": true, - "optional": true - }, - "esbuild-windows-32": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", - "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", - "dev": true, - "optional": true - }, - "esbuild-windows-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", - "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", - "dev": true, - "optional": true - }, - "esbuild-windows-arm64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", - "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", - "dev": true, - "optional": true - }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 4c618010b5..b28138d72b 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -38,6 +38,7 @@ "backoffice:test:e2e": "npx playwright test", "test:e2e": "npm run auth:test:e2e && npm run backoffice:test:e2e", "lint": "eslint src apps libs e2e", + "lint:errors": "npm run lint -- --quiet", "lint:fix": "npm run lint -- --fix", "format": "prettier 'src/**/*.ts'", "format:fix": "npm run format -- --write", diff --git a/src/Umbraco.Web.UI.Client/public-assets/docs/data-flow.svg b/src/Umbraco.Web.UI.Client/public-assets/docs/data-flow.svg new file mode 100644 index 0000000000..e7a8481c17 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/docs/data-flow.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/public-assets/docs/entity-action-collection-menu.svg b/src/Umbraco.Web.UI.Client/public-assets/docs/entity-action-collection-menu.svg new file mode 100644 index 0000000000..d9918b302a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/docs/entity-action-collection-menu.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/public-assets/docs/entity-action-picker-context-menu.svg b/src/Umbraco.Web.UI.Client/public-assets/docs/entity-action-picker-context-menu.svg new file mode 100644 index 0000000000..6a3daa0be3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/docs/entity-action-picker-context-menu.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/public-assets/docs/entity-action-sidebar-context.svg b/src/Umbraco.Web.UI.Client/public-assets/docs/entity-action-sidebar-context.svg new file mode 100644 index 0000000000..5e0e9b44c2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/docs/entity-action-sidebar-context.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/public-assets/docs/entity-action-workspace-menu.svg b/src/Umbraco.Web.UI.Client/public-assets/docs/entity-action-workspace-menu.svg new file mode 100644 index 0000000000..483aededf3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/docs/entity-action-workspace-menu.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/public-assets/docs/entity-bulk-action-collection-menu.svg b/src/Umbraco.Web.UI.Client/public-assets/docs/entity-bulk-action-collection-menu.svg new file mode 100644 index 0000000000..71b4e5eb5c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/docs/entity-bulk-action-collection-menu.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/public-assets/docs/header-apps.svg b/src/Umbraco.Web.UI.Client/public-assets/docs/header-apps.svg new file mode 100644 index 0000000000..c9054b02e1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/docs/header-apps.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/public-assets/docs/menu-item.svg b/src/Umbraco.Web.UI.Client/public-assets/docs/menu-item.svg new file mode 100644 index 0000000000..89f8731ca3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/docs/menu-item.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/public-assets/docs/menu.svg b/src/Umbraco.Web.UI.Client/public-assets/docs/menu.svg new file mode 100644 index 0000000000..27c6acea65 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/docs/menu.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/public-assets/docs/section-menu-sidebar-app.svg b/src/Umbraco.Web.UI.Client/public-assets/docs/section-menu-sidebar-app.svg new file mode 100644 index 0000000000..393ab5ec2d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/docs/section-menu-sidebar-app.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/public-assets/docs/section-sidebar-apps.svg b/src/Umbraco.Web.UI.Client/public-assets/docs/section-sidebar-apps.svg new file mode 100644 index 0000000000..e781bc9900 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/docs/section-sidebar-apps.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/public-assets/docs/section-sidebar-composed-apps.svg b/src/Umbraco.Web.UI.Client/public-assets/docs/section-sidebar-composed-apps.svg new file mode 100644 index 0000000000..4081881518 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/docs/section-sidebar-composed-apps.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/public-assets/docs/section-sidebar.svg b/src/Umbraco.Web.UI.Client/public-assets/docs/section-sidebar.svg new file mode 100644 index 0000000000..f88666f33e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/docs/section-sidebar.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/public-assets/docs/section-views.svg b/src/Umbraco.Web.UI.Client/public-assets/docs/section-views.svg new file mode 100644 index 0000000000..45c88317b9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/docs/section-views.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/public-assets/docs/section.svg b/src/Umbraco.Web.UI.Client/public-assets/docs/section.svg new file mode 100644 index 0000000000..a525f169bf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/docs/section.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/public-assets/docs/workspace-actions.svg b/src/Umbraco.Web.UI.Client/public-assets/docs/workspace-actions.svg new file mode 100644 index 0000000000..7ddeb6d9f3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/docs/workspace-actions.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/public-assets/docs/workspace-views.svg b/src/Umbraco.Web.UI.Client/public-assets/docs/workspace-views.svg new file mode 100644 index 0000000000..0c04fed189 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/docs/workspace-views.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/public-assets/docs/workspace.svg b/src/Umbraco.Web.UI.Client/public-assets/docs/workspace.svg new file mode 100644 index 0000000000..a897bd6281 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/docs/workspace.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 31d1af18bc..75e0738142 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -2,9 +2,6 @@ import { defineElement } from '@umbraco-ui/uui-base/lib/registration'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html } from 'lit'; -import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '../core/modal'; -import { UmbUserStore } from './users/users/user.store'; -import { UmbUserGroupStore } from './users/user-groups/user-group.store'; import { UmbCurrentUserStore, UMB_CURRENT_USER_STORE_CONTEXT_TOKEN } from './users/current-user/current-user.store'; import { UmbCurrentUserHistoryStore, @@ -15,37 +12,14 @@ import { UmbBackofficeContext, UMB_BACKOFFICE_CONTEXT_TOKEN, } from './shared/components/backoffice-frame/backoffice.context'; -import { UmbDocumentTypeStore } from './documents/document-types/repository/document-type.store'; -import { UmbDocumentTypeTreeStore } from './documents/document-types/repository/document-type.tree.store'; -import { UmbMediaTypeDetailStore } from './media/media-types/repository/media-type.detail.store'; -import { UmbMediaTypeTreeStore } from './media/media-types/repository/media-type.tree.store'; -import { UmbDocumentStore } from './documents/documents/repository/document.store'; -import { UmbDocumentTreeStore } from './documents/documents/repository/document.tree.store'; -import { UmbMediaDetailStore } from './media/media/repository/media.detail.store'; -import { UmbMediaTreeStore } from './media/media/repository/media.tree.store'; -import { UmbMemberTypeDetailStore } from './members/member-types/repository/member-type.detail.store'; -import { UmbMemberTypeTreeStore } from './members/member-types/repository/member-type.tree.store'; -import { UmbMemberGroupDetailStore } from './members/member-groups/repository/member-group.detail.store'; -import { UmbMemberGroupTreeStore } from './members/member-groups/repository/member-group.tree.store'; -import { UmbMemberDetailStore } from './members/members/member.detail.store'; -import { UmbMemberTreeStore } from './members/members/repository/member.tree.store'; -import { UmbDictionaryDetailStore } from './translation/dictionary/repository/dictionary.detail.store'; -import { UmbDictionaryTreeStore } from './translation/dictionary/repository/dictionary.tree.store'; -import { UmbDocumentBlueprintDetailStore } from './documents/document-blueprints/document-blueprint.detail.store'; -import { UmbDocumentBlueprintTreeStore } from './documents/document-blueprints/document-blueprint.tree.store'; -import { UmbDataTypeStore } from './settings/data-types/repository/data-type.store'; -import { UmbDataTypeTreeStore } from './settings/data-types/repository/data-type.tree.store'; -import { UmbTemplateTreeStore } from './templating/templates/tree/data/template.tree.store'; -import { UmbTemplateDetailStore } from './templating/templates/workspace/data/template.detail.store'; import { UmbThemeContext } from './themes/theme.context'; -import { UmbLanguageStore } from './settings/languages/repository/language.store'; import { UMB_APP_LANGUAGE_CONTEXT_TOKEN, UmbAppLanguageContext, } from './settings/languages/app-language-select/app-language.context'; -import { UmbPackageStore } from './packages/repository/package.store'; import { UmbServerExtensionController } from './packages/repository/server-extension.controller'; -import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; +import { createExtensionClass, umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/notification'; import { UmbLitElement } from '@umbraco-cms/element'; @@ -81,45 +55,19 @@ export class UmbBackofficeElement extends UmbLitElement { constructor() { super(); - this.provideContext(UMB_MODAL_CONTEXT_TOKEN, new UmbModalContext()); + this.provideContext(UMB_MODAL_CONTEXT_TOKEN, new UmbModalContext(this)); this.provideContext(UMB_NOTIFICATION_CONTEXT_TOKEN, new UmbNotificationContext()); - - // TODO: find a way this is possible outside this element. It needs to be possible to register stores in extensions this.provideContext(UMB_CURRENT_USER_STORE_CONTEXT_TOKEN, new UmbCurrentUserStore()); - - new UmbDocumentStore(this); - new UmbDocumentTreeStore(this); - new UmbMediaDetailStore(this); - new UmbMediaTreeStore(this); - new UmbDataTypeStore(this); - new UmbDataTypeTreeStore(this); - new UmbUserStore(this); - new UmbMediaTypeDetailStore(this); - new UmbMediaTypeTreeStore(this); - new UmbDocumentTypeStore(this); - new UmbDocumentTypeTreeStore(this); - new UmbMemberTypeDetailStore(this); - new UmbMemberTypeTreeStore(this); - new UmbUserGroupStore(this); - new UmbMemberGroupDetailStore(this); - new UmbMemberGroupTreeStore(this); - new UmbMemberDetailStore(this); - new UmbMemberTreeStore(this); - new UmbDictionaryDetailStore(this); - new UmbDictionaryTreeStore(this); - new UmbDocumentBlueprintDetailStore(this); - new UmbDocumentBlueprintTreeStore(this); - new UmbTemplateTreeStore(this); - new UmbTemplateDetailStore(this); - new UmbLanguageStore(this); - this.provideContext(UMB_APP_LANGUAGE_CONTEXT_TOKEN, new UmbAppLanguageContext(this)); this.provideContext(UMB_BACKOFFICE_CONTEXT_TOKEN, new UmbBackofficeContext()); - this.provideContext(UMB_CURRENT_USER_HISTORY_STORE_CONTEXT_TOKEN, new UmbCurrentUserHistoryStore()); new UmbThemeContext(this); - - new UmbPackageStore(this); new UmbServerExtensionController(this, umbExtensionsRegistry); + this.provideContext(UMB_CURRENT_USER_HISTORY_STORE_CONTEXT_TOKEN, new UmbCurrentUserHistoryStore()); + + // Register All Stores + this.observe(umbExtensionsRegistry.extensionsOfTypes(['store', 'treeStore']), (stores) => { + stores.forEach((store) => createExtensionClass(store, [this])); + }); } render() { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.element.ts index e18076cf58..cff6bf9108 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.element.ts @@ -2,7 +2,8 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html, nothing } from 'lit'; import { customElement, state, query, property } from 'lit/decorators.js'; import { UUIButtonState, UUIPaginationElement, UUIPaginationEvent } from '@umbraco-ui/uui'; -import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '../../../../core/modal'; +import { UMB_CONFIRM_MODAL_TOKEN } from '../../../shared/modals/confirm'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; import { UmbLitElement } from '@umbraco-cms/element'; import { RedirectManagementResource, RedirectStatusModel, RedirectUrlModel } from '@umbraco-cms/backend-api'; import { tryExecuteAndNotify } from '@umbraco-cms/resources'; @@ -126,7 +127,7 @@ export class UmbDashboardRedirectManagementElement extends UmbLitElement { } private _removeRedirectHandler(data: RedirectUrlModel) { - const modalHandler = this._modalContext?.confirm({ + const modalHandler = this._modalContext?.open(UMB_CONFIRM_MODAL_TOKEN, { headline: 'Delete', content: html`
@@ -139,8 +140,8 @@ export class UmbDashboardRedirectManagementElement extends UmbLitElement { color: 'danger', confirmLabel: 'Delete', }); - modalHandler?.onClose().then(({ confirmed }) => { - if (confirmed) this._removeRedirect(data); + modalHandler?.onSubmit().then(() => { + this._removeRedirect(data); }); } @@ -157,14 +158,14 @@ export class UmbDashboardRedirectManagementElement extends UmbLitElement { } private _disableRedirectHandler() { - const modalHandler = this._modalContext?.confirm({ + const modalHandler = this._modalContext?.open(UMB_CONFIRM_MODAL_TOKEN, { headline: 'Disable URL tracker', content: html`Are you sure you want to disable the URL tracker?`, color: 'danger', confirmLabel: 'Disable', }); - modalHandler?.onClose().then(({ confirmed }) => { - if (confirmed) this._toggleRedirect(); + modalHandler?.onSubmit().then(() => { + this._toggleRedirect(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.detail.store.ts index 1e27078ee3..c847dbb2cb 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.detail.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.detail.store.ts @@ -4,32 +4,25 @@ import { ArrayState } from '@umbraco-cms/observable-api'; import { UmbStoreBase } from '@umbraco-cms/store'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; - -export const UMB_DocumentBlueprint_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbDocumentBlueprintDetailStore'); - - /** * @export - * @class UmbDocumentBlueprintDetailStore + * @class UmbDocumentBlueprintStore * @extends {UmbStoreBase} - * @description - Details Data Store for Document Blueprints + * @description - Data Store for Document Blueprints */ -export class UmbDocumentBlueprintDetailStore extends UmbStoreBase { - - +export class UmbDocumentBlueprintStore extends UmbStoreBase { // TODO: use the right type: #data = new ArrayState([], (x) => x.key); - constructor(host: UmbControllerHostInterface) { - super(host, UMB_DocumentBlueprint_DETAIL_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_DOCUMENT_BLUEPRINT_STORE_CONTEXT_TOKEN.toString()); } /** * @description - Request a Data Type by key. The Data Type is added to the store and is returned as an Observable. * @param {string} key * @return {*} {(Observable)} - * @memberof UmbDocumentBlueprintDetailStore + * @memberof UmbDocumentBlueprintStore */ getByKey(key: string) { // TODO: use backend cli when available. @@ -39,21 +32,18 @@ export class UmbDocumentBlueprintDetailStore extends UmbStoreBase { this.#data.append(data); }); - return this.#data.getObservablePart((documents) => - documents.find((document) => document.key === key) - ); + return this.#data.getObservablePart((documents) => documents.find((document) => document.key === key)); } getScaffold(entityType: string, parentKey: string | null) { - return { - } as DocumentBlueprintDetails; + return {} as DocumentBlueprintDetails; } // TODO: make sure UI somehow can follow the status of this action. /** * @description - Save a DocumentBlueprint. * @param {Array} Dictionaries - * @memberof UmbDocumentBlueprintDetailStore + * @memberof UmbDocumentBlueprintStore * @return {*} {Promise} */ save(data: DocumentBlueprintDetails[]) { @@ -86,7 +76,7 @@ export class UmbDocumentBlueprintDetailStore extends UmbStoreBase { /** * @description - Delete a Data Type. * @param {string[]} keys - * @memberof UmbDocumentBlueprintDetailStore + * @memberof UmbDocumentBlueprintStore * @return {*} {Promise} */ async delete(keys: string[]) { @@ -102,3 +92,7 @@ export class UmbDocumentBlueprintDetailStore extends UmbStoreBase { this.#data.remove(keys); } } + +export const UMB_DOCUMENT_BLUEPRINT_STORE_CONTEXT_TOKEN = new UmbContextToken( + 'UmbDocumentBlueprintStore' +); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.tree.store.ts index 89759657aa..08aa3e6b3e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.tree.store.ts @@ -1,11 +1,8 @@ -import { DocumentBlueprintResource, DocumentTreeItemModel } from '@umbraco-cms/backend-api'; -import { tryExecuteAndNotify } from '@umbraco-cms/resources'; import { UmbContextToken } from '@umbraco-cms/context-api'; -import { ArrayState } from '@umbraco-cms/observable-api'; -import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbTreeStoreBase } from '@umbraco-cms/store'; import type { UmbControllerHostInterface } from '@umbraco-cms/controller'; -export const UMB_DocumentBlueprint_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken( +export const UMB_DOCUMENT_BLUEPRINT_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken( 'UmbDocumentBlueprintTreeStore' ); @@ -15,81 +12,8 @@ export const UMB_DocumentBlueprint_TREE_STORE_CONTEXT_TOKEN = new UmbContextToke * @extends {UmbStoreBase} * @description - Tree Data Store for Document Blueprints */ -export class UmbDocumentBlueprintTreeStore extends UmbStoreBase { - #data = new ArrayState([], (x) => x.key); - +export class UmbDocumentBlueprintTreeStore extends UmbTreeStoreBase { constructor(host: UmbControllerHostInterface) { - super(host, UMB_DocumentBlueprint_TREE_STORE_CONTEXT_TOKEN.toString()); - } - - // TODO: How can we avoid having this in both stores? - /** - * @description - Delete a Document Blueprint Type. - * @param {string[]} keys - * @memberof UmbDocumentBlueprintsStore - * @return {*} {Promise} - */ - async delete(keys: string[]) { - // TODO: use backend cli when available. - await fetch('/umbraco/backoffice/data-type/delete', { - method: 'POST', - body: JSON.stringify(keys), - headers: { - 'Content-Type': 'application/json', - }, - }); - - this.#data.remove(keys); - } - - getTreeRoot() { - tryExecuteAndNotify(this._host, DocumentBlueprintResource.getTreeDocumentBlueprintRoot({})).then(({ data }) => { - if (data) { - // TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)? - this.#data.append(data.items); - } - }); - - // TODO: how do we handle trashed items? - // TODO: remove ignore when we know how to handle trashed items. - return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === null && !item.isTrashed)); - } - - getTreeItemChildren(key: string) { - /* - tryExecuteAndNotify( - this._host, - DocumentBlueprintResource.getTreeDocumentBlueprintChildren({ - parentKey: key, - }) - ).then(({ data }) => { - if (data) { - // TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)? - this.#data.append(data.items); - } - }); - */ - - // TODO: how do we handle trashed items? - // TODO: remove ignore when we know how to handle trashed items. - return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === key && !item.isTrashed)); - } - - getTreeItems(keys: Array) { - if (keys?.length > 0) { - tryExecuteAndNotify( - this._host, - DocumentBlueprintResource.getTreeDocumentBlueprintItem({ - key: keys, - }) - ).then(({ data }) => { - if (data) { - // TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)? - this.#data.append(data); - } - }); - } - - return this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? ''))); + super(host, UMB_DOCUMENT_BLUEPRINT_TREE_STORE_CONTEXT_TOKEN.toString()); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/manifests.ts index 5bee1cd866..4b80c03dbb 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/manifests.ts @@ -1,4 +1,24 @@ +import { UmbDocumentBlueprintStore } from './document-blueprint.detail.store'; +import { UmbDocumentBlueprintTreeStore } from './document-blueprint.tree.store'; import { manifests as menuItemManifests } from './menu-item/manifests'; import { manifests as workspaceManifests } from './workspace/manifests'; +import type { ManifestStore, ManifestTreeStore } from '@umbraco-cms/extensions-registry'; -export const manifests = [...menuItemManifests, ...workspaceManifests]; +export const DOCUMENT_BLUEPRINT_STORE_ALIAS = 'Umb.Store.DocumentBlueprint'; +export const DOCUMENT_BLUEPRINT_TREE_STORE_ALIAS = 'Umb.Store.DocumentBlueprintTree'; + +const store: ManifestStore = { + type: 'store', + alias: DOCUMENT_BLUEPRINT_STORE_ALIAS, + name: 'Document Blueprint Store', + class: UmbDocumentBlueprintStore, +}; + +const treeStore: ManifestTreeStore = { + type: 'treeStore', + alias: DOCUMENT_BLUEPRINT_TREE_STORE_ALIAS, + name: 'Document Blueprint Tree Store', + class: UmbDocumentBlueprintTreeStore, +}; + +export const manifests = [store, treeStore, ...menuItemManifests, ...workspaceManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/manifests.ts index 89c4abbb74..5a6bf52874 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/manifests.ts @@ -1,7 +1,9 @@ import { UmbDocumentTypeRepository } from './document-type.repository'; -import { ManifestRepository } from 'libs/extensions-registry/repository.models'; +import { UmbDocumentTypeStore } from './document-type.store'; +import { UmbDocumentTypeTreeStore } from './document-type.tree.store'; +import { ManifestRepository, ManifestStore, ManifestTreeStore } from '@umbraco-cms/extensions-registry'; -export const DOCUMENT_TYPE_REPOSITORY_ALIAS = 'Umb.Repository.DocumentTypes'; +export const DOCUMENT_TYPE_REPOSITORY_ALIAS = 'Umb.Repository.DocumentType'; const repository: ManifestRepository = { type: 'repository', @@ -10,4 +12,21 @@ const repository: ManifestRepository = { class: UmbDocumentTypeRepository, }; -export const manifests = [repository]; +export const DOCUMENT_TYPE_STORE_ALIAS = 'Umb.Store.DocumentType'; +export const DOCUMENT_TYPE_TREE_STORE_ALIAS = 'Umb.Store.DocumentTypeTree'; + +const store: ManifestStore = { + type: 'store', + alias: DOCUMENT_TYPE_STORE_ALIAS, + name: 'Document Type Store', + class: UmbDocumentTypeStore, +}; + +const treeStore: ManifestTreeStore = { + type: 'treeStore', + alias: DOCUMENT_TYPE_TREE_STORE_ALIAS, + name: 'Document Type Tree Store', + class: UmbDocumentTypeTreeStore, +}; + +export const manifests = [repository, store, treeStore]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.element.ts index fd5c9537ea..5e5a660c77 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.element.ts @@ -3,6 +3,7 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import type { UmbWorkspaceEntityElement } from '../../../shared/components/workspace/workspace-entity-element.interface'; +import { UMB_ICON_PICKER_MODAL_TOKEN } from '../../../shared/modals/icon-picker'; import { UmbWorkspaceDocumentTypeContext } from './document-type-workspace.context'; import type { DocumentTypeModel } from '@umbraco-cms/backend-api'; import { UmbLitElement } from '@umbraco-cms/element'; @@ -86,10 +87,10 @@ export class UmbDocumentTypeWorkspaceElement extends UmbLitElement implements Um } private async _handleIconClick() { - const modalHandler = this._modalContext?.iconPicker(); + const modalHandler = this._modalContext?.open(UMB_ICON_PICKER_MODAL_TOKEN); - modalHandler?.onClose().then((saved) => { - if (saved) this._workspaceContext?.setIcon(saved.icon); + modalHandler?.onSubmit().then((saved) => { + if (saved.icon) this._workspaceContext?.setIcon(saved.icon); // TODO save color ALIAS as well }); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/workspace-view-document-type-design.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/workspace-view-document-type-design.element.ts index 190b93bce4..3e48456f2d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/workspace-view-document-type-design.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/workspace-view-document-type-design.element.ts @@ -4,6 +4,7 @@ import { customElement, state } from 'lit/decorators.js'; import { UmbWorkspaceDocumentTypeContext } from '../../document-type-workspace.context'; import { UmbLitElement } from '@umbraco-cms/element'; import type { DocumentTypeModel } from '@umbraco-cms/backend-api'; +import '../../../../../shared/property-creator/property-creator.element.ts'; @customElement('umb-workspace-view-document-type-design') export class UmbWorkspaceViewDocumentTypeDesignElement extends UmbLitElement { @@ -12,6 +13,7 @@ export class UmbWorkspaceViewDocumentTypeDesignElement extends UmbLitElement { css` :host { display: block; + margin: var(--uui-size-space-6); padding: var(--uui-size-space-6); } `, @@ -41,7 +43,12 @@ export class UmbWorkspaceViewDocumentTypeDesignElement extends UmbLitElement { } render() { - return html`Design of ${this._documentType?.name}`; + return html` Design of ${this._documentType?.name} + +
+ +
+
`; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/create.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/create.action.ts deleted file mode 100644 index d88cca15bb..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/create.action.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { UmbDocumentRepository } from '../repository/document.repository'; -import { UmbEntityActionBase } from '@umbraco-cms/entity-action'; -import { UmbControllerHostInterface } from '@umbraco-cms/controller'; - -export class UmbCreateDocumentEntityAction extends UmbEntityActionBase { - constructor(host: UmbControllerHostInterface, repositoryAlias: string, unique: string) { - super(host, repositoryAlias, unique); - } - - async execute() { - console.log(`execute for: ${this.unique}`); - alert('open create dialog'); - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/create/create-document-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/create/create-document-modal.element.ts new file mode 100644 index 0000000000..e438a3e3e4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/create/create-document-modal.element.ts @@ -0,0 +1,48 @@ +import { html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement } from 'lit/decorators.js'; +import { UmbCreateDocumentModalData, UmbCreateDocumentModalResultData } from '.'; +import { UmbModalBaseElement } from '@umbraco-cms/modal'; + +@customElement('umb-create-document-modal') +export class UmbCreateDocumentModalElement extends UmbModalBaseElement< + UmbCreateDocumentModalData, + UmbCreateDocumentModalResultData +> { + static styles = [UUITextStyles]; + + private _handleCancel() { + this.modalHandler?.reject(); + } + + #onClick(event: PointerEvent) { + event.stopPropagation(); + const target = event.target as HTMLButtonElement; + const documentType = target.value; + this.modalHandler?.submit({ documentType }); + } + + render() { + return html` + +
Render list of create options for ${this.data?.unique}
+ +
    +
  • +
  • +
  • +
+ + Cancel +
+ `; + } +} + +export default UmbCreateDocumentModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-create-document-modal': UmbCreateDocumentModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/create/create.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/create/create.action.ts new file mode 100644 index 0000000000..fcdcc7be3c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/create/create.action.ts @@ -0,0 +1,35 @@ +import { UmbDocumentRepository } from '../../repository/document.repository'; +import type { UmbCreateDocumentModalResultData } from '.'; +import { UMB_CREATE_DOCUMENT_MODAL_TOKEN } from '.'; +import { UmbEntityActionBase } from '@umbraco-cms/entity-action'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; +import { UmbContextConsumerController } from '@umbraco-cms/context-api'; + +// TODO: temp import +import './create-document-modal.element.ts'; + +export class UmbCreateDocumentEntityAction extends UmbEntityActionBase { + #modalContext?: UmbModalContext; + + constructor(host: UmbControllerHostInterface, repositoryAlias: string, unique: string) { + super(host, repositoryAlias, unique); + + new UmbContextConsumerController(this.host, UMB_MODAL_CONTEXT_TOKEN, (instance) => { + this.#modalContext = instance; + }); + } + + async execute() { + // TODO: what to do if modal service is not available? + if (!this.#modalContext) return; + + const modalHandler = this.#modalContext?.open(UMB_CREATE_DOCUMENT_MODAL_TOKEN, { + unique: this.unique, + }); + + // TODO: get type from modal result + const { documentType }: UmbCreateDocumentModalResultData = await modalHandler.onSubmit(); + alert('create document with document type: ' + documentType); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/create/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/create/index.ts new file mode 100644 index 0000000000..679f84e431 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/create/index.ts @@ -0,0 +1,17 @@ +import { UmbModalToken } from '@umbraco-cms/modal'; + +export interface UmbCreateDocumentModalData { + unique: string | null; +} + +export interface UmbCreateDocumentModalResultData { + documentType: string; +} + +export const UMB_CREATE_DOCUMENT_MODAL_TOKEN = new UmbModalToken< + UmbCreateDocumentModalData, + UmbCreateDocumentModalResultData +>('Umb.Modal.CreateDocument', { + type: 'sidebar', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/manifests.ts index 7dc878c0b6..3ae95fe8ee 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-actions/manifests.ts @@ -1,4 +1,5 @@ -import { UmbCreateDocumentEntityAction } from './create.action'; +import { DOCUMENT_REPOSITORY_ALIAS } from '../repository/manifests'; +import { UmbCreateDocumentEntityAction } from './create/create.action'; import { UmbPublishDocumentEntityAction } from './publish.action'; import { UmbDocumentCultureAndHostnamesEntityAction } from './culture-and-hostnames.action'; import { UmbCreateDocumentBlueprintEntityAction } from './create-blueprint.action'; @@ -12,10 +13,9 @@ import { UmbTrashEntityAction, UmbSortChildrenOfEntityAction, } from '@umbraco-cms/entity-action'; -import { ManifestEntityAction } from '@umbraco-cms/extensions-registry'; +import { ManifestEntityAction, ManifestModal } from '@umbraco-cms/extensions-registry'; const entityType = 'document'; -const repositoryAlias = 'Umb.Repository.Documents'; const entityActions: Array = [ { @@ -27,7 +27,7 @@ const entityActions: Array = [ entityType, icon: 'umb:add', label: 'Create', - repositoryAlias, + repositoryAlias: DOCUMENT_REPOSITORY_ALIAS, api: UmbCreateDocumentEntityAction, }, }, @@ -40,7 +40,7 @@ const entityActions: Array = [ entityType, icon: 'umb:trash', label: 'Trash', - repositoryAlias, + repositoryAlias: DOCUMENT_REPOSITORY_ALIAS, api: UmbTrashEntityAction, }, }, @@ -53,7 +53,7 @@ const entityActions: Array = [ entityType, icon: 'umb:blueprint', label: 'Create Content Template', - repositoryAlias, + repositoryAlias: DOCUMENT_REPOSITORY_ALIAS, api: UmbCreateDocumentBlueprintEntityAction, }, }, @@ -66,7 +66,7 @@ const entityActions: Array = [ entityType, icon: 'umb:enter', label: 'Move', - repositoryAlias, + repositoryAlias: DOCUMENT_REPOSITORY_ALIAS, api: UmbMoveEntityAction, }, }, @@ -79,7 +79,7 @@ const entityActions: Array = [ entityType, icon: 'umb:documents', label: 'Copy', - repositoryAlias, + repositoryAlias: DOCUMENT_REPOSITORY_ALIAS, api: UmbCopyEntityAction, }, }, @@ -92,7 +92,7 @@ const entityActions: Array = [ entityType, icon: 'umb:navigation-vertical', label: 'Sort', - repositoryAlias, + repositoryAlias: DOCUMENT_REPOSITORY_ALIAS, api: UmbSortChildrenOfEntityAction, }, }, @@ -105,7 +105,7 @@ const entityActions: Array = [ entityType, icon: 'umb:home', label: 'Culture And Hostnames', - repositoryAlias, + repositoryAlias: DOCUMENT_REPOSITORY_ALIAS, api: UmbDocumentCultureAndHostnamesEntityAction, }, }, @@ -117,7 +117,7 @@ const entityActions: Array = [ entityType, icon: 'umb:vcard', label: 'Permissions', - repositoryAlias, + repositoryAlias: DOCUMENT_REPOSITORY_ALIAS, api: UmbDocumentPermissionsEntityAction, }, }, @@ -129,7 +129,7 @@ const entityActions: Array = [ entityType, icon: 'umb:lock', label: 'Public Access', - repositoryAlias, + repositoryAlias: DOCUMENT_REPOSITORY_ALIAS, api: UmbDocumentPublicAccessEntityAction, }, }, @@ -141,19 +141,7 @@ const entityActions: Array = [ entityType, icon: 'umb:globe', label: 'Publish', - repositoryAlias, - api: UmbPublishDocumentEntityAction, - }, - }, - { - type: 'entityAction', - alias: 'Umb.EntityAction.Document.Publish', - name: 'Publish Document Entity Action', - meta: { - entityType, - icon: 'umb:globe', - label: 'Publish', - repositoryAlias, + repositoryAlias: DOCUMENT_REPOSITORY_ALIAS, api: UmbPublishDocumentEntityAction, }, }, @@ -165,7 +153,7 @@ const entityActions: Array = [ entityType, icon: 'umb:globe', label: 'Unpublish', - repositoryAlias, + repositoryAlias: DOCUMENT_REPOSITORY_ALIAS, api: UmbUnpublishDocumentEntityAction, }, }, @@ -177,10 +165,19 @@ const entityActions: Array = [ entityType, icon: 'umb:undo', label: 'Rollback', - repositoryAlias, + repositoryAlias: DOCUMENT_REPOSITORY_ALIAS, api: UmbRollbackDocumentEntityAction, }, }, ]; -export const manifests = [...entityActions]; +const modals: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.CreateDocument', + name: 'Create Document Modal', + loader: () => import('./create/create-document-modal.element'), + }, +]; + +export const manifests = [...entityActions, ...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-bulk-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-bulk-actions/manifests.ts index 10da47405a..7edd0952e3 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-bulk-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/entity-bulk-actions/manifests.ts @@ -1,9 +1,9 @@ +import { DOCUMENT_REPOSITORY_ALIAS } from '../repository/manifests'; import { UmbDocumentMoveEntityBulkAction } from './move/move.action'; import { UmbDocumentCopyEntityBulkAction } from './copy/copy.action'; import { ManifestEntityBulkAction } from '@umbraco-cms/extensions-registry'; const entityType = 'document'; -const repositoryAlias = 'Umb.Repository.Documents'; const entityActions: Array = [ { @@ -14,7 +14,7 @@ const entityActions: Array = [ meta: { entityType, label: 'Move', - repositoryAlias, + repositoryAlias: DOCUMENT_REPOSITORY_ALIAS, api: UmbDocumentMoveEntityBulkAction, }, }, @@ -26,7 +26,7 @@ const entityActions: Array = [ meta: { entityType, label: 'Copy', - repositoryAlias, + repositoryAlias: DOCUMENT_REPOSITORY_ALIAS, api: UmbDocumentCopyEntityBulkAction, }, }, diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/manifests.ts index b36ebb6e5b..db1abd9914 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/manifests.ts @@ -5,6 +5,7 @@ import { manifests as treeManifests } from './tree/manifests'; import { manifests as workspaceManifests } from './workspace/manifests'; import { manifests as entityActionManifests } from './entity-actions/manifests'; import { manifests as entityBulkActionManifests } from './entity-bulk-actions/manifests'; +import { manifests as modalManifests } from './modals/manifests'; export const manifests = [ ...collectionManifests, @@ -14,4 +15,5 @@ export const manifests = [ ...workspaceManifests, ...entityActionManifests, ...entityBulkActionManifests, + ...modalManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/content-picker/modal-layout-content-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-picker/document-picker-modal.element.ts similarity index 76% rename from src/Umbraco.Web.UI.Client/src/core/modal/layouts/content-picker/modal-layout-content-picker.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-picker/document-picker-modal.element.ts index cc00f957aa..811c812a64 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/content-picker/modal-layout-content-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-picker/document-picker-modal.element.ts @@ -1,18 +1,16 @@ import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, state } from 'lit/decorators.js'; -import { UmbModalLayoutElement } from '../modal-layout.element'; - -export interface UmbModalContentPickerData { - multiple?: boolean; - selection?: Array; -} - -import { UmbTreeElement } from '../../../../backoffice/shared/components/tree/tree.element'; +import type { UmbTreeElement } from '../../../../shared/components/tree/tree.element'; +import { UmbDocumentPickerModalData, UmbDocumentPickerModalResult } from '.'; +import { UmbModalBaseElement } from '@umbraco-cms/modal'; // TODO: make use of UmbPickerLayoutBase -@customElement('umb-modal-layout-content-picker') -export class UmbModalLayoutContentPickerElement extends UmbModalLayoutElement { +@customElement('umb-document-picker-modal') +export class UmbDocumentPickerModalElement extends UmbModalBaseElement< + UmbDocumentPickerModalData, + UmbDocumentPickerModalResult +> { static styles = [ UUITextStyles, css` @@ -68,11 +66,11 @@ export class UmbModalLayoutContentPickerElement extends UmbModalLayoutElement = () => html` + + +`; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-picker/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-picker/index.ts new file mode 100644 index 0000000000..b67bffe04e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-picker/index.ts @@ -0,0 +1,18 @@ +import { UmbModalToken } from '@umbraco-cms/modal'; + +export interface UmbDocumentPickerModalData { + multiple?: boolean; + selection?: Array; +} + +export interface UmbDocumentPickerModalResult { + selection: Array; +} + +export const UMB_DOCUMENT_PICKER_MODAL_TOKEN = new UmbModalToken< + UmbDocumentPickerModalData, + UmbDocumentPickerModalResult +>('Umb.Modal.DocumentPicker', { + type: 'sidebar', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/manifests.ts new file mode 100644 index 0000000000..dcaf83f082 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/manifests.ts @@ -0,0 +1,12 @@ +import type { ManifestModal } from '@umbraco-cms/extensions-registry'; + +const modals: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.DocumentPicker', + name: 'Document Picker Modal', + loader: () => import('./document-picker/document-picker-modal.element'), + }, +]; + +export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts index 42b9f407c9..104b9092df 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts @@ -1,7 +1,7 @@ import type { RepositoryTreeDataSource } from '../../../../../libs/repository/repository-tree-data-source.interface'; import { DocumentTreeServerDataSource } from './sources/document.tree.server.data'; import { UmbDocumentTreeStore, UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from './document.tree.store'; -import { UmbDocumentStore, UMB_DOCUMENT_DETAIL_STORE_CONTEXT_TOKEN } from './document.store'; +import { UmbDocumentStore, UMB_DOCUMENT_STORE_CONTEXT_TOKEN } from './document.store'; import { UmbDocumentServerDataSource } from './sources/document.server.data'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbContextConsumerController } from '@umbraco-cms/context-api'; @@ -25,7 +25,7 @@ export class UmbDocumentRepository implements UmbTreeRepository, UmbDetailReposi #treeStore?: UmbDocumentTreeStore; #detailDataSource: UmbDocumentServerDataSource; - #detailStore?: UmbDocumentStore; + #store?: UmbDocumentStore; #notificationContext?: UmbNotificationContext; @@ -41,8 +41,8 @@ export class UmbDocumentRepository implements UmbTreeRepository, UmbDetailReposi this.#treeStore = instance; }), - new UmbContextConsumerController(this.#host, UMB_DOCUMENT_DETAIL_STORE_CONTEXT_TOKEN, (instance) => { - this.#detailStore = instance; + new UmbContextConsumerController(this.#host, UMB_DOCUMENT_STORE_CONTEXT_TOKEN, (instance) => { + this.#store = instance; }), new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { @@ -135,7 +135,7 @@ export class UmbDocumentRepository implements UmbTreeRepository, UmbDetailReposi const { data, error } = await this.#detailDataSource.get(key); if (data) { - this.#detailStore?.append(data); + this.#store?.append(data); } return { data, error }; @@ -159,7 +159,7 @@ export class UmbDocumentRepository implements UmbTreeRepository, UmbDetailReposi // TODO: we currently don't use the detail store for anything. // Consider to look up the data before fetching from the server - this.#detailStore?.append(item); + this.#store?.append(item); // TODO: Update tree store with the new item? or ask tree to request the new item? return { error }; @@ -182,7 +182,7 @@ export class UmbDocumentRepository implements UmbTreeRepository, UmbDetailReposi // TODO: we currently don't use the detail store for anything. // Consider to look up the data before fetching from the server // Consider notify a workspace if a document is updated in the store while someone is editing it. - this.#detailStore?.append(item); + this.#store?.append(item); //this.#treeStore?.updateItem(item.key, { name: item.name });// Port data to tree store. // TODO: would be nice to align the stores on methods/methodNames. @@ -208,7 +208,7 @@ export class UmbDocumentRepository implements UmbTreeRepository, UmbDetailReposi // TODO: we currently don't use the detail store for anything. // Consider to look up the data before fetching from the server. // Consider notify a workspace if a document is deleted from the store while someone is editing it. - this.#detailStore?.remove([key]); + this.#store?.remove([key]); this.#treeStore?.removeItem(key); // TODO: would be nice to align the stores on methods/methodNames. diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.store.ts index 9f7df54d24..0dfd392794 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.store.ts @@ -4,8 +4,6 @@ import { ArrayState } from '@umbraco-cms/observable-api'; import { UmbStoreBase } from '@umbraco-cms/store'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; -export const UMB_DOCUMENT_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbDocumentStore'); - /** * @export * @class UmbDocumentDetailStore @@ -21,7 +19,7 @@ export class UmbDocumentStore extends UmbStoreBase { * @memberof UmbDocumentDetailStore */ constructor(host: UmbControllerHostInterface) { - super(host, UMB_DOCUMENT_DETAIL_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_DOCUMENT_STORE_CONTEXT_TOKEN.toString()); } /** @@ -51,3 +49,5 @@ export class UmbDocumentStore extends UmbStoreBase { this.#data.remove(uniques); } } + +export const UMB_DOCUMENT_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbDocumentStore'); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/manifests.ts index 9df7f79d54..da7a9197b2 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/manifests.ts @@ -1,7 +1,10 @@ import { UmbDocumentRepository } from '../repository/document.repository'; +import { UmbDocumentStore } from './document.store'; +import { UmbDocumentTreeStore } from './document.tree.store'; import { ManifestRepository } from 'libs/extensions-registry/repository.models'; +import { ManifestStore, ManifestTreeStore } from '@umbraco-cms/extensions-registry'; -export const DOCUMENT_REPOSITORY_ALIAS = 'Umb.Repository.Documents'; +export const DOCUMENT_REPOSITORY_ALIAS = 'Umb.Repository.Document'; const repository: ManifestRepository = { type: 'repository', @@ -10,4 +13,21 @@ const repository: ManifestRepository = { class: UmbDocumentRepository, }; -export const manifests = [repository]; +export const DOCUMENT_STORE_ALIAS = 'Umb.Store.Document'; +export const DOCUMENT_TREE_STORE_ALIAS = 'Umb.Store.DocumentTree'; + +const store: ManifestStore = { + type: 'store', + alias: DOCUMENT_STORE_ALIAS, + name: 'Document Store', + class: UmbDocumentStore, +}; + +const treeStore: ManifestTreeStore = { + type: 'treeStore', + alias: DOCUMENT_TREE_STORE_ALIAS, + name: 'Document Tree Store', + class: UmbDocumentTreeStore, +}; + +export const manifests = [repository, store, treeStore]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace-split-view.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace-split-view.element.ts new file mode 100644 index 0000000000..ec66755894 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace-split-view.element.ts @@ -0,0 +1,92 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { css, html, nothing } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; +import { ActiveVariant } from '../../../shared/components/workspace/workspace-context/workspace-split-view-manager.class'; +import { UmbDocumentWorkspaceContext } from './document-workspace.context'; +import { UmbLitElement } from '@umbraco-cms/element'; +import '../../../shared/components/workspace/workspace-variant/workspace-variant.element'; + +@customElement('umb-document-workspace-split-view') +export class UmbDocumentWorkspaceSplitViewElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + :host { + width: 100%; + height: 100%; + + display: flex; + flex: 1; + flex-direction: column; + } + + #splitViews { + display: flex; + width: 100%; + height: 100%; + } + + #breadcrumbs { + margin: 0 var(--uui-size-layout-1); + } + `, + ]; + + private _workspaceContext?: UmbDocumentWorkspaceContext; + + @state() + _unique?: string; + + @state() + _variants?: Array; + + constructor() { + super(); + + this.consumeContext('umbWorkspaceContext', (context) => { + this._workspaceContext = context; + this._observeActiveVariantInfo(); + }); + } + + private _observeActiveVariantInfo() { + if (!this._workspaceContext) return; + this.observe( + this._workspaceContext.splitView.activeVariantsInfo, + (variants) => { + this._variants = variants; + }, + '_observeActiveVariantsInfo' + ); + } + + render() { + return this._variants + ? html`
+ ${repeat( + this._variants, + (view) => view.index, + (view) => html` + + ` + )} +
+ + + + ` + : nothing; + } +} + +export default UmbDocumentWorkspaceSplitViewElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-document-workspace-split-view': UmbDocumentWorkspaceSplitViewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts index fe520cf1d6..f62c5943fc 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts @@ -4,17 +4,12 @@ import { UmbDocumentTypeRepository } from '../../document-types/repository/docum import { UmbWorkspaceVariableEntityContextInterface } from '../../../shared/components/workspace/workspace-context/workspace-variable-entity-context.interface'; import { UmbVariantId } from '../../../shared/variants/variant-id.class'; import { UmbWorkspacePropertyStructureManager } from '../../../shared/components/workspace/workspace-context/workspace-property-structure-manager.class'; +import { UmbWorkspaceSplitViewManager } from '../../../shared/components/workspace/workspace-context/workspace-split-view-manager.class'; import type { DocumentModel } from '@umbraco-cms/backend-api'; -import { partialUpdateFrozenArray, ObjectState, ArrayState, UmbObserverController } from '@umbraco-cms/observable-api'; +import { partialUpdateFrozenArray, ObjectState, UmbObserverController } from '@umbraco-cms/observable-api'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; // TODO: should this context be called DocumentDraft instead of workspace? or should the draft be part of this? - -export type ActiveVariant = { - index: number; - culture: string | null; - segment: string | null; -}; // TODO: Should we have a DocumentStructureContext and maybe even a DocumentDraftContext? type EntityType = DocumentModel; @@ -33,21 +28,21 @@ export class UmbDocumentWorkspaceContext * The document is the current state/draft version of the document. */ #draft = new ObjectState(undefined); - documentTypeKey = this.#draft.getObservablePart((data) => data?.contentTypeKey); + readonly unique = this.#draft.getObservablePart((data) => data?.key); + readonly documentTypeKey = this.#draft.getObservablePart((data) => data?.contentTypeKey); - variants = this.#draft.getObservablePart((data) => data?.variants || []); - urls = this.#draft.getObservablePart((data) => data?.urls || []); - templateKey = this.#draft.getObservablePart((data) => data?.templateKey || null); - - #activeVariantsInfo = new ArrayState([], (x) => x.index); - activeVariantsInfo = this.#activeVariantsInfo.asObservable(); + readonly variants = this.#draft.getObservablePart((data) => data?.variants || []); + readonly urls = this.#draft.getObservablePart((data) => data?.urls || []); + readonly templateKey = this.#draft.getObservablePart((data) => data?.templateKey || null); readonly structure; + readonly splitView; constructor(host: UmbControllerHostInterface) { super(host, new UmbDocumentRepository(host)); this.structure = new UmbWorkspacePropertyStructureManager(this.host, new UmbDocumentTypeRepository(this.host)); + this.splitView = new UmbWorkspaceSplitViewManager(this.host); new UmbObserverController(this.host, this.documentTypeKey, (key) => this.structure.loadType(key)); } @@ -90,28 +85,10 @@ export class UmbDocumentWorkspaceContext return 'document'; } - setActiveVariant(index: number, culture: string | null, segment: string | null) { - const activeVariants = [...this.#activeVariantsInfo.getValue()]; - if (index < activeVariants.length) { - activeVariants[index] = { index, culture, segment }; - } else { - activeVariants.push({ index, culture, segment }); - } - this.#activeVariantsInfo.next(activeVariants); - } - - openSplitView(culture: string | null, segment: string | null) { - this.setActiveVariant(1, culture, segment); - } - getVariant(variantId: UmbVariantId) { return this.#draft.getValue()?.variants?.find((x) => variantId.compare(x)); } - activeVariantInfoByIndex(index: number) { - return this.#activeVariantsInfo.getObservablePart((data) => data[index] || undefined); - } - getName(variantId?: UmbVariantId) { const variants = this.#draft.getValue()?.variants; if (!variants) return; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.element.ts index e4fb29c8ad..d2f69c3f86 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.element.ts @@ -1,12 +1,16 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { css, html, nothing } from 'lit'; +import { css, html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; -import { repeat } from 'lit/directives/repeat.js'; +import { IRoute, IRoutingInfo } from 'router-slot'; import type { UmbWorkspaceEntityElement } from '../../../shared/components/workspace/workspace-entity-element.interface'; -import { ActiveVariant, UmbDocumentWorkspaceContext } from './document-workspace.context'; +import { UmbVariantId } from '../../../shared/variants/variant-id.class'; +import { ActiveVariant } from '../../../shared/components/workspace/workspace-context/workspace-split-view-manager.class'; +import { UmbDocumentWorkspaceContext } from './document-workspace.context'; +import { UmbDocumentWorkspaceSplitViewElement } from './document-workspace-split-view.element'; import { UmbLitElement } from '@umbraco-cms/element'; import '../../../shared/components/workspace/workspace-variant/workspace-variant.element'; -import { DocumentModel } from '@umbraco-cms/backend-api'; +import { DocumentModel, VariantViewModelBaseModel } from '@umbraco-cms/backend-api'; +import { UmbRouterSlotInitEvent } from '@umbraco-cms/router'; @customElement('umb-document-workspace') export class UmbDocumentWorkspaceElement extends UmbLitElement implements UmbWorkspaceEntityElement { @@ -14,7 +18,7 @@ export class UmbDocumentWorkspaceElement extends UmbLitElement implements UmbWor UUITextStyles, css` :host { - display: flex; + display: block; width: 100%; height: 100%; } @@ -22,16 +26,29 @@ export class UmbDocumentWorkspaceElement extends UmbLitElement implements UmbWor ]; private _workspaceContext: UmbDocumentWorkspaceContext = new UmbDocumentWorkspaceContext(this); + //private _defaultVariant?: VariantViewModelBaseModel; + private splitViewElement = new UmbDocumentWorkspaceSplitViewElement(); @state() _unique?: string; + @state() + _routes?: Array; + + @state() + _availableVariants: Array = []; + @state() _workspaceSplitViews: Array = []; constructor() { super(); - this.observe(this._workspaceContext.activeVariantsInfo, (variants) => { + + this.observe(this._workspaceContext.variants, (variants) => { + this._availableVariants = variants; + this._generateRoutes(); + }); + this.observe(this._workspaceContext.splitView.activeVariantsInfo, (variants) => { this._workspaceSplitViews = variants; }); } @@ -48,28 +65,80 @@ export class UmbDocumentWorkspaceElement extends UmbLitElement implements UmbWor private _gotDocumentData(data: DocumentModel | undefined) { if (data && data.variants && data.variants.length > 0) { - this._workspaceContext.setActiveVariant(0, data.variants[0].culture || null, data.variants[0].segment || null); + //this._defaultVariant = data.variants[0]; this._unique = data.key; + // Maybe we need to re-generate routes here? } else { // Fail beautifully? } } + private _handleVariantFolderPart(index: number, folderPart: string) { + const variantSplit = folderPart.split('_'); + const culture = variantSplit[0]; + const segment = variantSplit[1]; + this._workspaceContext.splitView.setActiveVariant(index, culture, segment); + } + + private _generateRoutes() { + if (!this._availableVariants || this._availableVariants.length === 0) return; + + // Generate split view routes for all available routes + const routes: Array = []; + + // Split view routes: + this._availableVariants.forEach((variantA) => { + this._availableVariants.forEach((variantB) => { + routes.push({ + path: new UmbVariantId(variantA).toString() + '_&_' + new UmbVariantId(variantB).toString(), + //component: () => import('./document-workspace-split-view.element'), + component: this.splitViewElement, + setup: (component: HTMLElement | Promise, info: IRoutingInfo) => { + // Set split view/active info.. + const variantSplit = info.match.fragments.consumed.split('_&_'); + variantSplit.forEach((part, index) => { + this._handleVariantFolderPart(index, part); + }); + }, + }); + }); + }); + + // Single view: + this._availableVariants.forEach((variant) => { + routes.push({ + path: new UmbVariantId(variant).toString(), + //component: () => import('./document-workspace-split-view.element'), + component: this.splitViewElement, + setup: (component: HTMLElement | Promise, info: IRoutingInfo) => { + // cause we might come from a split-view, we need to reset index 1. + this._workspaceContext.splitView.removeActiveVariant(1); + this._handleVariantFolderPart(0, info.match.fragments.consumed); + }, + }); + }); + + if (routes.length !== 0) { + // Using first single view as the default route for now (hence the math below): + routes.push({ + path: '**', + redirectTo: routes[this._availableVariants.length * this._availableVariants.length]?.path, + }); + } + + this._routes = routes; + } + + private _gotWorkspaceRoute = (e: UmbRouterSlotInitEvent) => { + this._workspaceContext.splitView.setWorkspaceRoute(e.target.absoluteRouterPath); + }; + render() { - return this._unique - ? repeat( - this._workspaceSplitViews, - (view) => view.index, - (view) => html` - - - - ` - ) - : nothing; + return this._routes + ? html`${this.splitViewElement}` + : ''; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/views/document-workspace-view-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/views/document-workspace-view-edit.element.ts index a7ac1b3fd3..26b33df552 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/views/document-workspace-view-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/views/document-workspace-view-edit.element.ts @@ -89,16 +89,6 @@ export class UmbDocumentWorkspaceViewEditElement extends UmbLitElement { private _createRoutes() { const routes: any[] = []; - if (this._hasRootGroups) { - routes.push({ - path: 'root', - component: () => import('./document-workspace-view-edit-tab.element'), - setup: (component: Promise) => { - (component as any).noTabName = true; - }, - }); - } - if (this._tabs.length > 0) { this._tabs?.forEach((tab) => { const tabName = tab.name; @@ -112,12 +102,17 @@ export class UmbDocumentWorkspaceViewEditElement extends UmbLitElement { }); } - if (routes.length !== 0) { + if (this._hasRootGroups) { routes.push({ path: '', - redirectTo: routes[0]?.path, + component: () => import('./document-workspace-view-edit-tab.element'), + setup: (component: Promise) => { + (component as any).noTabName = true; + }, }); + } + if (routes.length !== 0) { routes.push({ path: '**', redirectTo: routes[0]?.path, @@ -129,14 +124,14 @@ export class UmbDocumentWorkspaceViewEditElement extends UmbLitElement { render() { return html` - ${this._tabs.length > 1 + ${this._routerPath && this._tabs.length > 1 ? html` ${this._hasRootGroups && this._tabs.length > 1 ? html` Content ` diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/index.ts index 5f5aac6a97..7cfabc7cce 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/index.ts @@ -8,18 +8,17 @@ import { manifests as documentManifests } from './documents/manifests'; import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; import { ManifestTypes } from '@umbraco-cms/extensions-registry'; -const registerExtensions = (manifests: Array) => { - manifests.forEach((manifest) => { - if (umbExtensionsRegistry.isRegistered(manifest.alias)) return; - umbExtensionsRegistry.register(manifest); - }); -}; - -registerExtensions([ +export const manifests = [ ...dashboardManifests, ...contentSectionManifests, ...contentMenuManifest, ...documentBlueprintManifests, ...documentTypeManifests, ...documentManifests, -]); +]; + +const registerExtensions = (manifests: Array) => { + manifests.forEach((manifest) => umbExtensionsRegistry.register(manifest)); +}; + +registerExtensions(manifests); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/index.ts index 7afe8c49db..76a1e6c199 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/index.ts @@ -6,11 +6,10 @@ import { manifests as mediaTypesManifests } from './media-types/manifests'; import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; import { ManifestTypes } from '@umbraco-cms/extensions-registry'; +export const manifests = [...mediaSectionManifests, ...mediaMenuManifests, ...mediaManifests, ...mediaTypesManifests]; + const registerExtensions = (manifests: Array) => { - manifests.forEach((manifest) => { - if (umbExtensionsRegistry.isRegistered(manifest.alias)) return; - umbExtensionsRegistry.register(manifest); - }); + manifests.forEach((manifest) => umbExtensionsRegistry.register(manifest)); }; -registerExtensions([...mediaSectionManifests, ...mediaMenuManifests, ...mediaManifests, ...mediaTypesManifests]); +registerExtensions(manifests); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/manifests.ts index 2460e11e38..dc5de79647 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/manifests.ts @@ -1,13 +1,33 @@ import { UmbMediaTypeRepository } from './media-type.repository'; +import { UmbMediaTypeStore } from './media-type.detail.store'; +import { UmbMediaTypeTreeStore } from './media-type.tree.store'; import { ManifestRepository } from 'libs/extensions-registry/repository.models'; +import { ManifestStore, ManifestTreeStore } from '@umbraco-cms/extensions-registry'; -export const MEDIA_TYPE_REPOSITORY_ALIAS = 'Umb.Repository.MediaTypes'; +export const MEDIA_TYPE_REPOSITORY_ALIAS = 'Umb.Repository.MediaType'; const repository: ManifestRepository = { type: 'repository', alias: MEDIA_TYPE_REPOSITORY_ALIAS, - name: 'Media Types Repository', + name: 'Media Type Repository', class: UmbMediaTypeRepository, }; -export const manifests = [repository]; +export const MEDIA_TYPE_STORE_ALIAS = 'Umb.Store.MediaType'; +export const MEDIA_TYPE_TREE_STORE_ALIAS = 'Umb.Store.MediaTypeTree'; + +const store: ManifestStore = { + type: 'store', + alias: MEDIA_TYPE_STORE_ALIAS, + name: 'Media Type Store', + class: UmbMediaTypeStore, +}; + +const treeStore: ManifestTreeStore = { + type: 'treeStore', + alias: MEDIA_TYPE_TREE_STORE_ALIAS, + name: 'Media Type Tree Store', + class: UmbMediaTypeTreeStore, +}; + +export const manifests = [store, treeStore, repository]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.detail.store.ts index 841ca9822a..57dc378d61 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.detail.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.detail.store.ts @@ -10,13 +10,11 @@ import type { MediaTypeDetails } from '@umbraco-cms/models'; * @extends {UmbStoreBase} * @description - Details Data Store for Media Types */ -export class UmbMediaTypeDetailStore - extends UmbStoreBase -{ +export class UmbMediaTypeStore extends UmbStoreBase { #data = new ArrayState([], (x) => x.key); constructor(host: UmbControllerHostInterface) { - super(host, UMB_MEDIA_TYPE_DETAIL_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_MEDIA_TYPE_STORE_CONTEXT_TOKEN.toString()); } append(mediaType: MediaTypeDetails) { @@ -28,6 +26,4 @@ export class UmbMediaTypeDetailStore } } -export const UMB_MEDIA_TYPE_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken( - 'UmbMediaTypeDetailStore' -); +export const UMB_MEDIA_TYPE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMediaTypeStore'); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts index 1ded02b0fe..8123483e57 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts @@ -1,6 +1,6 @@ import { UmbMediaTypeTreeStore, UMB_MEDIA_TYPE_TREE_STORE_CONTEXT_TOKEN } from './media-type.tree.store'; import { UmbMediaTypeDetailServerDataSource } from './sources/media-type.detail.server.data'; -import { UmbMediaTypeDetailStore, UMB_MEDIA_TYPE_DETAIL_STORE_CONTEXT_TOKEN } from './media-type.detail.store'; +import { UmbMediaTypeStore, UMB_MEDIA_TYPE_STORE_CONTEXT_TOKEN } from './media-type.detail.store'; import { MediaTypeTreeServerDataSource } from './sources/media-type.tree.server.data'; import { ProblemDetailsModel } from '@umbraco-cms/backend-api'; import { UmbContextConsumerController } from '@umbraco-cms/context-api'; @@ -18,7 +18,7 @@ export class UmbMediaTypeRepository implements UmbTreeRepository { #treeStore?: UmbMediaTypeTreeStore; #detailSource: UmbMediaTypeDetailServerDataSource; - #detailStore?: UmbMediaTypeDetailStore; + #store?: UmbMediaTypeStore; #notificationContext?: UmbNotificationContext; @@ -30,8 +30,8 @@ export class UmbMediaTypeRepository implements UmbTreeRepository { this.#detailSource = new UmbMediaTypeDetailServerDataSource(this.#host); this.#init = Promise.all([ - new UmbContextConsumerController(this.#host, UMB_MEDIA_TYPE_DETAIL_STORE_CONTEXT_TOKEN, (instance) => { - this.#detailStore = instance; + new UmbContextConsumerController(this.#host, UMB_MEDIA_TYPE_STORE_CONTEXT_TOKEN, (instance) => { + this.#store = instance; }), new UmbContextConsumerController(this.#host, UMB_MEDIA_TYPE_TREE_STORE_CONTEXT_TOKEN, (instance) => { @@ -120,7 +120,7 @@ export class UmbMediaTypeRepository implements UmbTreeRepository { const { data, error } = await this.#detailSource.get(key); if (data) { - this.#detailStore?.append(data); + this.#store?.append(data); } return { data, error }; } @@ -150,7 +150,7 @@ export class UmbMediaTypeRepository implements UmbTreeRepository { // TODO: we currently don't use the detail store for anything. // Consider to look up the data before fetching from the server // Consider notify a workspace if a media type is updated in the store while someone is editing it. - this.#detailStore?.append(mediaType); + this.#store?.append(mediaType); this.#treeStore?.updateItem(mediaType.key, { name: mediaType.name }); // TODO: would be nice to align the stores on methods/methodNames. diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.tree.store.ts index 0f82fd5a61..a70127f5d6 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.tree.store.ts @@ -9,7 +9,6 @@ import type { UmbControllerHostInterface } from '@umbraco-cms/controller'; * @description - Tree Data Store for Media Types */ export class UmbMediaTypeTreeStore extends UmbTreeStoreBase { - /** * Creates an instance of UmbMediaTypeTreeStore. * @param {UmbControllerHostInterface} host diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-actions/manifests.ts index 07c8ca11f8..e34ec47335 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-actions/manifests.ts @@ -1,3 +1,4 @@ +import { MEDIA_REPOSITORY_ALIAS } from '../repository/manifests'; import { UmbTrashEntityAction } from '@umbraco-cms/entity-action'; import { ManifestEntityAction } from 'libs/extensions-registry/entity-action.models'; @@ -11,7 +12,7 @@ const entityActions: Array = [ icon: 'umb:trash', label: 'Trash', api: UmbTrashEntityAction, - repositoryAlias: 'Umb.Repository.Media', + repositoryAlias: MEDIA_REPOSITORY_ALIAS, }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-bulk-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-bulk-actions/manifests.ts index 14a585f00a..86b84b2856 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-bulk-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-bulk-actions/manifests.ts @@ -1,10 +1,10 @@ +import { MEDIA_REPOSITORY_ALIAS } from '../repository/manifests'; import { UmbMediaMoveEntityBulkAction } from './move/move.action'; import { UmbMediaCopyEntityBulkAction } from './copy/copy.action'; import { UmbMediaTrashEntityBulkAction } from './trash/trash.action'; import { ManifestEntityBulkAction } from '@umbraco-cms/extensions-registry'; const entityType = 'media'; -const repositoryAlias = 'Umb.Repository.Media'; const entityActions: Array = [ { @@ -15,7 +15,7 @@ const entityActions: Array = [ meta: { entityType, label: 'Move', - repositoryAlias, + repositoryAlias: MEDIA_REPOSITORY_ALIAS, api: UmbMediaMoveEntityBulkAction, }, }, @@ -27,7 +27,7 @@ const entityActions: Array = [ meta: { entityType, label: 'Copy', - repositoryAlias, + repositoryAlias: MEDIA_REPOSITORY_ALIAS, api: UmbMediaCopyEntityBulkAction, }, }, @@ -39,7 +39,7 @@ const entityActions: Array = [ meta: { entityType, label: 'Trash', - repositoryAlias, + repositoryAlias: MEDIA_REPOSITORY_ALIAS, api: UmbMediaTrashEntityBulkAction, }, }, diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-bulk-actions/move/move.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-bulk-actions/move/move.action.ts index 30ae1aeac3..42f84f7b43 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-bulk-actions/move/move.action.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-bulk-actions/move/move.action.ts @@ -1,4 +1,5 @@ import type { UmbMediaRepository } from '../../repository/media.repository'; +import { UMB_MEDIA_PICKER_MODAL_TOKEN } from '../../modals/media-picker'; import { UmbEntityBulkActionBase } from '@umbraco-cms/entity-action'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbContextConsumerController } from '@umbraco-cms/context-api'; @@ -17,9 +18,14 @@ export class UmbMediaMoveEntityBulkAction extends UmbEntityBulkActionBase { #modalContext?: UmbModalContext; @@ -25,7 +26,7 @@ export class UmbMediaTrashEntityBulkAction extends UmbEntityBulkActionBase = [ + { + type: 'modal', + alias: 'Umb.Modal.MediaPicker', + name: 'Media Picker Modal', + loader: () => import('./media-picker/media-picker-modal.element'), + }, +]; + +export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/modals/media-picker/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/modals/media-picker/index.ts new file mode 100644 index 0000000000..7075158e15 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/modals/media-picker/index.ts @@ -0,0 +1,18 @@ +import { UmbModalToken } from '@umbraco-cms/modal'; + +export interface UmbMediaPickerModalData { + multiple?: boolean; + selection: Array; +} + +export interface UmbMediaPickerModalResult { + selection: Array; +} + +export const UMB_MEDIA_PICKER_MODAL_TOKEN = new UmbModalToken( + 'Umb.Modal.MediaPicker', + { + type: 'sidebar', + size: 'small', + } +); diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/media-picker/modal-layout-media-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/modals/media-picker/media-picker-modal.element.ts similarity index 77% rename from src/Umbraco.Web.UI.Client/src/core/modal/layouts/media-picker/modal-layout-media-picker.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/media/media/modals/media-picker/media-picker-modal.element.ts index 15f5d39ada..82fa6411f0 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/media-picker/modal-layout-media-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/modals/media-picker/media-picker-modal.element.ts @@ -1,17 +1,15 @@ import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, state } from 'lit/decorators.js'; -import { UmbModalLayoutElement } from '../modal-layout.element'; +import { UmbTreeElement } from '../../../../shared/components/tree/tree.element'; +import { UmbMediaPickerModalData, UmbMediaPickerModalResult } from '.'; +import { UmbModalBaseElement } from '@umbraco-cms/modal'; -export interface UmbModalMediaPickerData { - multiple?: boolean; - selection: Array; -} - -import { UmbTreeElement } from '../../../../backoffice/shared/components/tree/tree.element'; - -@customElement('umb-modal-layout-media-picker') -export class UmbModalLayoutMediaPickerElement extends UmbModalLayoutElement { +@customElement('umb-media-picker-modal') +export class UmbMediaPickerModalElement extends UmbModalBaseElement< + UmbMediaPickerModalData, + UmbMediaPickerModalResult +> { static styles = [ UUITextStyles, css` @@ -67,11 +65,11 @@ export class UmbModalLayoutMediaPickerElement extends UmbModalLayoutElement { - this.#detailStore = instance; + new UmbContextConsumerController(this.#host, UMB_MEDIA_STORE_CONTEXT_TOKEN, (instance) => { + this.#store = instance; }), new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { @@ -133,7 +133,7 @@ export class UmbMediaRepository implements UmbTreeRepository, UmbDetailRepositor const { data, error } = await this.#detailDataSource.get(key); if (data) { - this.#detailStore?.append(data); + this.#store?.append(data); } return { data, error }; @@ -157,7 +157,7 @@ export class UmbMediaRepository implements UmbTreeRepository, UmbDetailRepositor // TODO: we currently don't use the detail store for anything. // Consider to look up the data before fetching from the server - this.#detailStore?.append(template); + this.#store?.append(template); // TODO: Update tree store with the new item? or ask tree to request the new item? return { error }; @@ -180,7 +180,7 @@ export class UmbMediaRepository implements UmbTreeRepository, UmbDetailRepositor // TODO: we currently don't use the detail store for anything. // Consider to look up the data before fetching from the server // Consider notify a workspace if a template is updated in the store while someone is editing it. - this.#detailStore?.append(document); + this.#store?.append(document); this.#treeStore?.updateItem(document.key, { name: document.name }); // TODO: would be nice to align the stores on methods/methodNames. @@ -206,7 +206,7 @@ export class UmbMediaRepository implements UmbTreeRepository, UmbDetailRepositor // TODO: we currently don't use the detail store for anything. // Consider to look up the data before fetching from the server. // Consider notify a workspace if a template is deleted from the store while someone is editing it. - this.#detailStore?.remove([key]); + this.#store?.remove([key]); this.#treeStore?.removeItem(key); // TODO: would be nice to align the stores on methods/methodNames. diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.store.ts similarity index 67% rename from src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.detail.store.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.store.ts index 9130ba65bf..070a596885 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.detail.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.store.ts @@ -4,30 +4,28 @@ import { ArrayState } from '@umbraco-cms/observable-api'; import { UmbStoreBase } from '@umbraco-cms/store'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; -export const UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMediaDetailStore'); - /** * @export - * @class UmbMediaDetailStore + * @class UmbMediaStore * @extends {UmbStoreBase} * @description - Data Store for Template Details */ -export class UmbMediaDetailStore extends UmbStoreBase { +export class UmbMediaStore extends UmbStoreBase { #data = new ArrayState([], (x) => x.key); /** - * Creates an instance of UmbMediaDetailStore. + * Creates an instance of UmbMediaStore. * @param {UmbControllerHostInterface} host - * @memberof UmbMediaDetailStore + * @memberof UmbMediaStore */ constructor(host: UmbControllerHostInterface) { - super(host, UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_MEDIA_STORE_CONTEXT_TOKEN.toString()); } /** * Append a media to the store * @param {MediaDetails} media - * @memberof UmbMediaDetailStore + * @memberof UmbMediaStore */ append(media: MediaDetails) { this.#data.append([media]); @@ -36,9 +34,11 @@ export class UmbMediaDetailStore extends UmbStoreBase { /** * Removes media in the store with the given uniques * @param {string[]} uniques - * @memberof UmbMediaDetailStore + * @memberof UmbMediaStore */ remove(uniques: string[]) { this.#data.remove(uniques); } } + +export const UMB_MEDIA_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMediaStore'); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.tree.store.ts index 07299c5e8f..a070665602 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.tree.store.ts @@ -1,7 +1,7 @@ import { EntityTreeItemModel } from '@umbraco-cms/backend-api'; import { UmbContextToken } from '@umbraco-cms/context-api'; import { ArrayState } from '@umbraco-cms/observable-api'; -import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbTreeStoreBase } from '@umbraco-cms/store'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; export const UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMediaTreeStore'); @@ -9,11 +9,10 @@ export const UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken([], (x) => x.key); /** @@ -24,68 +23,4 @@ export class UmbMediaTreeStore extends UmbStoreBase { constructor(host: UmbControllerHostInterface) { super(host, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN.toString()); } - - /** - * Appends items to the store - * @param {Array} items - * @memberof UmbMediaTreeStore - */ - appendItems(items: Array) { - this.#data.append(items); - } - - /** - * Updates an item in the store - * @param {string} key - * @param {Partial} data - * @memberof UmbMediaTreeStore - */ - updateItem(key: string, data: Partial) { - const entries = this.#data.getValue(); - const entry = entries.find((entry) => entry.key === key); - - if (entry) { - this.#data.appendOne({ ...entry, ...data }); - } - } - - /** - * Removes an item from the store - * @param {string} key - * @memberof UmbMediaTreeStore - */ - removeItem(key: string) { - const entries = this.#data.getValue(); - const entry = entries.find((entry) => entry.key === key); - - if (entry) { - this.#data.remove([key]); - } - } - - /** - * An observable to observe the root items - * @memberof UmbMediaTreeStore - */ - rootItems = this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === null)); - - /** - * Returns an observable to observe the children of a given parent - * @param {(string | null)} parentKey - * @return {*} - * @memberof UmbMediaTreeStore - */ - childrenOf(parentKey: string | null) { - return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === parentKey)); - } - - /** - * Returns an observable to observe the items with the given keys - * @param {Array} keys - * @return {*} - * @memberof UmbMediaTreeStore - */ - items(keys: Array) { - return this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? ''))); - } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/manifests.ts index 29af2a95c6..3c363283c7 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/manifests.ts @@ -1,3 +1,4 @@ +import { MEDIA_REPOSITORY_ALIAS } from '../repository/manifests'; import { UmbSaveWorkspaceAction } from '@umbraco-cms/workspace'; import type { ManifestWorkspace, @@ -59,7 +60,7 @@ const workspaceViewCollections: Array = [ pathname: 'collection', icon: 'umb:grid', entityType: 'media', - repositoryAlias: 'Umb.Repository.Media', + repositoryAlias: MEDIA_REPOSITORY_ALIAS, }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/section.manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/section.manifests.ts index e7f137f2b8..9ba71485a5 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/section.manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/section.manifests.ts @@ -1,3 +1,4 @@ +import { MEDIA_REPOSITORY_ALIAS } from './media/repository/manifests'; import type { ManifestDashboardCollection, ManifestSection, ManifestMenuSectionSidebarApp } from '@umbraco-cms/models'; const sectionAlias = 'Umb.Section.Media'; @@ -24,7 +25,7 @@ const dashboards: Array = [ sections: [sectionAlias], pathname: 'media-management', entityType: 'media', - repositoryAlias: 'Umb.Repository.Media', + repositoryAlias: MEDIA_REPOSITORY_ALIAS, }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/index.ts index 0f5f91f7e7..386a38582f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/index.ts @@ -7,17 +7,16 @@ import { manifests as memberManifests } from './members/manifests'; import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; import { ManifestTypes } from '@umbraco-cms/extensions-registry'; -const registerExtensions = (manifests: Array) => { - manifests.forEach((manifest) => { - if (umbExtensionsRegistry.isRegistered(manifest.alias)) return; - umbExtensionsRegistry.register(manifest); - }); -}; - -registerExtensions([ +export const manifests = [ ...memberSectionManifests, ...menuSectionManifests, ...memberGroupManifests, ...memberTypeManifests, ...memberManifests, -]); +]; + +const registerExtensions = (manifests: Array) => { + manifests.forEach((manifest) => umbExtensionsRegistry.register(manifest)); +}; + +registerExtensions(manifests); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/entity-actions/manifests.ts index 5e84d8d6db..e132cc6f8f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/entity-actions/manifests.ts @@ -1,3 +1,4 @@ +import { MEMBER_GROUP_REPOSITORY_ALIAS } from '../repository/manifests'; import { UmbDeleteEntityAction } from '@umbraco-cms/entity-action'; import { ManifestEntityAction } from 'libs/extensions-registry/entity-action.models'; @@ -11,7 +12,7 @@ const entityActions: Array = [ icon: 'umb:trash', label: 'Delete', api: UmbDeleteEntityAction, - repositoryAlias: 'Umb.Repository.MemberGroup', + repositoryAlias: MEMBER_GROUP_REPOSITORY_ALIAS, }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/manifests.ts index 728b8ae95f..c4fb973526 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/manifests.ts @@ -1,5 +1,8 @@ import { UmbMemberGroupRepository } from './member-group.repository'; +import { UmbMemberGroupStore } from './member-group.store'; +import { UmbMemberGroupTreeStore } from './member-group.tree.store'; import { ManifestRepository } from 'libs/extensions-registry/repository.models'; +import { ManifestStore, ManifestTreeStore } from '@umbraco-cms/extensions-registry'; export const MEMBER_GROUP_REPOSITORY_ALIAS = 'Umb.Repository.MemberGroup'; @@ -10,4 +13,21 @@ const repository: ManifestRepository = { class: UmbMemberGroupRepository, }; -export const manifests = [repository]; +export const MEMBER_GROUP_STORE_ALIAS = 'Umb.Store.MemberGroup'; +export const MEMBER_GROUP_TREE_STORE_ALIAS = 'Umb.Store.MemberGroupTree'; + +const store: ManifestStore = { + type: 'store', + alias: MEMBER_GROUP_STORE_ALIAS, + name: 'Member Group Store', + class: UmbMemberGroupStore, +}; + +const treeStore: ManifestTreeStore = { + type: 'treeStore', + alias: MEMBER_GROUP_TREE_STORE_ALIAS, + name: 'Member Group Tree Store', + class: UmbMemberGroupTreeStore, +}; + +export const manifests = [store, treeStore, repository]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.repository.ts index 80e323c25e..0c0f315b71 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.repository.ts @@ -1,6 +1,6 @@ import { UmbMemberGroupTreeStore, UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN } from './member-group.tree.store'; import { UmbMemberGroupDetailServerDataSource } from './sources/member-group.detail.server.data'; -import { UmbMemberGroupDetailStore, UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN } from './member-group.detail.store'; +import { UmbMemberGroupStore, UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN } from './member-group.store'; import { MemberGroupTreeServerDataSource } from './sources/member-group.tree.server.data'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/notification'; @@ -19,7 +19,7 @@ export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRep #treeStore?: UmbMemberGroupTreeStore; #detailSource: UmbMemberGroupDetailServerDataSource; - #detailStore?: UmbMemberGroupDetailStore; + #store?: UmbMemberGroupStore; #notificationContext?: UmbNotificationContext; @@ -33,8 +33,8 @@ export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRep this.#treeStore = instance; }); - new UmbContextConsumerController(this.#host, UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN, (instance) => { - this.#detailStore = instance; + new UmbContextConsumerController(this.#host, UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN, (instance) => { + this.#store = instance; }); new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { @@ -74,7 +74,7 @@ export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRep async rootTreeItems() { await this.#init; - return this.#treeStore!.rootItems(); + return this.#treeStore!.rootItems; } async treeItemsOf(parentKey: string | null) { @@ -106,7 +106,7 @@ export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRep const { data, error } = await this.#detailSource.get(key); if (data) { - this.#detailStore?.append(data); + this.#store?.append(data); } return { data, error }; } @@ -144,7 +144,7 @@ export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRep this.#notificationContext?.peek('positive', notification); } - this.#detailStore?.append(memberGroup); + this.#store?.append(memberGroup); this.#treeStore?.updateItem(memberGroup.key, { name: memberGroup.name }); return { error }; @@ -168,7 +168,7 @@ export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRep // TODO: we currently don't use the detail store for anything. // Consider to look up the data before fetching from the server. // Consider notify a workspace if a template is deleted from the store while someone is editing it. - this.#detailStore?.remove([key]); + this.#store?.remove([key]); this.#treeStore?.removeItem(key); // TODO: would be nice to align the stores on methods/methodNames. diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.store.ts similarity index 62% rename from src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.detail.store.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.store.ts index ce6053cbd6..864430facf 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.detail.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.store.ts @@ -4,23 +4,17 @@ import { ArrayState } from '@umbraco-cms/observable-api'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbStoreBase } from '@umbraco-cms/store'; -export const UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken( - 'UmbMemberGroupDetailStore' -); - /** * @export - * @class UmbMemberGroupDetailStore + * @class UmbMemberGroupStore * @extends {UmbStoreBase} - * @description - Details Data Store for Member Groups + * @description - Data Store for Member Groups */ -export class UmbMemberGroupDetailStore - extends UmbStoreBase -{ +export class UmbMemberGroupStore extends UmbStoreBase { #data = new ArrayState([], (x) => x.key); constructor(host: UmbControllerHostInterface) { - super(host, UMB_MEMBER_GROUP_DETAIL_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN.toString()); } append(memberGroup: MemberGroupDetails) { @@ -31,3 +25,5 @@ export class UmbMemberGroupDetailStore this.#data.remove(uniques); } } + +export const UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberGroupStore'); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.tree.store.ts index bad706d967..4d1f4ce44a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.tree.store.ts @@ -1,93 +1,22 @@ -import type { EntityTreeItemModel } from '@umbraco-cms/backend-api'; import { UmbContextToken } from '@umbraco-cms/context-api'; -import { ArrayState } from '@umbraco-cms/observable-api'; -import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbTreeStoreBase } from '@umbraco-cms/store'; import type { UmbControllerHostInterface } from '@umbraco-cms/controller'; /** * @export * @class UmbMemberGroupTreeStore - * @extends {UmbStoreBase} + * @extends {UmbTreeStoreBase} * @description - Tree Data Store for Member Groups */ -export class UmbMemberGroupTreeStore extends UmbStoreBase { - #data = new ArrayState([], (x) => x.key); - +export class UmbMemberGroupTreeStore extends UmbTreeStoreBase { /** - * Creates an instance of UmbTemplateTreeStore. + * Creates an instance of UmbMemberGroupTreeStore. * @param {UmbControllerHostInterface} host * @memberof UmbMemberGroupTreeStore */ constructor(host: UmbControllerHostInterface) { super(host, UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN.toString()); } - - /** - * Appends items to the store - * @param {Array} items - * @memberof UmbTemplateTreeStore - */ - appendItems(items: Array) { - this.#data.append(items); - } - - /** - * Updates an item in the store - * @param {string} key - * @param {Partial} data - * @memberof UmbMemberGroupTreeStore - */ - updateItem(key: string, data: Partial) { - const entries = this.#data.getValue(); - const entry = entries.find((entry) => entry.key === key); - - if (entry) { - this.#data.appendOne({ ...entry, ...data }); - } - } - - /** - * Removes an item from the store - * @param {string} key - * @memberof UmbMemberGroupTreeStore - */ - removeItem(key: string) { - const entries = this.#data.getValue(); - const entry = entries.find((entry) => entry.key === key); - - if (entry) { - this.#data.remove([key]); - } - } - - /** - * Returns an observable to observe the root items - * @return {*} - * @memberof UmbMemberGroupTreeStore - */ - rootItems() { - return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === null)); - } - - /** - * Returns an observable to observe the children of a given parent - * @param {(string | null)} parentKey - * @return {*} - * @memberof UmbMemberGroupTreeStore - */ - childrenOf(parentKey: string | null) { - return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === parentKey)); - } - - /** - * Returns an observable to observe the items with the given keys - * @param {Array} keys - * @return {*} - * @memberof UmbMemberGroupTreeStore - */ - items(keys: Array) { - return this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? ''))); - } } export const UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken( diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/manifests.ts index da187f5066..74c4fd94d3 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/manifests.ts @@ -1,4 +1,3 @@ -import { MEMBER_GROUP_REPOSITORY_ALIAS } from '../repository/manifests'; import { UmbSaveWorkspaceAction } from '@umbraco-cms/workspace'; import type { ManifestWorkspace, ManifestWorkspaceAction, ManifestWorkspaceView } from '@umbraco-cms/models'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/manifests.ts index deee635772..f40c631433 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/manifests.ts @@ -1,7 +1,9 @@ import { UmbMemberTypeRepository } from './member-type.repository'; -import { ManifestRepository } from 'libs/extensions-registry/repository.models'; +import { UmbMemberTypeStore } from './member-type.store'; +import { UmbMemberTypeTreeStore } from './member-type.tree.store'; +import type { ManifestRepository, ManifestStore, ManifestTreeStore } from '@umbraco-cms/extensions-registry'; -export const MEMBER_TYPES_REPOSITORY_ALIAS = 'Umb.Repository.MemberTypes'; +export const MEMBER_TYPES_REPOSITORY_ALIAS = 'Umb.Repository.MemberType'; const repository: ManifestRepository = { type: 'repository', @@ -10,4 +12,21 @@ const repository: ManifestRepository = { class: UmbMemberTypeRepository, }; -export const manifests = [repository]; \ No newline at end of file +export const MEMBER_TYPE_STORE_ALIAS = 'Umb.Store.MemberType'; +export const MEMBER_TYPE_TREE_STORE_ALIAS = 'Umb.Store.MemberTypeTree'; + +const store: ManifestStore = { + type: 'store', + alias: MEMBER_TYPE_STORE_ALIAS, + name: 'Member Type Store', + class: UmbMemberTypeStore, +}; + +const treeStore: ManifestTreeStore = { + type: 'treeStore', + alias: MEMBER_TYPE_TREE_STORE_ALIAS, + name: 'Member Type Tree Store', + class: UmbMemberTypeTreeStore, +}; + +export const manifests = [store, treeStore, repository]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.repository.ts index ec294e6ee6..83120d5b3d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.repository.ts @@ -1,6 +1,6 @@ import { MemberTypeTreeServerDataSource } from './sources/member-type.tree.server.data'; import { UmbMemberTypeTreeStore, UMB_MEMBER_TYPE_TREE_STORE_CONTEXT_TOKEN } from './member-type.tree.store'; -import { UmbMemberTypeDetailStore, UMB_MEMBER_TYPE_DETAIL_STORE_CONTEXT_TOKEN } from './member-type.detail.store'; +import { UmbMemberTypeStore, UMB_MEMBER_TYPE_STORE_CONTEXT_TOKEN } from './member-type.store'; import { UmbMemberTypeDetailServerDataSource } from './sources/member-type.detail.server.data'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbContextConsumerController } from '@umbraco-cms/context-api'; @@ -21,7 +21,7 @@ export class UmbMemberTypeRepository implements UmbTreeRepository, UmbDetailRepo #treeStore?: UmbMemberTypeTreeStore; #detailSource: UmbMemberTypeDetailServerDataSource; - #detailStore?: UmbMemberTypeDetailStore; + #store?: UmbMemberTypeStore; #notificationContext?: UmbNotificationContext; @@ -33,8 +33,8 @@ export class UmbMemberTypeRepository implements UmbTreeRepository, UmbDetailRepo this.#detailSource = new UmbMemberTypeDetailServerDataSource(this.#host); this.#init = Promise.all([ - new UmbContextConsumerController(this.#host, UMB_MEMBER_TYPE_DETAIL_STORE_CONTEXT_TOKEN, (instance) => { - this.#detailStore = instance; + new UmbContextConsumerController(this.#host, UMB_MEMBER_TYPE_STORE_CONTEXT_TOKEN, (instance) => { + this.#store = instance; }), new UmbContextConsumerController(this.#host, UMB_MEMBER_TYPE_TREE_STORE_CONTEXT_TOKEN, (instance) => { @@ -123,7 +123,7 @@ export class UmbMemberTypeRepository implements UmbTreeRepository, UmbDetailRepo const { data, error } = await this.#detailSource.requestByKey(key); if (data) { - this.#detailStore?.append(data); + this.#store?.append(data); } return { data, error }; } @@ -146,7 +146,7 @@ export class UmbMemberTypeRepository implements UmbTreeRepository, UmbDetailRepo // TODO: we currently don't use the detail store for anything. // Consider to look up the data before fetching from the server. // Consider notify a workspace if a member type is deleted from the store while someone is editing it. - this.#detailStore?.remove([key]); + this.#store?.remove([key]); this.#treeStore?.removeItem(key); // TODO: would be nice to align the stores on methods/methodNames. @@ -173,7 +173,7 @@ export class UmbMemberTypeRepository implements UmbTreeRepository, UmbDetailRepo // TODO: we currently don't use the detail store for anything. // Consider to look up the data before fetching from the server // Consider notify a workspace if a member type is updated in the store while someone is editing it. - this.#detailStore?.append(detail); + this.#store?.append(detail); this.#treeStore?.updateItem(detail.key, { name: detail.name }); // TODO: would be nice to align the stores on methods/methodNames. diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.store.ts similarity index 63% rename from src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.detail.store.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.store.ts index ebeb8af837..c13dacc56f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.detail.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.store.ts @@ -6,17 +6,15 @@ import type { MemberTypeDetails } from '@umbraco-cms/models'; /** * @export - * @class UmbMemberTypeDetailStore + * @class UmbMemberTypeStore * @extends {UmbStoreBase} - * @description - Details Data Store for Member Types + * @description - Data Store for Member Types */ -export class UmbMemberTypeDetailStore - extends UmbStoreBase -{ +export class UmbMemberTypeStore extends UmbStoreBase { #data = new ArrayState([], (x) => x.key); constructor(host: UmbControllerHostInterface) { - super(host, UMB_MEMBER_TYPE_DETAIL_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_MEMBER_TYPE_STORE_CONTEXT_TOKEN.toString()); } append(MemberType: MemberTypeDetails) { @@ -28,6 +26,4 @@ export class UmbMemberTypeDetailStore } } -export const UMB_MEMBER_TYPE_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken( - 'UmbMemberTypeDetailStore' -); +export const UMB_MEMBER_TYPE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberTypeStore'); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.tree.store.ts index 3b87c4988f..d10cf2953e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.tree.store.ts @@ -9,7 +9,6 @@ import type { UmbControllerHostInterface } from '@umbraco-cms/controller'; * @description - Tree Data Store for Member Types */ export class UmbMemberTypeTreeStore extends UmbTreeStoreBase { - constructor(host: UmbControllerHostInterface) { super(host, UMB_MEMBER_TYPE_TREE_STORE_CONTEXT_TOKEN.toString()); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/entity-actions/manifests.ts index 6f040a256b..57654c41d0 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/entity-actions/manifests.ts @@ -1,3 +1,4 @@ +import { MEMBER_REPOSITORY_ALIAS } from '../repository/manifests'; import { UmbDeleteEntityAction } from '@umbraco-cms/entity-action'; import { ManifestEntityAction } from 'libs/extensions-registry/entity-action.models'; @@ -11,7 +12,7 @@ const entityActions: Array = [ icon: 'umb:trash', label: 'Delete', api: UmbDeleteEntityAction, - repositoryAlias: 'Umb.Repository.Member', + repositoryAlias: MEMBER_REPOSITORY_ALIAS, }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.detail.store.ts index bb665251ad..0a79950126 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.detail.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.detail.store.ts @@ -6,33 +6,29 @@ import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbEntityDetailStore, UmbStoreBase } from '@umbraco-cms/store'; import { umbMemberData } from 'src/core/mocks/data/member.data'; -export const UMB_MEMBER_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberDetailStore'); - /** * @export - * @class UmbMemberDetailStore + * @class UmbMemberStore * @extends {UmbStoreBase} - * @description - Detail Data Store for Members + * @description - Data Store for Members */ -export class UmbMemberDetailStore extends UmbStoreBase implements UmbEntityDetailStore { - - #data = new ArrayState([], x => x.key); +export class UmbMemberStore extends UmbStoreBase implements UmbEntityDetailStore { + #data = new ArrayState([], (x) => x.key); public groups = this.#data.asObservable(); constructor(private host: UmbControllerHostInterface) { - super(host, UMB_MEMBER_DETAIL_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_MEMBER_STORE_CONTEXT_TOKEN.toString()); } getScaffold(entityType: string, parentKey: string | null) { - return { - } as MemberDetails; + return {} as MemberDetails; } /** * @description - Request a Member by key. The Member is added to the store and is returned as an Observable. * @param {string} key * @return {*} {(Observable)} - * @memberof UmbMemberDetailStore + * @memberof UmbMemberStore */ getByKey(key: string): Observable { // tryExecuteAndNotify(this.host, MemberResource.getMemberByKey({ key })).then(({ data }) => { @@ -47,13 +43,12 @@ export class UmbMemberDetailStore extends UmbStoreBase implements UmbEntityDetai this.#data.appendOne(member); } - return createObservablePart( - this.#data, - (members) => members.find((member) => member.key === key) as MemberDetails - ); + return createObservablePart(this.#data, (members) => members.find((member) => member.key === key) as MemberDetails); } async save(member: Array): Promise { return null as any; } } + +export const UMB_MEMBER_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberStore'); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/manifests.ts index cce97581b9..a2e150c0ca 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/manifests.ts @@ -1,5 +1,8 @@ import { UmbMemberRepository } from './member.repository'; +import { UmbMemberStore } from './member.store'; +import { UmbMemberTreeStore } from './member.tree.store'; import { ManifestRepository } from 'libs/extensions-registry/repository.models'; +import { ManifestStore, ManifestTreeStore } from '@umbraco-cms/extensions-registry'; export const MEMBER_REPOSITORY_ALIAS = 'Umb.Repository.Member'; @@ -10,4 +13,21 @@ const repository: ManifestRepository = { class: UmbMemberRepository, }; -export const manifests = [repository]; +export const MEMBER_STORE_ALIAS = 'Umb.Store.Member'; +export const MEMBER_TREE_STORE_ALIAS = 'Umb.Store.MemberTree'; + +const store: ManifestStore = { + type: 'store', + alias: MEMBER_STORE_ALIAS, + name: 'Member Store', + class: UmbMemberStore, +}; + +const treeStore: ManifestTreeStore = { + type: 'treeStore', + alias: MEMBER_TREE_STORE_ALIAS, + name: 'Member Tree Store', + class: UmbMemberTreeStore, +}; + +export const manifests = [store, treeStore, repository]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.repository.ts index db2744a5ed..74b1d752fe 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.repository.ts @@ -73,7 +73,7 @@ export class UmbMemberRepository implements UmbTreeRepository { async rootTreeItems() { await this.#init; - return this.#treeStore!.rootItems(); + return this.#treeStore!.rootItems; } async treeItemsOf(parentKey: string | null) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.store.ts new file mode 100644 index 0000000000..991ada05f4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.store.ts @@ -0,0 +1,29 @@ +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { ArrayState } from '@umbraco-cms/observable-api'; +import type { MemberDetails } from '@umbraco-cms/models'; + +/** + * @export + * @class UmbMemberStore + * @extends {UmbStoreBase} + * @description - Data Store for Members + */ +export class UmbMemberStore extends UmbStoreBase { + #data = new ArrayState([], (x) => x.key); + + constructor(host: UmbControllerHostInterface) { + super(host, UMB_MEMBER_STORE_CONTEXT_TOKEN.toString()); + } + + append(member: MemberDetails) { + this.#data.append([member]); + } + + remove(uniques: string[]) { + this.#data.remove(uniques); + } +} + +export const UMB_MEMBER_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberStore'); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.tree.store.ts index c3ea8a4e18..6dd095fded 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.tree.store.ts @@ -1,7 +1,5 @@ -import type { EntityTreeItemModel } from '@umbraco-cms/backend-api'; import { UmbContextToken } from '@umbraco-cms/context-api'; -import { ArrayState } from '@umbraco-cms/observable-api'; -import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbTreeStoreBase } from '@umbraco-cms/store'; import type { UmbControllerHostInterface } from '@umbraco-cms/controller'; export const UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMemberTreeStore'); @@ -9,12 +7,10 @@ export const UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken([], (x) => x.key); - +export class UmbMemberTreeStore extends UmbTreeStoreBase { /** * Creates an instance of UmbTemplateTreeStore. * @param {UmbControllerHostInterface} host @@ -23,71 +19,4 @@ export class UmbMemberTreeStore extends UmbStoreBase { constructor(host: UmbControllerHostInterface) { super(host, UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN.toString()); } - - /** - * Appends items to the store - * @param {Array} items - * @memberof UmbTemplateTreeStore - */ - appendItems(items: Array) { - this.#data.append(items); - } - - /** - * Updates an item in the store - * @param {string} key - * @param {Partial} data - * @memberof UmbMemberGroupTreeStore - */ - updateItem(key: string, data: Partial) { - const entries = this.#data.getValue(); - const entry = entries.find((entry) => entry.key === key); - - if (entry) { - this.#data.appendOne({ ...entry, ...data }); - } - } - - /** - * Removes an item from the store - * @param {string} key - * @memberof UmbMemberGroupTreeStore - */ - removeItem(key: string) { - const entries = this.#data.getValue(); - const entry = entries.find((entry) => entry.key === key); - - if (entry) { - this.#data.remove([key]); - } - } - - /** - * Returns an observable to observe the root items - * @return {*} - * @memberof UmbMemberGroupTreeStore - */ - rootItems() { - return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === null)); - } - - /** - * Returns an observable to observe the children of a given parent - * @param {(string | null)} parentKey - * @return {*} - * @memberof UmbMemberGroupTreeStore - */ - childrenOf(parentKey: string | null) { - return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === parentKey)); - } - - /** - * Returns an observable to observe the items with the given keys - * @param {Array} keys - * @return {*} - * @memberof UmbMemberGroupTreeStore - */ - items(keys: Array) { - return this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? ''))); - } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/sources/member.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/sources/member.tree.server.data.ts index 4a73347356..4f4fce08c8 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/sources/member.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/sources/member.tree.server.data.ts @@ -1,5 +1,4 @@ import { MemberTreeDataSource } from '.'; -import { ProblemDetailsModel } from '@umbraco-cms/backend-api'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; /** diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/workspace/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/workspace/member-workspace.context.ts index b7cb38e6ec..a032a5fefc 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/workspace/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/workspace/member-workspace.context.ts @@ -1,7 +1,7 @@ import { UmbEntityWorkspaceManager } from '../../../shared/components/workspace/workspace-context/entity-manager-controller'; import { UmbWorkspaceContext } from '../../../shared/components/workspace/workspace-context/workspace-context'; import { UmbWorkspaceEntityContextInterface } from '../../../shared/components/workspace/workspace-context/workspace-entity-context.interface'; -import { UMB_MEMBER_DETAIL_STORE_CONTEXT_TOKEN } from '../member.detail.store'; +import { UMB_MEMBER_STORE_CONTEXT_TOKEN } from '../member.detail.store'; import { UmbMemberRepository } from '../repository/member.repository'; import type { MemberDetails } from '@umbraco-cms/models'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; @@ -10,7 +10,7 @@ export class UmbWorkspaceMemberContext extends UmbWorkspaceContext implements UmbWorkspaceEntityContextInterface { - #manager = new UmbEntityWorkspaceManager(this.host, 'member', UMB_MEMBER_DETAIL_STORE_CONTEXT_TOKEN); + #manager = new UmbEntityWorkspaceManager(this.host, 'member', UMB_MEMBER_STORE_CONTEXT_TOKEN); public readonly data = this.#manager.state.asObservable(); public readonly name = this.#manager.state.getObservablePart((state) => state?.name); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/packages/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/packages/index.ts index 7a1e274f75..f0a7107779 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/packages/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/packages/index.ts @@ -1,3 +1,4 @@ +import { manifests as repositoryManifests } from './repository/manifests'; import { manifests as packageBuilderManifests } from './package-builder/manifests'; import { manifests as packageRepoManifests } from './package-repo/manifests'; import { manifests as packageSectionManifests } from './package-section/manifests'; @@ -5,11 +6,15 @@ import { manifests as packageSectionManifests } from './package-section/manifest import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; import { ManifestTypes } from '@umbraco-cms/extensions-registry'; +export const manifests = [ + ...repositoryManifests, + ...packageBuilderManifests, + ...packageRepoManifests, + ...packageSectionManifests, +]; + const registerExtensions = (manifests: Array) => { - manifests.forEach((manifest) => { - if (umbExtensionsRegistry.isRegistered(manifest.alias)) return; - umbExtensionsRegistry.register(manifest); - }); + manifests.forEach((manifest) => umbExtensionsRegistry.register(manifest)); }; -registerExtensions([...packageBuilderManifests, ...packageRepoManifests, ...packageSectionManifests]); +registerExtensions(manifests); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/created/packages-created-overview.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/created/packages-created-overview.element.ts index 20b33ad3f8..0d23f0c4ef 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/created/packages-created-overview.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/created/packages-created-overview.element.ts @@ -7,6 +7,7 @@ import { PackageDefinitionModel, PackageResource } from '@umbraco-cms/backend-ap import { UmbLitElement } from '@umbraco-cms/element'; import { tryExecuteAndNotify } from '@umbraco-cms/resources'; import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; +import { UMB_CONFIRM_MODAL_TOKEN } from 'src/backoffice/shared/modals/confirm'; @customElement('umb-packages-created-overview') export class UmbPackagesCreatedOverviewElement extends UmbLitElement { @@ -134,18 +135,14 @@ export class UmbPackagesCreatedOverviewElement extends UmbLitElement { async #deletePackage(p: PackageDefinitionModel) { if (!p.key) return; - const modalHandler = this._modalContext?.confirm({ + const modalHandler = this._modalContext?.open(UMB_CONFIRM_MODAL_TOKEN, { color: 'danger', headline: `Remove ${p.name}?`, content: 'Are you sure you want to delete this package', confirmLabel: 'Delete', }); - const deleteConfirmed = await modalHandler?.onClose().then(({ confirmed }: any) => { - return confirmed; - }); - - if (!deleteConfirmed == true) return; + await modalHandler?.onSubmit(); const { error } = await tryExecuteAndNotify(this, PackageResource.deletePackageCreatedByKey({ key: p.key })); if (error) return; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/installed/installed-packages-section-view-item.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/installed/installed-packages-section-view-item.element.ts index 3df5afd999..e8ff26a0dd 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/installed/installed-packages-section-view-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/installed/installed-packages-section-view-item.element.ts @@ -4,7 +4,7 @@ import { customElement, property, state } from 'lit/decorators.js'; import { firstValueFrom, map } from 'rxjs'; import { UUIButtonState } from '@umbraco-ui/uui'; -import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '../../../../../core/modal'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; import { createExtensionElement, umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; import type { ManifestPackageView } from '@umbraco-cms/models'; @@ -12,6 +12,7 @@ import { UmbLitElement } from '@umbraco-cms/element'; import { tryExecuteAndNotify } from '@umbraco-cms/resources'; import { PackageResource } from '@umbraco-cms/backend-api'; import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/notification'; +import { UMB_CONFIRM_MODAL_TOKEN } from 'src/backoffice/shared/modals/confirm'; @customElement('umb-installed-packages-section-view-item') export class UmbInstalledPackagesSectionViewItem extends UmbLitElement { @@ -81,18 +82,15 @@ export class UmbInstalledPackagesSectionViewItem extends UmbLitElement { async _onMigration() { if (!this.name) return; - const modalHandler = this._modalContext?.confirm({ + const modalHandler = this._modalContext?.open(UMB_CONFIRM_MODAL_TOKEN, { color: 'positive', headline: `Run migrations for ${this.name}?`, content: `Do you want to start run migrations for ${this.name}`, confirmLabel: 'Run migrations', }); - const migrationConfirmed = await modalHandler?.onClose().then(({ confirmed }: any) => { - return confirmed; - }); + await modalHandler?.onSubmit(); - if (!migrationConfirmed == true) return; this._migrationButtonState = 'waiting'; const { error } = await tryExecuteAndNotify( this, @@ -141,11 +139,15 @@ export class UmbInstalledPackagesSectionViewItem extends UmbLitElement { return; } + // TODO: add dedicated modal for package views, and register it in a manifest. + alert('package view modal temporarily disabled. See comment in code.'); + /* this._modalContext?.open(element, { data: { name: this.name, version: this.version }, size: 'full', type: 'sidebar', }); + */ } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/installed/installed-packages-section-view.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/installed/installed-packages-section-view.element.ts index 08449e0cad..7880202c02 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/installed/installed-packages-section-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-section/views/installed/installed-packages-section-view.element.ts @@ -1,4 +1,4 @@ -import { html, css, nothing } from 'lit'; +import { html, css } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; import { combineLatest } from 'rxjs'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/packages/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/packages/repository/manifests.ts new file mode 100644 index 0000000000..ab052945f7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/packages/repository/manifests.ts @@ -0,0 +1,24 @@ +import { UmbPackageRepository } from './package.repository'; +import { UmbPackageStore } from './package.store'; +import { ManifestRepository } from 'libs/extensions-registry/repository.models'; +import { ManifestStore } from '@umbraco-cms/extensions-registry'; + +export const PACKAGE_REPOSITORY_ALIAS = 'Umb.Repository.Package'; + +const repository: ManifestRepository = { + type: 'repository', + alias: PACKAGE_REPOSITORY_ALIAS, + name: 'Package Repository', + class: UmbPackageRepository, +}; + +export const PACKAGE_STORE_ALIAS = 'Umb.Store.Package'; + +const store: ManifestStore = { + type: 'store', + alias: PACKAGE_STORE_ALIAS, + name: 'Package Store', + class: UmbPackageStore, +}; + +export const manifests = [store, repository]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/search/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/search/index.ts index 01360ce456..e0d341e7cc 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/search/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/search/index.ts @@ -3,11 +3,10 @@ import { manifests as searchManifests } from '../search/manifests'; import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; import { ManifestTypes } from '@umbraco-cms/extensions-registry'; +export const manifests = [...searchManifests]; + const registerExtensions = (manifests: Array) => { - manifests.forEach((manifest) => { - if (umbExtensionsRegistry.isRegistered(manifest.alias)) return; - umbExtensionsRegistry.register(manifest); - }); + manifests.forEach((manifest) => umbExtensionsRegistry.register(manifest)); }; -registerExtensions([...searchManifests]); +registerExtensions(manifests); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/search/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/search/modals/manifests.ts new file mode 100644 index 0000000000..7765c306fc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/search/modals/manifests.ts @@ -0,0 +1,12 @@ +import type { ManifestModal } from '@umbraco-cms/extensions-registry'; + +const modals: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.Search', + name: 'Search Modal', + loader: () => import('./search/search-modal.element'), + }, +]; + +export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/search/modals/search/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/search/modals/search/index.ts new file mode 100644 index 0000000000..9859d03434 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/search/modals/search/index.ts @@ -0,0 +1,3 @@ +import { UmbModalToken } from '@umbraco-cms/modal'; + +export const UMB_SEARCH_MODAL_TOKEN = new UmbModalToken('Umb.Modal.Search'); diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/search/modals/search/search-modal.element.ts similarity index 97% rename from src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/search/modals/search/search-modal.element.ts index df4ef06d2f..b294c49eda 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/search/modals/search/search-modal.element.ts @@ -14,8 +14,8 @@ export type SearchGroupItem = { name: string; items: Array; }; -@customElement('umb-modal-layout-search') -export class UmbModalLayoutSearchElement extends LitElement { +@customElement('umb-search-modal') +export class UmbSearchModalElement extends LitElement { static styles = [ UUITextStyles, css` @@ -308,10 +308,10 @@ export class UmbModalLayoutSearchElement extends LitElement { ]; } -export default UmbModalLayoutSearchElement; +export default UmbSearchModalElement; declare global { interface HTMLElementTagNameMap { - 'umb-modal-layout-search': UmbModalLayoutSearchElement; + 'umb-search-modal': UmbSearchModalElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/manifests.ts index 0bc2ae18bd..7502a562d3 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/manifests.ts @@ -1,7 +1,7 @@ import { UmbCultureRepository } from '../repository/culture.repository'; import { ManifestRepository } from 'libs/extensions-registry/repository.models'; -export const CULTURE_REPOSITORY_ALIAS = 'Umb.Repository.Cultures'; +export const CULTURE_REPOSITORY_ALIAS = 'Umb.Repository.Culture'; const repository: ManifestRepository = { type: 'repository', diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/modal-views/fields-settings.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/modal-views/fields-settings.element.ts index ce43b2b8cd..8da8cf647e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/modal-views/fields-settings.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/modal-views/fields-settings.element.ts @@ -1,15 +1,14 @@ import { html, css } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, state } from 'lit/decorators.js'; -import { UmbModalLayoutElement } from '../../../../../../core/modal'; +import { UmbCreateDocumentModalResultData, UmbExamineFieldsSettingsModalData } from '.'; +import { UmbModalBaseElement } from '@umbraco-cms/modal'; -export interface UmbModalFieldsSettingsData { - name: string; - exposed: boolean; -} - -@customElement('umb-modal-layout-fields-settings') -export class UmbModalLayoutFieldsSettingsElement extends UmbModalLayoutElement { +@customElement('umb-examine-fields-settings-modal') +export class UmbExamineFieldsSettingsModalElement extends UmbModalBaseElement< + UmbExamineFieldsSettingsModalData, + UmbCreateDocumentModalResultData +> { static styles = [ UUITextStyles, css` @@ -44,10 +43,10 @@ export class UmbModalLayoutFieldsSettingsElement extends UmbModalLayoutElement { - return { name: field.name, exposed: field.exposed }; - })) - : ''; + this._fields = + this.data?.map((field) => { + return { name: field.name, exposed: field.exposed }; + }) || undefined; } render() { @@ -92,6 +90,6 @@ export class UmbModalLayoutFieldsSettingsElement extends UmbModalLayoutElement { +@customElement('umb-modal-element-fields-viewer') +export class UmbModalElementFieldsViewerElement extends UmbModalBaseElement { static styles = [ UUITextStyles, css` @@ -16,7 +16,6 @@ export class UmbModalLayoutFieldsViewerElement extends UmbModalLayoutElement; + +export interface UmbCreateDocumentModalResultData { + fields?: UmbExamineFieldsSettingsModalData; +} + +export const UMB_EXAMINE_FIELDS_SETTINGS_MODAL_TOKEN = new UmbModalToken< + UmbExamineFieldsSettingsModalData, + UmbCreateDocumentModalResultData +>('Umb.Modal.ExamineFieldsSettings', { + type: 'sidebar', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/section-view-examine-indexers.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/section-view-examine-indexers.ts index 34c20af5d2..1fcfb4384d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/section-view-examine-indexers.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/section-view-examine-indexers.ts @@ -1,16 +1,14 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html, nothing } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; - import { UUIButtonState } from '@umbraco-ui/uui-button'; - -import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '../../../../../core/modal'; - -import './section-view-examine-searchers'; - +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; import { HealthStatusModel, IndexModel, IndexerResource } from '@umbraco-cms/backend-api'; import { UmbLitElement } from '@umbraco-cms/element'; import { tryExecuteAndNotify } from '@umbraco-cms/resources'; +import { UMB_CONFIRM_MODAL_TOKEN } from 'src/backoffice/shared/modals/confirm'; + +import './section-view-examine-searchers'; @customElement('umb-dashboard-examine-index') export class UmbDashboardExamineIndexElement extends UmbLitElement { @@ -120,7 +118,7 @@ export class UmbDashboardExamineIndexElement extends UmbLitElement { } private async _onRebuildHandler() { - const modalHandler = this._modalContext?.confirm({ + const modalHandler = this._modalContext?.open(UMB_CONFIRM_MODAL_TOKEN, { headline: `Rebuild ${this.indexName}`, content: html` This will cause the index to be rebuilt.
@@ -131,8 +129,8 @@ export class UmbDashboardExamineIndexElement extends UmbLitElement { color: 'danger', confirmLabel: 'Rebuild', }); - modalHandler?.onClose().then(({ confirmed }) => { - if (confirmed) this._rebuild(); + modalHandler?.onSubmit().then(() => { + this._rebuild(); }); } private async _rebuild() { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/section-view-examine-searchers.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/section-view-examine-searchers.ts index 941f79b998..fd7f1b6f14 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/section-view-examine-searchers.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/section-view-examine-searchers.ts @@ -1,15 +1,14 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html, nothing } from 'lit'; import { customElement, state, query, property } from 'lit/decorators.js'; - -import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '../../../../../core/modal'; - +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; import { SearchResultModel, SearcherResource, FieldModel } from '@umbraco-cms/backend-api'; import { UmbLitElement } from '@umbraco-cms/element'; import { tryExecuteAndNotify } from '@umbraco-cms/resources'; import './modal-views/fields-viewer.element'; import './modal-views/fields-settings.element'; +import { UMB_EXAMINE_FIELDS_SETTINGS_MODAL_TOKEN } from './modal-views'; interface ExposedSearchResultField { name?: string | null; @@ -175,12 +174,10 @@ export class UmbDashboardExamineSearcherElement extends UmbLitElement { } private _onFieldFilterClick() { - const modalHandler = this._modalContext?.open('umb-modal-layout-fields-settings', { - type: 'sidebar', - size: 'small', - data: { ...this._exposedFields }, + const modalHandler = this._modalContext?.open(UMB_EXAMINE_FIELDS_SETTINGS_MODAL_TOKEN, { + ...this._exposedFields, }); - modalHandler?.onClose().then(({ fields } = {}) => { + modalHandler?.onSubmit().then(({ fields } = {}) => { if (!fields) return; this._exposedFields = fields; }); @@ -241,7 +238,7 @@ export class UmbDashboardExamineSearcherElement extends UmbLitElement { look="secondary" label="Open sidebar to see all fields" @click="${() => - this._modalContext?.open('umb-modal-layout-fields-viewer', { + this._modalContext?.open('umb-modal-element-fields-viewer', { type: 'sidebar', size: 'medium', data: { ...rowData, name: this.getSearchResultNodeName(rowData) }, diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/manifests.ts index 16ca3c3f9b..f3475347ba 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/manifests.ts @@ -1,4 +1,4 @@ -import type { ManifestDashboard } from '@umbraco-cms/models'; +import type { ManifestDashboard, ManifestModal } from '@umbraco-cms/models'; const dashboards: Array = [ { @@ -94,4 +94,13 @@ const dashboards: Array = [ }, ]; -export const manifests = [...dashboards]; +const modals: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.ExamineFieldsSettings', + name: 'Examine Field Settings Modal', + loader: () => import('./examine-management/views/modal-views/fields-settings.element'), + }, +]; + +export const manifests = [...dashboards, ...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/published-status/dashboard-published-status.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/published-status/dashboard-published-status.element.ts index 8235fd382f..3d79b89afc 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/published-status/dashboard-published-status.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/published-status/dashboard-published-status.element.ts @@ -2,9 +2,8 @@ import { UUIButtonState } from '@umbraco-ui/uui'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; - -import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '../../../../core/modal'; - +import { UMB_CONFIRM_MODAL_TOKEN } from '../../../shared/modals/confirm'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; import { PublishedCacheResource } from '@umbraco-cms/backend-api'; import { tryExecuteAndNotify } from '@umbraco-cms/resources'; import { UmbLitElement } from '@umbraco-cms/element'; @@ -82,14 +81,14 @@ export class UmbDashboardPublishedStatusElement extends UmbLitElement { } } private async _onReloadCacheHandler() { - const modalHandler = this._modalContext?.confirm({ + const modalHandler = this._modalContext?.open(UMB_CONFIRM_MODAL_TOKEN, { headline: 'Reload', content: html` Trigger a in-memory and local file cache reload on all servers. `, color: 'danger', confirmLabel: 'Continue', }); - modalHandler?.onClose().then(({ confirmed }) => { - if (confirmed) this._reloadMemoryCache(); + modalHandler?.onSubmit().then(() => { + this._reloadMemoryCache(); }); } @@ -105,14 +104,14 @@ export class UmbDashboardPublishedStatusElement extends UmbLitElement { } private async _onRebuildCacheHandler() { - const modalHandler = this._modalContext?.confirm({ + const modalHandler = this._modalContext?.open(UMB_CONFIRM_MODAL_TOKEN, { headline: 'Rebuild', content: html` Rebuild content in cmsContentNu database table. Expensive.`, color: 'danger', confirmLabel: 'Continue', }); - modalHandler?.onClose().then(({ confirmed }) => { - if (confirmed) this._rebuildDatabaseCache(); + modalHandler?.onSubmit().then(() => { + this._rebuildDatabaseCache(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/manifests.ts index fbe0167037..4d54186a90 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/manifests.ts @@ -1,13 +1,33 @@ import { UmbDataTypeRepository } from '../repository/data-type.repository'; +import { UmbDataTypeStore } from './data-type.store'; +import { UmbDataTypeTreeStore } from './data-type.tree.store'; import { ManifestRepository } from 'libs/extensions-registry/repository.models'; +import { ManifestStore, ManifestTreeStore } from '@umbraco-cms/extensions-registry'; -export const DATA_TYPE_REPOSITORY_ALIAS = 'Umb.Repository.DataTypes'; +export const DATA_TYPE_REPOSITORY_ALIAS = 'Umb.Repository.DataType'; const repository: ManifestRepository = { type: 'repository', alias: DATA_TYPE_REPOSITORY_ALIAS, - name: 'Data Types Repository', + name: 'Data Type Repository', class: UmbDataTypeRepository, }; -export const manifests = [repository]; +export const DATA_TYPE_STORE_ALIAS = 'Umb.Store.DataType'; +export const DATA_TYPE_TREE_STORE_ALIAS = 'Umb.Store.DataTypeTree'; + +const store: ManifestStore = { + type: 'store', + alias: DATA_TYPE_STORE_ALIAS, + name: 'Data Type Store', + class: UmbDataTypeStore, +}; + +const treeStore: ManifestTreeStore = { + type: 'treeStore', + alias: DATA_TYPE_TREE_STORE_ALIAS, + name: 'Data Type Tree Store', + class: UmbDataTypeTreeStore, +}; + +export const manifests = [repository, store, treeStore]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/views/edit/data-type-workspace-view-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/views/edit/data-type-workspace-view-edit.element.ts index c3495deef6..fd12cca7b2 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/views/edit/data-type-workspace-view-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/views/edit/data-type-workspace-view-edit.element.ts @@ -1,8 +1,9 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html, nothing } from 'lit'; import { customElement, state } from 'lit/decorators.js'; -import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '../../../../../../core/modal'; import { UmbDataTypeWorkspaceContext } from '../../data-type-workspace.context'; +import { UMB_PROPERTY_EDITOR_UI_PICKER_MODAL_TOKEN } from '../../../../../shared/property-editors/modals/property-editor-ui-picker'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; import { UmbLitElement } from '@umbraco-cms/element'; import type { DataTypeModel } from '@umbraco-cms/backend-api'; import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; @@ -100,12 +101,11 @@ export class UmbDataTypeWorkspaceViewEditElement extends UmbLitElement { private _openPropertyEditorUIPicker() { if (!this._dataType) return; - const modalHandler = this._modalContext?.propertyEditorUIPicker({ + const modalHandler = this._modalContext?.open(UMB_PROPERTY_EDITOR_UI_PICKER_MODAL_TOKEN, { selection: this._propertyEditorUiAlias ? [this._propertyEditorUiAlias] : [], }); - modalHandler?.onClose().then(({ selection } = {}) => { - if (!selection) return; + modalHandler?.onSubmit().then(({ selection }) => { this._selectPropertyEditorUI(selection[0]); }); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/extensions/workspace/extension-root-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/extensions/workspace/extension-root-workspace.element.ts index 3558e546ce..f53810199b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/extensions/workspace/extension-root-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/extensions/workspace/extension-root-workspace.element.ts @@ -4,6 +4,7 @@ import { isManifestElementNameType, umbExtensionsRegistry } from '@umbraco-cms/e import type { ManifestBase } from '@umbraco-cms/models'; import { UmbLitElement } from '@umbraco-cms/element'; import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; +import { UMB_CONFIRM_MODAL_TOKEN } from 'src/backoffice/shared/modals/confirm'; @customElement('umb-extension-root-workspace') export class UmbExtensionRootWorkspaceElement extends UmbLitElement { @@ -27,19 +28,16 @@ export class UmbExtensionRootWorkspaceElement extends UmbLitElement { }); } - #removeExtension(extension: ManifestBase) { - const modalHandler = this._modalContext?.confirm({ + async #removeExtension(extension: ManifestBase) { + const modalHandler = this._modalContext?.open(UMB_CONFIRM_MODAL_TOKEN, { headline: 'Unload extension', confirmLabel: 'Unload', content: html`

Are you sure you want to unload the extension ${extension.alias}?

`, color: 'danger', }); - modalHandler?.onClose().then(({ confirmed }: any) => { - if (confirmed) { - umbExtensionsRegistry.unregister(extension.alias); - } - }); + await modalHandler?.onSubmit(); + umbExtensionsRegistry.unregister(extension.alias); } render() { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/index.ts index f566504513..abe3d1ba75 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/index.ts @@ -10,14 +10,7 @@ import { manifests as logviewerManifests } from './logviewer/manifests'; import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; import { ManifestTypes } from '@umbraco-cms/extensions-registry'; -const registerExtensions = (manifests: Array) => { - manifests.forEach((manifest) => { - if (umbExtensionsRegistry.isRegistered(manifest.alias)) return; - umbExtensionsRegistry.register(manifest); - }); -}; - -registerExtensions([ +export const manifests = [ ...settingsSectionManifests, ...settingsMenuManifests, ...dashboardManifests, @@ -26,4 +19,10 @@ registerExtensions([ ...cultureManifests, ...languageManifests, ...logviewerManifests, -]); +]; + +const registerExtensions = (manifests: Array) => { + manifests.forEach((manifest) => umbExtensionsRegistry.register(manifest)); +}; + +registerExtensions(manifests); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/entity-actions/manifests.ts index e8846b75a6..7852f3e09c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/entity-actions/manifests.ts @@ -1,8 +1,8 @@ +import { LANGUAGE_REPOSITORY_ALIAS } from '../repository/manifests'; import { UmbDeleteEntityAction } from '@umbraco-cms/entity-action'; import { ManifestEntityAction } from '@umbraco-cms/extensions-registry'; const entityType = 'language'; -const repositoryAlias = 'Umb.Repository.Languages'; const entityActions: Array = [ { @@ -11,7 +11,7 @@ const entityActions: Array = [ name: 'Delete Language Entity Action', meta: { entityType, - repositoryAlias, + repositoryAlias: LANGUAGE_REPOSITORY_ALIAS, icon: 'umb:trash', label: 'Delete', api: UmbDeleteEntityAction, diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/manifests.ts index 4d649f5cd8..92434c12ef 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/manifests.ts @@ -3,6 +3,7 @@ import { manifests as treeManifests } from './menu-item/manifests'; import { manifests as entityActions } from './entity-actions/manifests'; import { manifests as workspaceManifests } from './workspace/manifests'; import { manifests as appLanguageSelect } from './app-language-select/manifests'; +import { manifests as modalManifests } from './modals/manifests'; export const manifests = [ ...repositoryManifests, @@ -10,4 +11,5 @@ export const manifests = [ ...treeManifests, ...workspaceManifests, ...appLanguageSelect, + ...modalManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/modals/language-picker/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/modals/language-picker/index.ts new file mode 100644 index 0000000000..5f551178ad --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/modals/language-picker/index.ts @@ -0,0 +1,20 @@ +import { LanguageModel } from '@umbraco-cms/backend-api'; +import { UmbModalToken } from '@umbraco-cms/modal'; + +export interface UmbLanguagePickerModalData { + multiple?: boolean; + selection?: Array; + filter?: (language: LanguageModel) => boolean; +} + +export interface UmbLanguagePickerModalResult { + selection: Array; +} + +export const UMB_LANGUAGE_PICKER_MODAL_TOKEN = new UmbModalToken< + UmbLanguagePickerModalData, + UmbLanguagePickerModalResult +>('Umb.Modal.LanguagePicker', { + type: 'sidebar', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language-picker/language-picker-modal-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/modals/language-picker/language-picker-modal.element.ts similarity index 79% rename from src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language-picker/language-picker-modal-layout.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/modals/language-picker/language-picker-modal.element.ts index f4f9d72886..98a4407e97 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language-picker/language-picker-modal-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/modals/language-picker/language-picker-modal.element.ts @@ -4,17 +4,12 @@ import { customElement, state } from 'lit/decorators.js'; import { repeat } from 'lit-html/directives/repeat.js'; import { UUIMenuItemElement, UUIMenuItemEvent } from '@umbraco-ui/uui'; import { ifDefined } from 'lit-html/directives/if-defined.js'; -import { UmbLanguageRepository } from '../repository/language.repository'; -import { UmbModalLayoutPickerBase } from '../../../../core/modal/layouts/modal-layout-picker-base'; +import { UmbLanguageRepository } from '../../repository/language.repository'; +import { UmbModalElementPickerBase } from '@umbraco-cms/modal'; import { LanguageModel } from '@umbraco-cms/backend-api'; -export interface UmbLanguagePickerModalData { - multiple: boolean; - selection: string[]; -} - -@customElement('umb-language-picker-modal-layout') -export class UmbLanguagePickerModalLayoutElement extends UmbModalLayoutPickerBase { +@customElement('umb-language-picker-modal') +export class UmbLanguagePickerModalElement extends UmbModalElementPickerBase { static styles = [UUITextStyles, css``]; @state() @@ -70,8 +65,10 @@ export class UmbLanguagePickerModalLayoutElement extends UmbModalLayoutPickerBas } } +export default UmbLanguagePickerModalElement; + declare global { interface HTMLElementTagNameMap { - 'umb-language-picker-modal-layout': UmbLanguagePickerModalLayoutElement; + 'umb-language-picker-modal': UmbLanguagePickerModalElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/modals/manifests.ts new file mode 100644 index 0000000000..06cf317a04 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/modals/manifests.ts @@ -0,0 +1,12 @@ +import type { ManifestModal } from '@umbraco-cms/extensions-registry'; + +const modals: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.LanguagePicker', + name: 'Language Picker Modal', + loader: () => import('./language-picker/language-picker-modal.element'), + }, +]; + +export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/manifests.ts index 7020f13c22..a69836165b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/manifests.ts @@ -1,7 +1,9 @@ import { UmbLanguageRepository } from '../repository/language.repository'; +import { UmbLanguageStore } from './language.store'; import { ManifestRepository } from 'libs/extensions-registry/repository.models'; +import { ManifestStore } from '@umbraco-cms/extensions-registry'; -export const LANGUAGE_REPOSITORY_ALIAS = 'Umb.Repository.Languages'; +export const LANGUAGE_REPOSITORY_ALIAS = 'Umb.Repository.Language'; const repository: ManifestRepository = { type: 'repository', @@ -10,4 +12,13 @@ const repository: ManifestRepository = { class: UmbLanguageRepository, }; -export const manifests = [repository]; +export const LANGUAGE_STORE_ALIAS = 'Umb.Store.Language'; + +const store: ManifestStore = { + type: 'store', + alias: LANGUAGE_STORE_ALIAS, + name: 'Language Store', + class: UmbLanguageStore, +}; + +export const manifests = [repository, store]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/menu-item/manifests.ts index dc7b08158f..558df963b7 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/menu-item/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/menu-item/manifests.ts @@ -8,7 +8,7 @@ const menuItem: ManifestMenuItem = { meta: { label: 'Log Viewer', icon: 'umb:box-alt', - entityType: 'logviewer-root', + entityType: 'logviewer', menus: ['Umb.Menu.Settings'], }, }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/repository/log-viewer.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/repository/log-viewer.repository.ts new file mode 100644 index 0000000000..5870e28762 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/repository/log-viewer.repository.ts @@ -0,0 +1,101 @@ +import { UmbLogMessagesServerDataSource, UmbLogSearchesServerDataSource } from './sources/log-viewer.server.data'; +import { UmbContextConsumerController } from '@umbraco-cms/context-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/notification'; +import { DirectionModel, LogLevelModel } from '@umbraco-cms/backend-api'; + +// Move to documentation / JSdoc +/* We need to create a new instance of the repository from within the element context. We want the notifications to be displayed in the right context. */ +// element -> context -> repository -> (store) -> data source +// All methods should be async and return a promise. Some methods might return an observable as part of the promise response. +export class UmbLogViewerRepository { + #host: UmbControllerHostInterface; + #searchDataSource: UmbLogSearchesServerDataSource; + #messagesDataSource: UmbLogMessagesServerDataSource; + #notificationService?: UmbNotificationContext; + #initResolver?: () => void; + #initialized = false; + + constructor(host: UmbControllerHostInterface) { + this.#host = host; + this.#searchDataSource = new UmbLogSearchesServerDataSource(this.#host); + this.#messagesDataSource = new UmbLogMessagesServerDataSource(this.#host); + + new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { + this.#notificationService = instance; + this.#checkIfInitialized(); + }); + } + + #init() { + // TODO: This would only works with one user of this method. If two, the first one would be forgotten, but maybe its alright for now as I guess this is temporary. + return new Promise((resolve) => { + this.#initialized ? resolve() : (this.#initResolver = resolve); + }); + } + + #checkIfInitialized() { + if (this.#notificationService) { + this.#initialized = true; + this.#initResolver?.(); + } + } + + async getSavedSearches({ skip, take }: { skip: number; take: number }) { + await this.#init(); + + return this.#searchDataSource.getAllSavedSearches({ skip, take }); + } + + async getMessageTemplates({ skip, take }: { skip: number; take: number }) { + await this.#init(); + + return this.#messagesDataSource.getLogViewerMessageTemplate({ skip, take }); + } + + async getLogCount({ startDate, endDate }: { startDate?: string; endDate?: string }) { + await this.#init(); + + return this.#messagesDataSource.getLogViewerLevelCount({ startDate, endDate }); + } + + async getLogs({ + skip = 0, + take = 100, + orderDirection, + filterExpression, + logLevel, + startDate, + endDate, + }: { + skip?: number; + take?: number; + orderDirection?: DirectionModel; + filterExpression?: string; + logLevel?: Array; + startDate?: string; + endDate?: string; + }) { + await this.#init(); + + return this.#messagesDataSource.getLogViewerLogs({ + skip, + take, + orderDirection, + filterExpression, + logLevel, + startDate, + endDate, + }); + } + + async getLogLevels({ skip = 0, take = 100 }: { skip: number; take: number }) { + await this.#init(); + return this.#messagesDataSource.getLogViewerLevel({ skip, take }); + } + + async getLogViewerValidateLogsSize({ startDate, endDate }: { startDate?: string; endDate?: string }) { + await this.#init(); + return this.#messagesDataSource.getLogViewerValidateLogsSize({ startDate, endDate }); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/repository/sources/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/repository/sources/index.ts new file mode 100644 index 0000000000..518a5e18af --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/repository/sources/index.ts @@ -0,0 +1,69 @@ +import { + DirectionModel, + LogLevelCountsModel, + LogLevelModel, + PagedLoggerModel, + PagedLogMessageModel, + PagedLogTemplateModel, + PagedSavedLogSearchModel, + SavedLogSearchModel, +} from '@umbraco-cms/backend-api'; +import type { DataSourceResponse } from '@umbraco-cms/models'; + + + +export interface LogSearchDataSource { + getAllSavedSearches({ + skip, + take, + }: { + skip?: number; + take?: number; + }): Promise>; + getSavedSearchByName({ name }: { name: string }): Promise>; + deleteSavedSearchByName({ name }: { name: string }): Promise>; + postLogViewerSavedSearch({ + requestBody, + }: { + requestBody?: SavedLogSearchModel; + }): Promise>; +} + +export interface LogMessagesDataSource { + getLogViewerLevel({ skip, take }: { skip?: number; take?: number }): Promise>; + getLogViewerLevelCount({ + startDate, + endDate, + }: { + startDate?: string; + endDate?: string; + }): Promise>; + getLogViewerLogs({ + skip, + take = 100, + orderDirection, + filterExpression, + logLevel, + startDate, + endDate, + }: { + skip?: number; + take?: number; + orderDirection?: DirectionModel; + filterExpression?: string; + logLevel?: Array; + startDate?: string; + endDate?: string; + }): Promise>; + getLogViewerMessageTemplate({ + skip, + take = 100, + startDate, + endDate, + }: { + skip?: number; + take?: number; + startDate?: string; + endDate?: string; + }): Promise>; +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/repository/sources/log-viewer.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/repository/sources/log-viewer.server.data.ts new file mode 100644 index 0000000000..ebdf1264e5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/repository/sources/log-viewer.server.data.ts @@ -0,0 +1,213 @@ +import { LogMessagesDataSource, LogSearchDataSource } from '.'; +import { DirectionModel, LogLevelModel, LogViewerResource, SavedLogSearchModel } from '@umbraco-cms/backend-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; + +/** + * A data source for the log saved searches + * @export + * @class UmbLogSearchesServerDataSource + * @implements {TemplateDetailDataSource} + */ +export class UmbLogSearchesServerDataSource implements LogSearchDataSource { + #host: UmbControllerHostInterface; + + /** + * Creates an instance of UmbLogSearchesServerDataSource. + * @param {UmbControllerHostInterface} host + * @memberof UmbLogSearchesServerDataSource + */ + constructor(host: UmbControllerHostInterface) { + this.#host = host; + } + + /** + * Grabs all the log viewer saved searches from the server + * + * @param {{ skip?: number; take?: number }} { skip = 0, take = 100 } + * @return {*} + * @memberof UmbLogSearchesServerDataSource + */ + async getAllSavedSearches({ skip = 0, take = 100 }: { skip?: number; take?: number }) { + return await tryExecuteAndNotify(this.#host, LogViewerResource.getLogViewerSavedSearch({ skip, take })); + } + /** + * Get a log viewer saved search by name from the server + * + * @param {{ name: string }} { name } + * @return {*} + * @memberof UmbLogSearchesServerDataSource + */ + async getSavedSearchByName({ name }: { name: string }) { + return await tryExecuteAndNotify(this.#host, LogViewerResource.getLogViewerSavedSearchByName({ name })); + } + + /** + * Post a new log viewer saved search to the server + * + * @param {{ requestBody?: SavedLogSearch }} { requestBody } + * @return {*} + * @memberof UmbLogSearchesServerDataSource + */ + async postLogViewerSavedSearch({ requestBody }: { requestBody?: SavedLogSearchModel }) { + return await tryExecuteAndNotify(this.#host, LogViewerResource.postLogViewerSavedSearch({ requestBody })); + } + /** + * Remove a log viewer saved search by name from the server + * + * @param {{ name: string }} { name } + * @return {*} + * @memberof UmbLogSearchesServerDataSource + */ + async deleteSavedSearchByName({ name }: { name: string }) { + return await tryExecuteAndNotify(this.#host, LogViewerResource.deleteLogViewerSavedSearchByName({ name })); + } +} + /** + * A data source for the log messages and levels + * + * @export + * @class UmbLogMessagesServerDataSource + * @implements {LogMessagesDataSource} + */ + export class UmbLogMessagesServerDataSource implements LogMessagesDataSource { + #host: UmbControllerHostInterface; + + /** + * Creates an instance of UmbLogMessagesServerDataSource. + * @param {UmbControllerHostInterface} host + * @memberof UmbLogMessagesServerDataSource + */ + constructor(host: UmbControllerHostInterface) { + this.#host = host; + } + + /** + * Grabs all the loggers from the server + * + * @param {{ skip?: number; take?: number }} { skip = 0, take = 100 } + * @return {*} + * @memberof UmbLogMessagesServerDataSource + */ + async getLogViewerLevel({ skip = 0, take = 100 }: { skip?: number; take?: number }) { + return await tryExecuteAndNotify(this.#host, LogViewerResource.getLogViewerLevel({ skip, take })); + } + + /** + * Grabs all the number of different log messages from the server + * + * @param {{ skip?: number; take?: number }} { skip = 0, take = 100 } + * @return {*} + * @memberof UmbLogMessagesServerDataSource + */ + async getLogViewerLevelCount({ startDate, endDate }: { startDate?: string; endDate?: string }) { + return await tryExecuteAndNotify( + this.#host, + LogViewerResource.getLogViewerLevelCount({ + startDate, + endDate, + }) + ); + } + /** + * Grabs all the log messages from the server + * + * @param {{ + * skip?: number; + * take?: number; + * orderDirection?: DirectionModel; + * filterExpression?: string; + * logLevel?: Array; + * startDate?: string; + * endDate?: string; + * }} { + * skip = 0, + * take = 100, + * orderDirection, + * filterExpression, + * logLevel, + * startDate, + * endDate, + * } + * @return {*} + * @memberof UmbLogMessagesServerDataSource + */ + async getLogViewerLogs({ + skip = 0, + take = 100, + orderDirection, + filterExpression, + logLevel, + startDate, + endDate, + }: { + skip?: number; + take?: number; + orderDirection?: DirectionModel; + filterExpression?: string; + logLevel?: Array; + startDate?: string; + endDate?: string; + }) { + return await tryExecuteAndNotify( + this.#host, + LogViewerResource.getLogViewerLog({ + skip, + take, + orderDirection, + filterExpression, + logLevel, + startDate, + endDate, + }) + ); + } + /** + * Grabs all the log message templates from the server + * + * @param {{ + * skip?: number; + * take?: number; + * startDate?: string; + * endDate?: string; + * }} { + * skip, + * take = 100, + * startDate, + * endDate, + * } + * @return {*} + * @memberof UmbLogMessagesServerDataSource + */ + async getLogViewerMessageTemplate({ + skip, + take = 100, + startDate, + endDate, + }: { + skip?: number; + take?: number; + startDate?: string; + endDate?: string; + }) { + return await tryExecuteAndNotify( + this.#host, + LogViewerResource.getLogViewerMessageTemplate({ + skip, + take, + startDate, + endDate, + }) + ); + } + + async getLogViewerValidateLogsSize({ startDate, endDate }: { startDate?: string; endDate?: string }) { + return await tryExecuteAndNotify( + this.#host, + LogViewerResource.getLogViewerValidateLogsSize({ + startDate, + endDate, + }) + ); + } + } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/components/index.ts new file mode 100644 index 0000000000..e204c32424 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/components/index.ts @@ -0,0 +1,3 @@ +export * from './log-viewer-date-range-selector.element'; +export * from './log-viewer-level-tag.element'; +export * from './log-viewer-to-many-logs-warning.element'; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/components/log-viewer-date-range-selector.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/components/log-viewer-date-range-selector.element.ts new file mode 100644 index 0000000000..cfa465cb79 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/components/log-viewer-date-range-selector.element.ts @@ -0,0 +1,132 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, property, queryAll, state } from 'lit/decorators.js'; +import { query } from 'router-slot'; +import { + LogViewerDateRange, + UmbLogViewerWorkspaceContext, + UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, +} from '../../logviewer.context'; +import { UmbLitElement } from '@umbraco-cms/element'; + +@customElement('umb-log-viewer-date-range-selector') +export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + :host { + display: flex; + flex-direction: column; + gap: var(--uui-size-space-3); + } + + input { + font-family: inherit; + padding: var(--uui-size-1) var(--uui-size-space-3); + font-size: inherit; + color: inherit; + border-radius: 0; + box-sizing: border-box; + border: none; + background: none; + width: 100%; + height: 100%; + outline: none; + position: relative; + border-bottom: 2px solid transparent; + } + + /* find out better validation for that */ + input:out-of-range { + border-color: var(--uui-color-danger); + } + + :host([horizontal]) .input-container { + display: flex; + align-items: baseline; + } + `, + ]; + + @state() + private _startDate = ''; + + @state() + private _endDate = ''; + + @queryAll('input') + private _inputs!: NodeListOf; + + @property({ type: Boolean, reflect: true }) + horizontal = false; + + #logViewerContext?: UmbLogViewerWorkspaceContext; + constructor() { + super(); + this.addEventListener('input', this.#setDates); + this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => { + this.#logViewerContext = instance; + this.#logViewerContext?.getMessageTemplates(0, 10); + this.#observeStuff(); + }); + } + + #observeStuff() { + if (!this.#logViewerContext) return; + this.observe(this.#logViewerContext.dateRange, (dateRange: LogViewerDateRange) => { + this._startDate = dateRange?.startDate; + this._endDate = dateRange?.endDate; + }); + } + + #setDates() { + this._inputs.forEach((input) => { + if (input.id === 'start-date') { + this._startDate = input.value; + } else if (input.id === 'end-date') { + this._endDate = input.value; + } + }); + const newDateRange: LogViewerDateRange = { startDate: this._startDate, endDate: this._endDate }; + this.#logViewerContext?.setDateRange(newDateRange); + } + + render() { + return html` +
+ From: + { + (e.target as HTMLInputElement).showPicker(); + }} + + id="start-date" + type="date" + label="From" + .max=${this.#logViewerContext?.today ?? ''} + .value=${this._startDate}> + +
+
+ To: + { + (e.target as HTMLInputElement).showPicker(); + }} + id="end-date" + type="date" + label="To" + .min=${this._startDate} + .max=${this.#logViewerContext?.today ?? ''} + .value=${this._endDate}> + +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-log-viewer-date-range-selector': UmbLogViewerDateRangeSelectorElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/components/log-viewer-level-tag.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/components/log-viewer-level-tag.element.ts new file mode 100644 index 0000000000..d0fc55eb86 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/components/log-viewer-level-tag.element.ts @@ -0,0 +1,47 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html, LitElement } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { ifDefined } from 'lit-html/directives/if-defined.js'; +import { InterfaceColor, InterfaceLook } from '@umbraco-ui/uui-base/lib/types'; +import { LogLevelModel } from '@umbraco-cms/backend-api'; + +interface LevelMapStyles { + look?: InterfaceLook; + color?: InterfaceColor; + style?: string; +} + +@customElement('umb-log-viewer-level-tag') +export class UmbLogViewerLevelTagElement extends LitElement { + static styles = [UUITextStyles, css``]; + + @property() + level?: LogLevelModel; + + levelMap: Record = { + Verbose: { look: 'secondary' }, + Debug: { + look: 'default', + style: 'background-color: var(--umb-log-viewer-debug-color); color: var(--uui-color-surface)', + }, + Information: { look: 'primary', color: 'positive' }, + Warning: { look: 'primary', color: 'warning' }, + Error: { look: 'primary', color: 'danger' }, + Fatal: { look: 'primary' }, + }; + + render() { + return html`${this.level}`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-log-viewer-level-tag': UmbLogViewerLevelTagElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/components/log-viewer-to-many-logs-warning.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/components/log-viewer-to-many-logs-warning.element.ts new file mode 100644 index 0000000000..b0bdb0c1ce --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/components/log-viewer-to-many-logs-warning.element.ts @@ -0,0 +1,27 @@ +import { css, html, LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +@customElement('umb-log-viewer-to-many-logs-warning') +export class UmbLogViewerToManyLogsWarningElement extends LitElement { + static styles = [ + css` + :host { + text-align: center; + } + `, + ]; + + render() { + return html` +

Unable to view logs

+

Today's log file is too large to be viewed and would cause performance problems.

+

If you need to view the log files, narrow your date range or try opening them manually.

+
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-log-viewer-to-many-logs-warning': UmbLogViewerToManyLogsWarningElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/logviewer-root-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/logviewer-root-workspace.element.ts index f3f77a5ded..8579ccc331 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/logviewer-root-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/logviewer-root-workspace.element.ts @@ -1,21 +1,190 @@ -import { html, LitElement } from 'lit'; -import { customElement } from 'lit/decorators.js'; +import './components'; +import { map } from 'rxjs'; +import { css, html, nothing } from 'lit'; +import { customElement, state, property } from 'lit/decorators.js'; +import { IRoutingInfo } from 'router-slot'; +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { repeat } from 'lit-html/directives/repeat.js'; +import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../logviewer.context'; +import { UmbLitElement } from '@umbraco-cms/element'; +import { umbExtensionsRegistry, createExtensionElement } from '@umbraco-cms/extensions-api'; +import { ManifestWorkspaceView, ManifestWorkspaceViewCollection } from '@umbraco-cms/extensions-registry'; +import { UmbRouterSlotInitEvent, UmbRouterSlotChangeEvent } from '@umbraco-cms/router'; + +//TODO make uui-input accept min and max values +@customElement('umb-logviewer-workspace') +export class UmbLogViewerWorkspaceElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + :host { + display: block; + width: 100%; + height: 100%; + + --umb-log-viewer-debug-color: var(--uui-color-default-emphasis); + --umb-log-viewer-information-color: var(--uui-color-positive); + --umb-log-viewer-warning-color: var(--uui-color-warning); + --umb-log-viewer-error-color: var(--uui-color-danger); + --umb-log-viewer-fatal-color: var(--uui-color-default); + --umb-log-viewer-verbose-color: var(--uui-color-current); + } + + #header { + display: flex; + padding: 0 var(--uui-size-space-6); + gap: var(--uui-size-space-4); + align-items: center; + } + + #router-slot { + height: 100%; + } + + uui-tab-group { + --uui-tab-divider: var(--uui-color-border); + border-left: 1px solid var(--uui-color-border); + border-right: 1px solid var(--uui-color-border); + } + `, + ]; + + private _alias = 'Umb.Workspace.LogviewerRoot'; + + @state() + private _workspaceViews: Array = []; + + @state() + private _routes: any[] = []; + + @state() + private _activePath?: string; + + @state() + private _routerPath?: string; + + #logViewerContext = new UmbLogViewerWorkspaceContext(this); + + constructor() { + super(); + this.#logViewerContext.init(); + } + + connectedCallback() { + super.connectedCallback(); + this._observeWorkspaceViews(); + this.provideContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, this.#logViewerContext); + } + + load(): void { + // Not relevant for this workspace -added to prevent the error from popping up + } + + private _observeWorkspaceViews() { + this.observe( + umbExtensionsRegistry + .extensionsOfTypes(['workspaceView']) + .pipe(map((extensions) => extensions.filter((extension) => extension.meta.workspaces.includes(this._alias)))), + (workspaceViews) => { + this._workspaceViews = workspaceViews; + this._createRoutes(); + } + ); + } + + create(): void { + // Not relevant for this workspace + } + + private _createRoutes() { + this._routes = []; + + if (this._workspaceViews.length > 0) { + this._routes = this._workspaceViews.map((view) => { + return { + path: `${view.meta.pathname}`, + component: () => { + return createExtensionElement(view); + }, + setup: (component: Promise | HTMLElement, info: IRoutingInfo) => { + // When its using import, we get an element, when using createExtensionElement we get a Promise. + if ((component as any).then) { + (component as any).then((el: any) => (el.manifest = view)); + } else { + (component as any).manifest = view; + } + }, + }; + }); + + this._routes.push({ + path: '**', + redirectTo: `${this._workspaceViews[0].meta.pathname}`, + }); + } + } + + #renderRoutes() { + return html` + ${this._routes.length > 0 + ? html` + { + this._routerPath = event.target.absoluteRouterPath; + }} + @change=${(event: UmbRouterSlotChangeEvent) => { + this._activePath = event.target.localActiveViewPath; + }}> + ` + : nothing} + `; + } + + #renderViews() { + return html` + ${this._workspaceViews.length > 1 + ? html` + + ${repeat( + this._workspaceViews, + (view) => view.alias, + (view) => html` + + + ${view.meta.label || view.name} + + ` + )} + + ` + : nothing} + `; + } -@customElement('umb-logviewer-root-workspace') -export class UmbLogViewerRootWorkspaceElement extends LitElement { render() { return html` -
-

LogViewer Root Workspace

-
+ + + ${this.#renderViews()} ${this.#renderRoutes()} + + `; } } -export default UmbLogViewerRootWorkspaceElement; +export default UmbLogViewerWorkspaceElement; declare global { interface HTMLElementTagNameMap { - 'umb-logviewer-root-workspace': UmbLogViewerRootWorkspaceElement; + 'umb-logviewer-workspace': UmbLogViewerWorkspaceElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/manifests.ts index 1e06c4df7d..c740c29aa1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer-root/manifests.ts @@ -1,16 +1,45 @@ import type { ManifestWorkspace, ManifestWorkspaceAction, ManifestWorkspaceView } from '@umbraco-cms/models'; +const workspaceAlias = 'Umb.Workspace.LogviewerRoot'; + const workspace: ManifestWorkspace = { type: 'workspace', - alias: 'Umb.Workspace.LogviewerRoot', + alias: workspaceAlias, name: 'LogViewer Root Workspace', loader: () => import('./logviewer-root-workspace.element'), meta: { - entityType: 'logviewer-root', + entityType: 'logviewer', }, }; -const workspaceViews: Array = []; +const workspaceViews: Array = [ + { + type: 'workspaceView', + alias: 'Umb.WorkspaceView.Logviewer.Overview', + name: 'LogViewer Root Workspace Overview View', + loader: () => import('../views/overview/index'), + weight: 300, + meta: { + workspaces: [workspaceAlias], + label: 'Overview', + pathname: 'overview', + icon: 'umb:box-alt', + }, + }, + { + type: 'workspaceView', + alias: 'Umb.WorkspaceView.Logviewer.Search', + name: 'LogViewer Root Workspace Search View', + loader: () => import('../views/search/index'), + weight: 200, + meta: { + workspaces: [workspaceAlias], + label: 'Search', + pathname: 'search', + icon: 'umb:search', + }, + }, +]; const workspaceActions: Array = []; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer.context.ts new file mode 100644 index 0000000000..93d7896cc6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer.context.ts @@ -0,0 +1,253 @@ +import { UmbLogViewerRepository } from '../repository/log-viewer.repository'; +import { ArrayState, createObservablePart, DeepState, ObjectState, StringState } from '@umbraco-cms/observable-api'; +import { + DirectionModel, + LogLevelCountsModel, + LogLevelModel, + PagedLoggerModel, + PagedLogMessageModel, + PagedLogTemplateModel, + PagedSavedLogSearchModel, +} from '@umbraco-cms/backend-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { BasicState } from 'libs/observable-api/basic-state'; + +export type PoolingInterval = 0 | 2000 | 5000 | 10000 | 20000 | 30000; +export interface PoolingCOnfig { + enabled: boolean; + interval: PoolingInterval; +} +export interface LogViewerDateRange { + startDate: string; + endDate: string; +} + +export class UmbLogViewerWorkspaceContext { + #host: UmbControllerHostInterface; + #repository: UmbLogViewerRepository; + + get today() { + const today = new Date(); + const dd = String(today.getDate()).padStart(2, '0'); + const mm = String(today.getMonth() + 1).padStart(2, '0'); + const yyyy = today.getFullYear(); + + return yyyy + '-' + mm + '-' + dd; + } + + get yesterday() { + const yesterday = new Date(new Date().setDate(new Date().getDate() - 1)); + const dd = String(yesterday.getDate()).padStart(2, '0'); + const mm = String(yesterday.getMonth() + 1).padStart(2, '0'); + const yyyy = yesterday.getFullYear(); + + return yyyy + '-' + mm + '-' + dd; + } + + defaultDateRange: LogViewerDateRange = { + startDate: this.yesterday, + endDate: this.today, + }; + + #savedSearches = new DeepState(undefined); + savedSearches = createObservablePart(this.#savedSearches, (data) => data?.items); + + #logCount = new DeepState(null); + logCount = createObservablePart(this.#logCount, (data) => data); + + #dateRange = new DeepState(this.defaultDateRange); + dateRange = createObservablePart(this.#dateRange, (data) => data); + + #loggers = new DeepState(null); + loggers = createObservablePart(this.#loggers, (data) => data?.items); + + #canShowLogs = new BasicState(null); + canShowLogs = createObservablePart(this.#canShowLogs, (data) => data); + + #filterExpression = new StringState(''); + filterExpression = createObservablePart(this.#filterExpression, (data) => data); + + #messageTemplates = new DeepState(null); + messageTemplates = createObservablePart(this.#messageTemplates, (data) => data); + + #logLevelsFilter = new ArrayState([]); + logLevelsFilter = createObservablePart(this.#logLevelsFilter, (data) => data); + + #logs = new DeepState(null); + logs = createObservablePart(this.#logs, (data) => data?.items); + logsTotal = createObservablePart(this.#logs, (data) => data?.total); + + #polling = new ObjectState({ enabled: false, interval: 2000 }); + polling = createObservablePart(this.#polling, (data) => data); + + #sortingDirection = new BasicState(DirectionModel.ASCENDING); + sortingDirection = createObservablePart(this.#sortingDirection, (data) => data); + + #intervalID: number | null = null; + + currentPage = 1; + + constructor(host: UmbControllerHostInterface) { + this.#host = host; + this.#repository = new UmbLogViewerRepository(this.#host); + } + + async init() { + this.validateLogSize(); + } + + setDateRange(dateRange: LogViewerDateRange) { + const { startDate, endDate } = dateRange; + + const isAnyDateInTheFuture = new Date(startDate) > new Date() || new Date(endDate) > new Date(); + const isStartDateBiggerThenEndDate = new Date(startDate) > new Date(endDate); + if (isAnyDateInTheFuture || isStartDateBiggerThenEndDate) { + return; + } + + this.#dateRange.next(dateRange); + this.validateLogSize(); + this.getLogCount(); + } + + async getSavedSearches() { + const { data } = await this.#repository.getSavedSearches({ skip: 0, take: 100 }); + if (data) { + this.#savedSearches.next(data); + } else { + //falback to some default searches like in the old backoffice + this.#savedSearches.next({ + items: [ + { + name: 'Find all logs where the Level is NOT Verbose and NOT Debug', + query: "Not(@Level='Verbose') and Not(@Level='Debug')", + }, + { + name: 'Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)', + query: 'Has(@Exception)', + }, + { + name: "Find all logs that have the property 'Duration'", + query: 'Has(Duration)', + }, + { + name: "Find all logs that have the property 'Duration' and the duration is greater than 1000ms", + query: 'Has(Duration) and Duration > 1000', + }, + { + name: "Find all logs that are from the namespace 'Umbraco.Core'", + query: "StartsWith(SourceContext, 'Umbraco.Core')", + }, + { + name: 'Find all logs that use a specific log message template', + query: "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'", + }, + ], + total: 6, + }); + } + } + + async getLogCount() { + const { data } = await this.#repository.getLogCount({ ...this.#dateRange.getValue() }); + + if (data) { + this.#logCount.next(data); + } + } + + async getMessageTemplates(skip: number, take: number) { + const { data } = await this.#repository.getMessageTemplates({ skip, take }); + + if (data) { + this.#messageTemplates.next(data); + } + } + + async getLogLevels(skip: number, take: number) { + const { data } = await this.#repository.getLogLevels({ skip, take }); + + if (data) { + this.#loggers.next(data); + } + } + + async validateLogSize() { + const { data, error } = await this.#repository.getLogViewerValidateLogsSize({ ...this.#dateRange.getValue() }); + if (error) { + this.#canShowLogs.next(false); + console.info('LogViewer: ', error); + return; + } + this.#canShowLogs.next(true); + console.info('LogViewer:showinfg logs'); + } + + setCurrentPage(page: number) { + this.currentPage = page; + } + + getLogs = async () => { + if (!this.#canShowLogs.getValue()) { + return; + } + + const skip = (this.currentPage - 1) * 100; + const take = 100; + + const options = { + skip, + take, + orderDirection: this.#sortingDirection.getValue(), + filterExpression: this.#filterExpression.getValue(), + logLevel: this.#logLevelsFilter.getValue(), + ...this.#dateRange.getValue(), + }; + + const { data } = await this.#repository.getLogs(options); + + if (data) { + this.#logs.next(data); + } + }; + + setFilterExpression(query: string) { + this.#filterExpression.next(query); + } + + setLogLevelsFilter(logLevels: LogLevelModel[]) { + this.#logLevelsFilter.next(logLevels); + } + + togglePolling() { + const isEnabled = !this.#polling.getValue().enabled; + this.#polling.update({ + enabled: isEnabled, + }); + + if (isEnabled) { + this.#intervalID = setInterval(() => { + this.currentPage = 1; + this.getLogs(); + }, this.#polling.getValue().interval) as unknown as number; + return; + } + + clearInterval(this.#intervalID as number); + } + + setPollingInterval(interval: PoolingInterval) { + this.#polling.update({ interval, enabled: true }); + } + + toggleSortOrder() { + const direction = this.#sortingDirection.getValue(); + const newDirection = direction === DirectionModel.ASCENDING ? DirectionModel.DESCENDING : DirectionModel.ASCENDING; + this.#sortingDirection.next(newDirection); + } +} + +export const UMB_APP_LOG_VIEWER_CONTEXT_TOKEN = new UmbContextToken( + UmbLogViewerWorkspaceContext.name +); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/manifests.ts index 4f3d6db1e4..5a8c3cc38d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/manifests.ts @@ -1,3 +1,4 @@ import { manifests as logviewerRootManifests } from './logviewer-root/manifests'; + export const manifests = [...logviewerRootManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/components/index.ts new file mode 100644 index 0000000000..926372cc6e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/components/index.ts @@ -0,0 +1,4 @@ +export * from './log-viewer-saved-searches-overview.element'; +export * from './log-viewer-message-templates-overview.element'; +export * from './log-viewer-log-types-chart.element'; +export * from './log-viewer-log-level-overview.element'; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/components/log-viewer-log-level-overview.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/components/log-viewer-log-level-overview.element.ts new file mode 100644 index 0000000000..2aeafb27fa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/components/log-viewer-log-level-overview.element.ts @@ -0,0 +1,48 @@ +import { html } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../../logviewer.context'; +import { UmbLitElement } from '@umbraco-cms/element'; +import { LoggerModel } from '@umbraco-cms/backend-api'; + +//TODO: implement the saved searches pagination when the API total bug is fixed +@customElement('umb-log-viewer-log-level-overview') +export class UmbLogViewerLogLevelOverviewElement extends UmbLitElement { + #logViewerContext?: UmbLogViewerWorkspaceContext; + constructor() { + super(); + this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => { + this.#logViewerContext = instance; + this.#logViewerContext?.getSavedSearches(); + this.#observeLogLevels(); + }); + } + + @state() + private _loggers: LoggerModel[] = []; + /** + * The name of the logger to get the level for. Defaults to 'Global'. + * + * @memberof UmbLogViewerLogLevelOverviewElement + */ + @property() + loggerName = 'Global'; + + #observeLogLevels() { + if (!this.#logViewerContext) return; + this.observe(this.#logViewerContext.loggers, (loggers) => { + this._loggers = loggers ?? []; + }); + } + + render() { + return html`${this._loggers.length > 0 + ? this._loggers.find((logger) => logger.name === this.loggerName)?.level + : ''}`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-log-viewer-log-level-overview': UmbLogViewerLogLevelOverviewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/components/log-viewer-log-types-chart.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/components/log-viewer-log-types-chart.element.ts new file mode 100644 index 0000000000..1c7ec7a927 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/components/log-viewer-log-types-chart.element.ts @@ -0,0 +1,167 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../../logviewer.context'; +import { UmbLitElement } from '@umbraco-cms/element'; +import { LogLevelCountsModel } from '@umbraco-cms/backend-api'; + +@customElement('umb-log-viewer-log-types-chart') +export class UmbLogViewerLogTypesChartElement extends UmbLitElement { + static styles = [ + css` + #log-types-container { + display: flex; + gap: var(--uui-size-space-4); + flex-direction: column-reverse; + align-items: center; + justify-content: space-between; + } + + button { + all: unset; + display: flex; + align-items: center; + cursor: pointer; + } + + button:focus { + outline: 1px solid var(--uui-color-focus); + } + + button.active { + text-decoration: line-through; + } + + #chart { + width: 150px; + aspect-ratio: 1; + background: radial-gradient(white 40%, transparent 41%), + conic-gradient( + var(--umb-log-viewer-debug-color) 0% 20%, + var(--umb-log-viewer-information-color) 20% 40%, + var(--umb-log-viewer-warning-color) 40% 60%, + var(--umb-log-viewer-error-color) 60% 80%, + var(--umb-log-viewer-fatal-color) 80% 100% + ); + margin: 10px; + display: inline-block; + border-radius: 50%; + } + + ul { + list-style: none; + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; + } + + li { + display: flex; + align-items: center; + } + + li uui-icon { + margin-right: 1em; + } + `, + ]; + + #logViewerContext?: UmbLogViewerWorkspaceContext; + constructor() { + super(); + this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => { + this.#logViewerContext = instance; + this.#logViewerContext?.getLogCount(); + this.#observeStuff(); + }); + } + + @state() + private _logLevelCount: LogLevelCountsModel | null = null; + + @state() + private logLevelCount: [string, number][] = []; + + @state() + private _logLevelCountFilter: string[] = []; + + protected willUpdate(_changedProperties: Map): void { + if (_changedProperties.has('_logLevelCountFilter')) { + this.setLogLevelCount(); + } + } + + #setCountFilter(level: string) { + if (this._logLevelCountFilter.includes(level)) { + this._logLevelCountFilter = this._logLevelCountFilter.filter((item) => item !== level); + return; + } + + this._logLevelCountFilter = [...this._logLevelCountFilter, level]; + } + + setLogLevelCount() { + this.logLevelCount = this._logLevelCount + ? Object.entries(this._logLevelCount).filter(([level, number]) => !this._logLevelCountFilter.includes(level)) + : []; + } + + #observeStuff() { + if (!this.#logViewerContext) return; + this.observe(this.#logViewerContext.logCount, (logLevel) => { + this._logLevelCount = logLevel ?? null; + this.setLogLevelCount(); + }); + } + + render() { + return html` + +
+
+
    + ${this._logLevelCount + ? Object.keys(this._logLevelCount).map( + (level) => + html`
  • + +
  • ` + ) + : ''} +
+
+ + ${this._logLevelCount + ? this.logLevelCount.map( + ([level, number]) => + html` ` + ) + : ''} + +
+
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-log-viewer-log-types-chart': UmbLogViewerLogTypesChartElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/components/log-viewer-message-templates-overview.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/components/log-viewer-message-templates-overview.element.ts new file mode 100644 index 0000000000..5c9cd6f754 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/components/log-viewer-message-templates-overview.element.ts @@ -0,0 +1,124 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../../logviewer.context'; +import { UmbLitElement } from '@umbraco-cms/element'; +import { PagedLogTemplateModel, SavedLogSearchModel } from '@umbraco-cms/backend-api'; + +//TODO: fix pagination bug when API is fixed +@customElement('umb-log-viewer-message-templates-overview') +export class UmbLogViewerMessageTemplatesOverviewElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + #show-more-templates-btn { + margin-top: var(--uui-size-space-5); + } + + a { + display: flex; + align-items: center; + justify-content: space-between; + text-decoration: none; + color: inherit; + } + + uui-table-cell { + padding: 10px 20px; + height: unset; + } + + uui-table-row { + cursor: pointer; + } + + uui-table-row:hover > uui-table-cell { + background-color: var(--uui-color-surface-alt); + } + `, + ]; + + @state() + private _messageTemplates: PagedLogTemplateModel | null = null; + + #logViewerContext?: UmbLogViewerWorkspaceContext; + constructor() { + super(); + this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => { + this.#logViewerContext = instance; + this.#logViewerContext?.getMessageTemplates(0, 10); + this.#observeStuff(); + }); + } + + #observeStuff() { + if (!this.#logViewerContext) return; + this.observe(this.#logViewerContext.messageTemplates, (templates) => { + this._messageTemplates = templates ?? null; + }); + } + + #getMessageTemplates() { + const take = this._messageTemplates?.items?.length ?? 0; + this.#logViewerContext?.getMessageTemplates(0, take + 10); + } + + #renderSearchItem = (searchListItem: SavedLogSearchModel) => { + return html`
  • + { + this.#setCurrentQuery(searchListItem.query ?? ''); + }} + label="${searchListItem.name ?? ''}" + title="${searchListItem.name ?? ''}" + href=${'/section/settings/logviewer/search?lq=' + searchListItem.query} + >${searchListItem.name} +
  • `; + }; + + #setCurrentQuery = (query: string) => { + this.#logViewerContext?.setFilterExpression(query); + }; + + render() { + return html` + +

    Total Unique Message types: ${this._messageTemplates?.total}

    + + + ${this._messageTemplates + ? this._messageTemplates.items.map( + (template) => + html` + { + this.#setCurrentQuery(`@MessageTemplate='${template.messageTemplate}'` ?? ''); + }} + href=${'/section/settings/logviewer/search?lg=@MessageTemplate%3D' + template.messageTemplate}> + ${template.messageTemplate} ${template.count} + + + ` + ) + : ''} + + + Show more +
    + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-log-viewer-message-templates-overview': UmbLogViewerMessageTemplatesOverviewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/components/log-viewer-saved-searches-overview.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/components/log-viewer-saved-searches-overview.element.ts new file mode 100644 index 0000000000..588e51bd9f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/components/log-viewer-saved-searches-overview.element.ts @@ -0,0 +1,101 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../../logviewer.context'; +import { UmbLitElement } from '@umbraco-cms/element'; +import { SavedLogSearchModel } from '@umbraco-cms/backend-api'; + +//TODO: implement the saved searches pagination when the API total bug is fixed +@customElement('umb-log-viewer-saved-searches-overview') +export class UmbLogViewerSavedSearchesOverviewElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + uui-box { + height: 100%; + } + + ul { + list-style: none; + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; + } + + li { + display: flex; + align-items: center; + } + + li uui-icon { + margin-right: 1em; + } + `, + ]; + + @state() + private _savedSearches: SavedLogSearchModel[] = []; + + #logViewerContext?: UmbLogViewerWorkspaceContext; + constructor() { + super(); + this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => { + this.#logViewerContext = instance; + this.#logViewerContext?.getSavedSearches(); + this.#observeStuff(); + }); + } + + #observeStuff() { + if (!this.#logViewerContext) return; + this.observe(this.#logViewerContext.savedSearches, (savedSearches) => { + this._savedSearches = savedSearches ?? []; + }); + } + + #setCurrentQuery(query: string) { + this.#logViewerContext?.setFilterExpression(query); + } + + #renderSearchItem = (searchListItem: SavedLogSearchModel) => { + return html`
  • + { + this.#setCurrentQuery(searchListItem.query ?? ''); + }} + label="${searchListItem.name ?? ''}" + title="${searchListItem.name ?? ''}" + href=${'/section/settings/logviewer/search?lq=' + searchListItem.query} + >${searchListItem.name} +
  • `; + }; + + render() { + return html` +
      +
    • + { + this.#setCurrentQuery(''); + }} + label="All logs" + title="All logs" + href="/section/settings/logviewer/search" + >All logs +
    • + ${this._savedSearches.map(this.#renderSearchItem)} +
    +
    `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-log-viewer-saved-searches-overview': UmbLogViewerSavedSearchesOverviewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/index.ts new file mode 100644 index 0000000000..cf7acfb3b9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/index.ts @@ -0,0 +1,4 @@ +import './components'; +import { UmbLogViewerOverviewViewElement } from './log-overview-view.element'; + +export default UmbLogViewerOverviewViewElement; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/log-overview-view.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/log-overview-view.element.ts new file mode 100644 index 0000000000..38191cf274 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/overview/log-overview-view.element.ts @@ -0,0 +1,159 @@ +import { css, html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../logviewer.context'; +import { LogLevelCountsModel } from '@umbraco-cms/backend-api'; +import { UmbLitElement } from '@umbraco-cms/element'; + +//TODO: add a disabled attribute to the show more button when the total number of items is correctly returned from the endpoint +@customElement('umb-log-viewer-overview-view') +export class UmbLogViewerOverviewViewElement extends UmbLitElement { + static styles = [ + css` + :host { + display: block; + } + + #logviewer-layout { + margin: 20px; + height: calc(100vh - 160px); + display: grid; + grid-template-columns: 7fr 2fr; + grid-template-rows: 1fr 1fr; + gap: 20px 20px; + grid-auto-flow: row; + grid-template-areas: + 'saved-searches info' + 'common-messages info'; + } + + #info { + grid-area: info; + align-self: start; + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(4, 1fr); + gap: 20px 20px; + } + + #time-period { + grid-area: 1 / 1 / 2 / 3; + } + + #errors { + grid-area: 2 / 1 / 3 / 2; + } + + #level { + grid-area: 2 / 2 / 3 / 3; + } + + #log-lever { + color: var(--uui-color-positive); + text-align: center; + } + + #types { + grid-area: 3 / 1 / 5 / 3; + } + + #saved-searches-container, + to-many-logs-warning { + grid-area: saved-searches; + } + + #common-messages-container { + grid-area: common-messages; + --uui-box-default-padding: 0 var(--uui-size-space-5, 18px) var(--uui-size-space-5, 18px) + var(--uui-size-space-5, 18px); + } + + #common-messages-container > uui-box { + height: 100%; + } + + uui-label:nth-of-type(2) { + display: block; + margin-top: var(--uui-size-space-5); + } + + #error-count { + font-size: 4rem; + text-align: center; + color: var(--uui-color-danger); + } + `, + ]; + + @state() + private _errorCount = 0; + + @state() + private _logLevelCount: LogLevelCountsModel | null = null; + + @state() + private _canShowLogs = false; + + #logViewerContext?: UmbLogViewerWorkspaceContext; + constructor() { + super(); + this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => { + this.#logViewerContext = instance; + this.#observeErrorCount(); + this.#observeCanShowLogs(); + this.#logViewerContext?.getLogLevels(0, 100); + }); + } + + #observeErrorCount() { + if (!this.#logViewerContext) return; + + this.observe(this.#logViewerContext.logCount, () => { + this._errorCount = this._logLevelCount?.error ?? 0; + }); + } + + #observeCanShowLogs() { + if (!this.#logViewerContext) return; + this.observe(this.#logViewerContext.canShowLogs, (canShowLogs) => { + this._canShowLogs = canShowLogs ?? false; + }); + } + + render() { + return html` +
    +
    + + + + + +

    ${this._errorCount}

    +
    + + +

    +
    + + +
    + + ${this._canShowLogs + ? html`
    + +
    + +
    + +
    ` + : html``} +
    + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-log-viewer-overview-view': UmbLogViewerOverviewViewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/index.ts new file mode 100644 index 0000000000..2157e5df73 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/index.ts @@ -0,0 +1,5 @@ +export * from './log-viewer-log-level-filter-menu.element'; +export * from './log-viewer-message.element'; +export * from './log-viewer-messages-list.element'; +export * from './log-viewer-polling-button.element'; +export * from './log-viewer-search-input.element'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-log-level-filter-menu.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-log-level-filter-menu.element.ts new file mode 100644 index 0000000000..dc72dcec91 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-log-level-filter-menu.element.ts @@ -0,0 +1,118 @@ +import { UUICheckboxElement } from '@umbraco-ui/uui'; +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, queryAll, state } from 'lit/decorators.js'; +import _ from 'lodash'; +import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../../logviewer.context'; +import { LogLevelModel } from '@umbraco-cms/backend-api'; +import { UmbLitElement } from '@umbraco-cms/element'; + +@customElement('umb-log-viewer-log-level-filter-menu') +export class UmbLogViewerLogLevelFilterMenuElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + #log-level-selector { + padding: var(--uui-box-default-padding, var(--uui-size-space-5, 18px)); + width: 150px; + background-color: var(--uui-color-surface); + box-shadow: var(--uui-shadow-depth-3); + display: flex; + flex-direction: column; + gap: var(--uui-size-space-3); + } + + .log-level-button-indicator { + font-weight: 600; + } + + .log-level-button-indicator:not(:last-of-type)::after { + content: ', '; + } + `, + ]; + + @queryAll('#log-level-selector > uui-checkbox') + private _logLevelSelectorCheckboxes!: NodeListOf; + + @state() + private _logLevelFilter: LogLevelModel[] = []; + + #logViewerContext?: UmbLogViewerWorkspaceContext; + + constructor() { + super(); + this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => { + this.#logViewerContext = instance; + this.#observeLogLevelFilter(); + }); + } + + #observeLogLevelFilter() { + if (!this.#logViewerContext) return; + + this.observe(this.#logViewerContext.logLevelsFilter, (levelsFilter) => { + this._logLevelFilter = levelsFilter ?? []; + }); + } + + #setLogLevel() { + if (!this.#logViewerContext) return; + this.#logViewerContext?.setCurrentPage(1); + + const logLevels = Array.from(this._logLevelSelectorCheckboxes) + .filter((checkbox) => checkbox.checked) + .map((checkbox) => checkbox.value as LogLevelModel); + this.#logViewerContext?.setLogLevelsFilter(logLevels); + this.#logViewerContext.getLogs(); + } + + setLogLevelDebounce = _.debounce(this.#setLogLevel, 300); + + #selectAllLogLevels() { + this._logLevelSelectorCheckboxes.forEach((checkbox) => (checkbox.checked = true)); + this.#setLogLevel(); + } + + #deselectAllLogLevels() { + this._logLevelSelectorCheckboxes.forEach((checkbox) => (checkbox.checked = false)); + this.#setLogLevel(); + } + + #renderLogLevelSelector() { + return html` +
    + ${Object.values(LogLevelModel).map( + (logLevel) => + html`` + )} + Select all + Deselect all +
    + `; + } + + render() { + return html` + Log Level: + ${this._logLevelFilter.length > 0 + ? this._logLevelFilter.map((level) => html`${level}`) + : 'All'} + ${this.#renderLogLevelSelector()} + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-log-viewer-log-level-filter-menu': UmbLogViewerLogLevelFilterMenuElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-message.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-message.element.ts new file mode 100644 index 0000000000..8bc5edb9c6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-message.element.ts @@ -0,0 +1,303 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html, PropertyValueMap } from 'lit'; +import { customElement, property, query, state } from 'lit/decorators.js'; +import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../../logviewer.context'; +import { LogLevelModel, LogMessagePropertyModel } from '@umbraco-cms/backend-api'; +import { UmbLitElement } from '@umbraco-cms/element'; + +//TODO: check how to display EventId field in the message properties +@customElement('umb-log-viewer-message') +export class UmbLogViewerMessageElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + :host > details { + border-top: 1px solid var(--uui-color-border); + } + + :host(:last-child) > details { + border-bottom: 1px solid var(--uui-color-border); + } + + summary { + display: flex; + } + + details[open] { + margin-bottom: var(--uui-size-space-3); + } + + summary:hover, + #properties-list { + background-color: var(--uui-color-background); + } + + #properties-list { + margin: 0; + padding: 0; + list-style: none; + margin-bottom: var(--uui-size-space-3); + } + + .property { + padding: 10px 20px; + display: flex; + border-top: 1px solid var(--uui-color-border); + } + + summary > div { + box-sizing: border-box; + padding: 10px 20px; + display: flex; + align-items: center; + } + + #timestamp { + flex: 1 0 14ch; + } + + #level, + #machine { + flex: 1 0 14ch; + } + + #message { + flex: 6 0 14ch; + } + + .property-name, + .property-value { + display: flex; + align-items: center; + } + + .property-name { + font-weight: 600; + flex: 1 1 20ch; + } + + .property-value { + flex: 3 0 20ch; + } + + #search-menu { + margin: 0; + padding: 0; + margin-top: var(--uui-size-space-3); + background-color: var(--uui-color-surface); + box-shadow: var(--uui-shadow-depth-3); + max-width: 25%; + } + + #search-menu > li { + padding: 0; + } + + .search-item { + width: 100%; + } + + pre { + background-color: var(--uui-color-background); + border-top: 1px solid #d8d7d9; + border-left: 4px solid #d42054; + color: #303033; + display: block; + font-family: Lato, Helvetica Neue, Helvetica, Arial, sans-serif; + line-height: 20px; + margin: 0; + overflow-x: auto; + padding: 9.5px; + white-space: pre-wrap; + } + `, + ]; + + @query('details') + details!: HTMLDetailsElement; + + @property() + timestamp = ''; + + @state() + date?: Date; + + @property() + level: LogLevelModel | '' = ''; + + @property() + messageTemplate = ''; + + @property() + renderedMessage = ''; + + @property({ attribute: false }) + properties: Array = []; + + @property({ type: Boolean }) + open = false; + + @property() + exception = ''; + + willUpdate(changedProperties: Map) { + if (changedProperties.has('timestamp')) { + this.date = new Date(this.timestamp); + } + } + + protected updated(_changedProperties: PropertyValueMap | Map): void { + if (_changedProperties.has('open')) { + this.open ? this.details.setAttribute('open', 'true') : this.details.removeAttribute('open'); + } + } + + #logViewerContext?: UmbLogViewerWorkspaceContext; + constructor() { + super(); + this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => { + this.#logViewerContext = instance; + }); + } + + private _searchMenuData: Array<{ label: string; href: () => string; icon: string; title: string }> = [ + { + label: 'Search in Google', + title: '@logViewer_searchThisMessageWithGoogle', + href: () => `https://www.google.com/search?q=${this.renderedMessage}`, + icon: 'https://www.google.com/favicon.ico', + }, + { + label: 'Search in Bing', + title: 'Search this message with Bing', + href: () => `https://www.bing.com/search?q=${this.renderedMessage}`, + icon: 'https://www.bing.com/favicon.ico', + }, + { + label: 'Search in OurUmbraco', + title: 'Search this message on Our Umbraco forums and docs', + href: () => `https://our.umbraco.com/search?q=${this.renderedMessage}&content=wiki,forum,documentation`, + icon: 'https://our.umbraco.com/assets/images/app-icons/favicon.png', + }, + { + label: 'Search in OurUmbraco with Google', + title: 'Search Our Umbraco forums using Google', + href: () => + `https://www.google.co.uk/?q=site:our.umbraco.com ${this.renderedMessage}&safe=off#q=site:our.umbraco.com ${ + this.renderedMessage + } ${this.properties.find((property) => property.name === 'SourceContext')?.value}&safe=off"`, + icon: 'https://www.google.com/favicon.ico', + }, + { + label: 'Search Umbraco Source', + title: 'Search within Umbraco source code on Github', + href: () => + `https://github.com/umbraco/Umbraco-CMS/search?q=${ + this.properties.find((property) => property.name === 'SourceContext')?.value + }`, + icon: 'https://github.githubassets.com/favicon.ico', + }, + { + label: 'Search Umbraco Issues', + title: 'Search Umbraco Issues on Github', + href: () => + `https://github.com/umbraco/Umbraco-CMS/issues?q=${ + this.properties.find((property) => property.name === 'SourceContext')?.value + }`, + icon: 'https://github.githubassets.com/favicon.ico', + }, + ]; + + private _propertiesWithSearchMenu: Array = ['HttpRequestNumber', 'SourceContext', 'MachineName']; + + private _findLogsWithProperty({ name, value }: LogMessagePropertyModel) { + let queryString = ''; + + if (isNaN(+(value ?? ''))) { + queryString = name + "='" + value + "'"; + } else { + queryString = name + '=' + value; + } + + this.#logViewerContext?.setFilterExpression(queryString); + this.#logViewerContext?.setCurrentPage(1); + this.details.removeAttribute('open'); + this.#logViewerContext?.getLogs(); + } + + #setOpen(event: Event) { + this.open = (event.target as HTMLDetailsElement).open; + } + + render() { + return html` +
    + +
    ${this.date?.toLocaleString()}
    +
    + +
    +
    ${this.properties.find((property) => property.name === 'MachineName')?.value}
    +
    ${this.renderedMessage}
    +
    + ${this.exception ? html`
    ${this.exception}
    ` : ''} +
      +
    • +
      Timestamp
      +
      ${this.date?.toLocaleString()}
      +
    • +
    • +
      @MessageTemplate
      +
      ${this.messageTemplate}
      +
    • + ${this.properties.map( + (property) => + html`
    • +
      ${property.name}:
      +
      + ${property.value} + ${this._propertiesWithSearchMenu.includes(property.name ?? '') + ? html` { + this._findLogsWithProperty(property); + }} + look="secondary" + label="Find logs with ${property.name}" + title="Find logs with ${property.name}" + >` + : ''} +
      +
    • ` + )} +
    + + Search +
      + ${this._searchMenuData.map( + (menuItem) => html` +
    • + + + +
    • + ` + )} +
    +
    +
    + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-log-viewer-message': UmbLogViewerMessageElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-messages-list.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-messages-list.element.ts new file mode 100644 index 0000000000..1fc81d55fa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-messages-list.element.ts @@ -0,0 +1,158 @@ +import { UUIScrollContainerElement, UUIPaginationElement } from '@umbraco-ui/uui'; +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, query, state } from 'lit/decorators.js'; +import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../../logviewer.context'; +import { UmbLitElement } from '@umbraco-cms/element'; +import { DirectionModel, LogMessageModel } from '@umbraco-cms/backend-api'; + +@customElement('umb-log-viewer-messages-list') +export class UmbLogViewerMessagesListElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + #message-list-header { + display: flex; + font-weight: 600; + } + + #message-list-header > div { + box-sizing: border-box; + padding: 10px 20px; + display: flex; + align-items: center; + } + + #timestamp { + flex: 1 0 14ch; + } + + #level, + #machine { + flex: 1 0 14ch; + } + + #message { + flex: 6 0 14ch; + } + + #empty { + display: flex; + justify-content: center; + align-items: center; + gap: var(--uui-size-space-3); + } + + #pagination { + margin: var(--uui-size-space-5, 18px) 0; + } + `, + ]; + + @query('#logs-scroll-container') + private _logsScrollContainer!: UUIScrollContainerElement; + + @state() + private _sortingDirection: DirectionModel = DirectionModel.ASCENDING; + + @state() + private _logs: LogMessageModel[] = []; + + @state() + private _logsTotal = 0; + + #logViewerContext?: UmbLogViewerWorkspaceContext; + + constructor() { + super(); + this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => { + this.#logViewerContext = instance; + this.#observeLogs(); + this.#logViewerContext.getLogs(); + }); + } + + #observeLogs() { + if (!this.#logViewerContext) return; + + this.observe(this.#logViewerContext.logs, (logs) => { + this._logs = logs ?? []; + }); + + this.observe(this.#logViewerContext.logsTotal, (total) => { + this._logsTotal = total ?? 0; + }); + + this.observe(this.#logViewerContext.sortingDirection, (direction) => { + this._sortingDirection = direction; + }); + } + + #sortLogs() { + this.#logViewerContext?.toggleSortOrder(); + this.#logViewerContext?.setCurrentPage(1); + this.#logViewerContext?.getLogs(); + } + + _onPageChange(event: Event): void { + const current = (event.target as UUIPaginationElement).current; + this.#logViewerContext?.setCurrentPage(current); + this.#logViewerContext?.getLogs(); + this._logsScrollContainer.scrollTop = 0; + } + + private _renderPagination() { + if (!this._logsTotal) return ''; + + const totalPages = Math.ceil(this._logsTotal / 100); + + if (totalPages <= 1) return ''; + + return html``; + } + + render() { + return html` +

    Total items: ${this._logsTotal}

    +
    +
    + Timestamp + + + +
    +
    Level
    +
    Machine name
    +
    Message
    +
    + + ${this._logs.length > 0 + ? html` ${this._logs.map( + (log) => html`` + )}` + : html` + Sorry, we cannot find what you are looking for. + `} + + ${this._renderPagination()} +
    `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-log-viewer-messages-list': UmbLogViewerMessagesListElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-polling-button.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-polling-button.element.ts new file mode 100644 index 0000000000..5ee674f1d4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-polling-button.element.ts @@ -0,0 +1,138 @@ +import { UUIPopoverElement, UUISymbolExpandElement } from '@umbraco-ui/uui'; +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, query, state } from 'lit/decorators.js'; +import { + PoolingCOnfig, + PoolingInterval, + UmbLogViewerWorkspaceContext, + UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, +} from '../../../logviewer.context'; +import { UmbLitElement } from '@umbraco-cms/element'; + +@customElement('umb-log-viewer-polling-button') +export class UmbLogViewerPollingButtonElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + #polling-interval-menu { + margin: 0; + padding: 0; + width: 20ch; + background-color: var(--uui-color-surface); + box-shadow: var(--uui-shadow-depth-3); + display: flex; + flex-direction: column; + transform: translateX(calc((100% - 33px) * -1)); + } + + #polling-enabled-icon { + margin-right: var(--uui-size-space-3); + margin-bottom: 1px; + -webkit-animation: rotate-center 0.8s ease-in-out infinite both; + animation: rotate-center 0.8s ease-in-out infinite both; + } + + @-webkit-keyframes rotate-center { + 0% { + -webkit-transform: rotate(0); + transform: rotate(0); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } + } + @keyframes rotate-center { + 0% { + -webkit-transform: rotate(0); + transform: rotate(0); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } + } + `, + ]; + + @query('#polling-popover') + private _pollingPopover!: UUIPopoverElement; + + @query('#polling-expand-symbol') + private _polingExpandSymbol!: UUISymbolExpandElement; + + @state() + private _poolingConfig: PoolingCOnfig = { enabled: false, interval: 0 }; + + #pollingIntervals: PoolingInterval[] = [2000, 5000, 10000, 20000, 30000]; + + #logViewerContext?: UmbLogViewerWorkspaceContext; + + constructor() { + super(); + this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => { + this.#logViewerContext = instance; + this.#observePoolingConfig(); + this.#logViewerContext.getLogs(); + }); + } + + #observePoolingConfig() { + if (!this.#logViewerContext) return; + + this.observe(this.#logViewerContext.polling, (poolingConfig) => { + this._poolingConfig = { ...poolingConfig }; + }); + } + + #togglePolling() { + this.#logViewerContext?.togglePolling(); + } + + #setPolingInterval(interval: PoolingInterval) { + this.#logViewerContext?.setPollingInterval(interval); + this.#closePoolingPopover(); + } + + #openPoolingPopover() { + this._pollingPopover.open = true; + this._polingExpandSymbol.open = true; + } + + #closePoolingPopover() { + this._pollingPopover.open = false; + this._polingExpandSymbol.open = false; + } + + render() { + return html` + ${this._poolingConfig.enabled + ? html`Polling + ${this._poolingConfig.interval / 1000} seconds` + : 'Pooling'} + (this._polingExpandSymbol.open = false)}> + + + + +
      + ${this.#pollingIntervals.map( + (interval: PoolingInterval) => + html` this.#setPolingInterval(interval)}>` + )} +
    +
    +
    `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-log-viewer-polling-button': UmbLogViewerPollingButtonElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-search-input.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-search-input.element.ts new file mode 100644 index 0000000000..c4ca1a8ffa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-search-input.element.ts @@ -0,0 +1,211 @@ +import { UUIInputElement, UUIPopoverElement, UUISymbolExpandElement } from '@umbraco-ui/uui'; +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, query, state } from 'lit/decorators.js'; +import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../../logviewer.context'; +import { SavedLogSearchModel } from '@umbraco-cms/backend-api'; +import { UmbLitElement } from '@umbraco-cms/element'; + +@customElement('umb-log-viewer-search-input') +export class UmbLogViewerSearchInputElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + :host { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--uui-size-space-4); + } + + #search-input { + width: 100%; + } + + #saved-searches-button { + flex-shrink: 0; + } + + #saved-searches-popover { + flex: 1; + } + + #saved-searches-container { + width: 100%; + max-height: 300px; + background-color: var(--uui-color-surface); + box-shadow: var(--uui-shadow-depth-1); + } + + .saved-search-item { + display: flex; + justify-content: space-between; + align-items: stretch; + border-bottom: 1px solid #e9e9eb; + } + + .saved-search-item-button { + display: flex; + font-family: inherit; + flex: 1; + background: 0 0; + padding: 0 0; + border: 0; + clear: both; + cursor: pointer; + display: flex; + font-weight: 400; + line-height: 20px; + text-align: left; + align-items: center; + white-space: nowrap; + color: var(--uui-color-interactive); + } + + .saved-search-item-button:hover { + background-color: var(--uui-color-surface-emphasis, rgb(250, 250, 250)); + color: var(--color-standalone); + } + + .saved-search-item-name { + font-weight: 600; + margin: 0 var(--uui-size-space-3); + } + + #polling-symbol-expand, + #saved-search-expand-symbol, + uui-symbol-sort { + margin-left: var(--uui-size-space-3); + } + `, + ]; + + @query('#saved-searches-popover') + private _savedSearchesPopover!: UUIPopoverElement; + + @query('#saved-search-expand-symbol') + private _savedSearchesExpandSymbol!: UUISymbolExpandElement; + + @state() + private _savedSearches: SavedLogSearchModel[] = []; + + @state() + private _inputQuery = ''; + + #logViewerContext?: UmbLogViewerWorkspaceContext; + + constructor() { + super(); + this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => { + this.#logViewerContext = instance; + this.#observeStuff(); + this.#logViewerContext.getLogs(); + }); + } + + #observeStuff() { + if (!this.#logViewerContext) return; + this.observe(this.#logViewerContext.savedSearches, (savedSearches) => { + this._savedSearches = savedSearches ?? []; + }); + + this.observe(this.#logViewerContext.filterExpression, (query) => { + this._inputQuery = query; + }); + } + + #toggleSavedSearchesPopover() { + this._savedSearchesPopover.open = !this._savedSearchesPopover.open; + } + + #toggleSavedSearchesExpandSymbol() { + this._savedSearchesExpandSymbol.open = !this._savedSearchesExpandSymbol.open; + } + + #openSavedSearchesPopover() { + this.#toggleSavedSearchesPopover(); + this.#toggleSavedSearchesExpandSymbol(); + } + + #setQuery(event: Event) { + const target = event.target as UUIInputElement; + this._inputQuery = target.value as string; + this.#logViewerContext?.setFilterExpression(this._inputQuery); + } + + #setQueryFromSavedSearch(query: string) { + this._inputQuery = query; + this.#logViewerContext?.setFilterExpression(query); + this.#logViewerContext?.setCurrentPage(1); + + this.#logViewerContext?.getLogs(); + this._savedSearchesPopover.open = false; + } + + #clearQuery() { + this._inputQuery = ''; + this.#logViewerContext?.setFilterExpression(''); + this.#logViewerContext?.getLogs(); + } + + #search() { + this.#logViewerContext?.setCurrentPage(1); + + this.#logViewerContext?.getLogs(); + } + + render() { + return html` + + ${this._inputQuery + ? html`` + : html``} + Saved searches + + + + ${this._savedSearches.map( + (search) => + html`
  • + +
  • ` + )} +
    +
    + Search`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-log-viewer-search-input': UmbLogViewerSearchInputElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/index.ts new file mode 100644 index 0000000000..4760a61f5a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/index.ts @@ -0,0 +1,4 @@ +import './components'; +import { UmbLogViewerSearchViewElement } from './log-search-view.element'; + +export default UmbLogViewerSearchViewElement; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/log-search-view.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/log-search-view.element.ts new file mode 100644 index 0000000000..c9813ae53c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/log-search-view.element.ts @@ -0,0 +1,87 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { UmbLogViewerWorkspaceContext, UMB_APP_LOG_VIEWER_CONTEXT_TOKEN } from '../../logviewer.context'; +import { UmbLitElement } from '@umbraco-cms/element'; + +@customElement('umb-log-viewer-search-view') +export class UmbLogViewerSearchViewElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + #layout { + margin: 20px; + } + #levels-container, + #input-container { + display: flex; + align-items: center; + gap: var(--uui-size-space-4); + width: 100%; + margin-bottom: 20px; + } + + #levels-container { + justify-content: space-between; + } + + #dates-polling-container { + display: flex; + align-items: baseline; + } + + umb-log-viewer-search-input { + flex: 1; + } + + umb-log-viewer-date-range-selector { + flex-direction: row; + } + `, + ]; + + @state() + private _canShowLogs = false; + + #logViewerContext?: UmbLogViewerWorkspaceContext; + constructor() { + super(); + this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT_TOKEN, (instance) => { + this.#logViewerContext = instance; + this.#observeCanShowLogs(); + }); + } + + #observeCanShowLogs() { + if (!this.#logViewerContext) return; + this.observe(this.#logViewerContext.canShowLogs, (canShowLogs) => { + this._canShowLogs = canShowLogs ?? false; + }); + } + + render() { + return html` +
    +
    + +
    + + +
    +
    +
    + +
    + ${this._canShowLogs + ? html`` + : html``} +
    + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-log-viewer-search-view': UmbLogViewerSearchViewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice-modal-container.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice-modal-container.element.ts index 1ad3d99bf3..dd257e3841 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice-modal-container.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice-modal-container.element.ts @@ -2,7 +2,7 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, CSSResultGroup, html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; -import { UmbModalHandler, UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '../../../../core/modal'; +import { UmbModalHandler, UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; import { UmbLitElement } from '@umbraco-cms/element'; @customElement('umb-backoffice-modal-container') @@ -41,7 +41,7 @@ export class UmbBackofficeModalContainer extends UmbLitElement { render() { return html` - ${this._modals ? repeat(this._modals, (modalHandler) => html`${modalHandler.element}`) : ''} + ${this._modals ? repeat(this._modals, (modalHandler) => html`${modalHandler.modalElement}`) : ''} `; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts index 0770f98a60..66406ae01c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts @@ -2,6 +2,18 @@ import { css, html, LitElement, nothing } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; +/** + * @element umb-body-layout + * @description Layout element to arrange elements in a body layout. A general layout for most views. + * @slot icon - Slot for icon + * @slot name - Slot for name + * @slot footer - Slot for workspace footer + * @slot actions - Slot for workspace footer actions + * @slot default - slot for main content + * @export + * @class UmbBodyLayout + * @extends {UmbLitElement} + */ @customElement('umb-body-layout') export class UmbBodyLayout extends LitElement { static styles = [ @@ -40,24 +52,6 @@ export class UmbBodyLayout extends LitElement { flex: 1; flex-direction: column; } - - #footer { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - height: 54px; /* TODO: missing var(--uui-size-18);*/ - border-top: 1px solid var(--uui-color-border); - background-color: var(--uui-color-surface); - box-sizing: border-box; - } - - #actions { - display: flex; - gap: var(--uui-size-space-2); - margin: 0 var(--uui-size-layout-1); - margin-left: auto; - } `, ]; @@ -77,15 +71,15 @@ export class UmbBodyLayout extends LitElement { @state() private _tabsSlotHasChildren = false; + @state() + private _actionsMenuSlotHasChildren = false; + @state() private _footerSlotHasChildren = false; @state() private _actionsSlotHasChildren = false; - @state() - private _actionsMenuSlotHasChildren = false; - #hasNodes = (e: Event) => { return (e.target as HTMLSlotElement).assignedNodes({ flatten: true }).length > 0; }; @@ -123,20 +117,19 @@ export class UmbBodyLayout extends LitElement { - + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.stories.ts index b26242bee7..087ae4c959 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.stories.ts @@ -1,19 +1,22 @@ -import './body-layout.element'; - -import { Meta, Story } from '@storybook/web-components'; +import { Meta, StoryObj } from '@storybook/web-components'; import { html } from 'lit-html'; +import './body-layout.element'; import type { UmbBodyLayout } from './body-layout.element'; -export default { - title: 'Workspaces/Shared/Workspace Layout', - component: 'umb-body-layout', - id: 'umb-body-layout', -} as Meta; +const meta: Meta = { + title: 'Components/Workspace Layout', + component: 'umb-body-layout' +}; + +export default meta; +type Story = StoryObj; -export const AAAOverview: Story = () => html` -
    Header slot
    - Main slot -
    Footer slot
    -
    `; -AAAOverview.storyName = 'Overview'; +export const Overview: Story = { + render: () => html` + +
    Header slot
    + Main slot +
    Footer slot
    +
    ` +}; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/button-with-dropdown/button-with-dropdown.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/button-with-dropdown/button-with-dropdown.element.ts new file mode 100644 index 0000000000..b9aeb923cd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/button-with-dropdown/button-with-dropdown.element.ts @@ -0,0 +1,82 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html, LitElement } from 'lit'; +import { customElement, property, query } from 'lit/decorators.js'; +import { PopoverPlacement, UUIPopoverElement, UUISymbolExpandElement } from '@umbraco-ui/uui'; +import { InterfaceColor, InterfaceLook } from '@umbraco-ui/uui-base/lib/types'; + +// TODO: maybe this should go to UI library? It's a common pattern +// TODO: consider not using this, but instead use dropdown, which is more generic shared component of backoffice. (this is at the movement only used in Log Viewer) +@customElement('umb-button-with-dropdown') +export class UmbButtonWithDropdownElement extends LitElement { + static styles = [ + UUITextStyles, + css` + uui-symbol-expand { + margin-left: var(--uui-size-space-3); + } + `, + ]; + + @property() + label = ''; + + @property() + open = false; + + @property() + look: InterfaceLook = 'default'; + + @property() + color: InterfaceColor = 'default'; + + @property() + placement: PopoverPlacement = 'bottom-start'; + + @query('#symbol-expand') + symbolExpand!: UUISymbolExpandElement; + + @query('#popover') + popover!: UUIPopoverElement; + + #openPopover() { + this.open = true; + this.popover.open = true; + this.symbolExpand.open = true; + } + + #closePopover() { + this.open = false; + this.popover.open = false; + this.symbolExpand.open = false; + } + + #togglePopover() { + this.open ? this.#closePopover() : this.#openPopover(); + } + + render() { + return html` + + + + + +
    + +
    +
    + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-button-with-dropdown': UmbButtonWithDropdownElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/button-with-dropdown/button-with-dropdown.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/button-with-dropdown/button-with-dropdown.stories.ts new file mode 100644 index 0000000000..d42a021c21 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/button-with-dropdown/button-with-dropdown.stories.ts @@ -0,0 +1,16 @@ + +import { Meta, Story } from '@storybook/web-components'; +import { html } from 'lit-html'; +import { UmbButtonWithDropdownElement } from './button-with-dropdown.element'; + +export default { + title: 'Components/Button with dropdown', + component: 'umb-button-with-dropdown', + id: 'umb-button-with-dropdown', +} as Meta; + +export const AAAOverview: Story = () => html` + Open me +
    I am a dropdown
    +
    `; +AAAOverview.storyName = 'Overview'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-block/code-block.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-block/code-block.stories.ts new file mode 100644 index 0000000000..533a46b28e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-block/code-block.stories.ts @@ -0,0 +1,26 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import { html } from 'lit-html'; +import './code-block.element'; +import type { UUICodeBlock } from './code-block.element'; + +const meta: Meta = { + title: 'Components/Code Block', + component: 'uui-code-block' +}; + +export default meta; +type Story = StoryObj; + +export const Overview: Story = { + args: { + } +}; + +export const WithCode: Story = { + decorators: [], + render: () => html` + + // Lets write some javascript + alert("Hello World"); + ` +}; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/debug.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/debug.element.ts index ae75471fe0..f790057ecf 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/debug.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/debug.element.ts @@ -1,6 +1,7 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html, nothing, TemplateResult } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; +import { UMB_CONTEXT_DEBUGGER_MODAL_TOKEN } from './modals/debug'; import { UmbContextDebugRequest } from '@umbraco-cms/context-api'; import { UmbLitElement } from '@umbraco-cms/element'; import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; @@ -102,10 +103,8 @@ export class UmbDebug extends UmbLitElement { } private _openDialog() { - this._modalContext?.openBasic({ - header: html` Debug: Contexts`, - content: this._htmlContent(), - overlaySize: 'small', + this._modalContext?.open(UMB_CONTEXT_DEBUGGER_MODAL_TOKEN, { + content: html`${this._renderContextAliases()}`, }); } @@ -125,19 +124,15 @@ export class UmbDebug extends UmbLitElement {
    -
    ${this._htmlContent()}
    +
    +
      + ${this._renderContextAliases()} +
    +
    `; } - private _htmlContent() { - return html` -
      - ${this._renderContextAliases()} -
    - `; - } - private _renderContextAliases() { const contextsTemplates: TemplateResult[] = []; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/manifests.ts new file mode 100644 index 0000000000..2b82b6cb72 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/manifests.ts @@ -0,0 +1,12 @@ +import type { ManifestModal } from '@umbraco-cms/extensions-registry'; + +const modals: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.ContextDebugger', + name: 'Context Debugger Modal', + loader: () => import('./modals/debug/debug-modal.element'), + }, +]; + +export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/modals/debug/debug-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/modals/debug/debug-modal.element.ts new file mode 100644 index 0000000000..841bc6c32b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/modals/debug/debug-modal.element.ts @@ -0,0 +1,76 @@ +import { css, html } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { UmbContextDebuggerModalData } from '.'; +import { UmbModalBaseElement } from '@umbraco-cms/modal'; + +@customElement('umb-context-debugger-modal') +export default class UmbContextDebuggerModalElement extends UmbModalBaseElement { + static styles = [ + UUITextStyles, + css` + uui-dialog-layout { + display: flex; + flex-direction: column; + height: 100%; + + padding: var(--uui-size-space-5); + box-sizing: border-box; + } + + uui-scroll-container { + overflow-y: scroll; + max-height: 100%; + min-height: 0; + flex: 1; + } + + uui-icon { + vertical-align: text-top; + color: var(--uui-color-danger); + } + + .context { + padding: 15px 0; + border-bottom: 1px solid var(--uui-color-danger-emphasis); + } + + h3 { + margin-top: 0; + margin-bottom: 0; + } + + h3 > span { + border-radius: var(--uui-size-4); + background-color: var(--uui-color-danger); + color: var(--uui-color-danger-contrast); + padding: 8px; + font-size: 12px; + } + + ul { + margin-top: 0; + } + `, + ]; + + private _handleClose() { + this.modalHandler?.reject(); + } + + render() { + return html` + + Debug: Contexts + ${this.data?.content} + Close + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-context-debugger-modal': UmbContextDebuggerModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/modals/debug/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/modals/debug/index.ts new file mode 100644 index 0000000000..2bf39440e8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/modals/debug/index.ts @@ -0,0 +1,14 @@ +import { TemplateResult } from 'lit'; +import { UmbModalToken } from '@umbraco-cms/modal'; + +export interface UmbContextDebuggerModalData { + content: TemplateResult | string; +} + +export const UMB_CONTEXT_DEBUGGER_MODAL_TOKEN = new UmbModalToken( + 'Umb.Modal.ContextDebugger', + { + type: 'sidebar', + size: 'small', + } +); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/stories/debug.mdx b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/stories/debug.mdx index 3497502ad5..511a284cb4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/stories/debug.mdx +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/stories/debug.mdx @@ -1,11 +1,8 @@ -import { Canvas, Meta, Story } from '@storybook/addon-docs'; +import { Meta } from '@storybook/blocks'; import DebugDialogImage from './umb-debug-dialog.jpg'; import DebugImage from './umb-debug.jpg'; - - - - + # Debugging diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/donut-chart/donut-chart.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/donut-chart/donut-chart.stories.ts new file mode 100644 index 0000000000..72529b5b1e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/donut-chart/donut-chart.stories.ts @@ -0,0 +1,20 @@ +import './donut-slice'; +import './donut-chart'; + +import { Meta } from '@storybook/web-components'; +import { html } from 'lit-html'; + +export default { + title: 'Components/Donut chart', + component: 'umb-donut-chart', + id: 'umb-donut-chart', + tags: ['autodocs'], +} as Meta; + +export const AAAOverview = () => html` + + + + +`; +AAAOverview.storyName = 'Overview'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/donut-chart/donut-chart.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/donut-chart/donut-chart.ts new file mode 100644 index 0000000000..916ce79310 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/donut-chart/donut-chart.ts @@ -0,0 +1,337 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html, LitElement, svg } from 'lit'; +import { customElement, property, query, queryAssignedElements, state } from 'lit/decorators.js'; +import { clamp } from 'lodash-es'; +import { UmbDonutSliceElement } from './donut-slice'; + +export interface Circle { + color: string; + name: string; + percent: number; + kind: string; +} + +interface CircleWithCommands extends Circle { + offset: number; + commands: string; +} +//TODO: maybe move to UI Library +/** + * This is a donut chart component that can be used to display data in a circular way. + * + * @export + * @class UmbDonutChartElement + * @extends {LitElement} + */ +@customElement('umb-donut-chart') +export class UmbDonutChartElement extends LitElement { + static percentToDegrees(percent: number): number { + return percent * 3.6; + } + + static styles = [ + UUITextStyles, + css` + path { + pointer-events: visibleFill; + } + .circle { + filter: url(#erode); + } + + .highlight { + transition: opacity 200ms linear; + filter: url(#filter); + opacity: 0; + } + + .highlight:hover { + opacity: 0.5; + } + + #container { + position: relative; + width: 200px; + } + + #details-box { + background: #ffffffe6; + border: 1px solid var(--uui-color-border-standalone); + border-radius: var(--uui-border-radius); + box-sizing: border-box; + top: 0; + left: 0; + position: absolute; + opacity: 0; + padding: 0.5em; + line-height: 1.5; + font-size: var(--uui-type-small-size); + box-shadow: var(--uui-shadow-depth-1); + transform: translate3d(var(--pos-x), var(--pos-y), 0); + transition: transform 0.2s cubic-bezier(0.02, 1.23, 0.79, 1.08); + transition: opacity 150ms linear; + } + + #details-box.show { + opacity: 1; + } + + #details-box uui-icon { + /* optically correct alignment */ + color: var(--umb-donut-detail-color); + margin-right: 0.2em; + } + + #details-title { + font-weight: bold; + display: flex; + align-items: center; + } + `, + ]; + + /** + * Circle radius in pixels + * + * @memberof UmbDonutChartElement + */ + @property({ type: Number }) + radius = 45; + + /** + * The circle thickness in pixels + * + * @memberof UmbDonutChartElement + */ + @property({ type: Number, attribute: 'border-size' }) + borderSize = 20; + + /** + * The size of SVG element in pixels + * + * @memberof UmbDonutChartElement + */ + @property({ type: Number, attribute: 'svg-size' }) + svgSize = 100; + + /** + * Description of the graph, added for accessibility purposes + * + * @memberof UmbDonutChartElement + */ + @property() + description = ''; + + /** + * Hides the box that appears oh hover with the details of the slice + * + * @memberof UmbDonutChartElement + */ + @property({ type: Boolean }) + hideDetailBox = false; + + @queryAssignedElements({ selector: 'umb-donut-slice' }) + private _slices!: UmbDonutSliceElement[]; + + @query('#container') + private _container!: HTMLDivElement; + + @query('#details-box') + private _detailsBox!: HTMLDivElement; + + @state() + private circles: CircleWithCommands[] = []; + + @state() + private viewBox = 100; + + @state() + private _posY = 0; + + @state() + private _posX = 0; + + @state() + private _detailName = ''; + + @state() + private _detailAmount = 0; + + @state() + private _detailColor = 'black'; + + @state() + private _totalAmount = 0; + + @state() + private _detailKind = ''; + + #containerBounds: DOMRect | undefined; + + firstUpdated() { + this.#containerBounds = this._container.getBoundingClientRect(); + } + + protected willUpdate(_changedProperties: Map): void { + if (_changedProperties.has('radius') || _changedProperties.has('borderSize') || _changedProperties.has('svgSize')) { + if (this.borderSize > this.radius) { + throw new Error('Border size cannot be bigger than radius'); + } + + this.#printCircles(); + } + + + } + + #calculatePercentage(partialValue: number) { + if (this._totalAmount === 0) return 0; + const percent = Math.round((100 * partialValue) / this._totalAmount); + return clamp(percent, 0, 99); + } + + #printCircles(event: Event | null = null) { + this._totalAmount = this._slices.reduce((acc, slice) => acc + slice.amount, 0); + event?.stopPropagation(); + this.circles = this.#addCommands( + this._slices.map((slice) => { + return { + percent: this.#calculatePercentage(slice.amount), + color: slice.color, + name: slice.name, + kind: slice.kind, + }; + }) + ); + } + + #addCommands(Circles: Circle[]): CircleWithCommands[] { + let previousPercent = 0; + return Circles.map((slice) => { + const sliceWithCommands: CircleWithCommands = { + ...slice, + commands: this.#getSliceCommands(slice, this.radius, this.svgSize, this.borderSize), + offset: previousPercent * 3.6 * -1, + }; + previousPercent += slice.percent; + return sliceWithCommands; + }); + } + + #getSliceCommands(Circle: Circle, radius: number, svgSize: number, borderSize: number): string { + const degrees = UmbDonutChartElement.percentToDegrees(Circle.percent); + const longPathFlag = degrees > 180 ? 1 : 0; + const innerRadius = radius - borderSize; + + const commands: string[] = []; + commands.push(`M ${svgSize / 2 + radius} ${svgSize / 2}`); + commands.push(`A ${radius} ${radius} 0 ${longPathFlag} 0 ${this.#getCoordFromDegrees(degrees, radius, svgSize)}`); + commands.push(`L ${this.#getCoordFromDegrees(degrees, innerRadius, svgSize)}`); + commands.push(`A ${innerRadius} ${innerRadius} 0 ${longPathFlag} 1 ${svgSize / 2 + innerRadius} ${svgSize / 2}`); + return commands.join(' '); + } + + #getCoordFromDegrees(angle: number, radius: number, svgSize: number): string { + const x = Math.cos((angle * Math.PI) / 180); + const y = Math.sin((angle * Math.PI) / 180); + const coordX = x * radius + svgSize / 2; + const coordY = y * -radius + svgSize / 2; + return [coordX, coordY].join(' '); + } + + #calculateDetailsBoxPosition = (event: MouseEvent) => { + const x = this.#containerBounds ? event.clientX - this.#containerBounds?.left : 0; + const y = this.#containerBounds ? event.clientY - this.#containerBounds?.top : 0; + this._posX = x - 10; + this._posY = y - 70; + }; + + #setDetailsBoxData(event: MouseEvent) { + const target = event.target as SVGPathElement; + const index = target.dataset.index as unknown as number; + const circle = this.circles[index]; + this._detailName = circle.name; + this._detailAmount = circle.percent; + this._detailColor = circle.color; + this._detailKind = circle.kind; + } + + #showDetailsBox(event: MouseEvent) { + if (this.hideDetailBox) return; + this.#setDetailsBoxData(event); + this._detailsBox.classList.add('show'); + } + + #hideDetailsBox() { + if (this.hideDetailBox) return; + this._detailsBox.classList.remove('show'); + } + + #renderCircles() { + return svg` + + + + + + + + + + + + + + + + ${this.description} + ${this.circles.map( + (circle, i) => svg` + + + + ` + )} + + `; + } + + render() { + return html`
    + ${this.#renderCircles()} +
    +
    ${this._detailName}
    + ${this._detailAmount} ${this._detailKind} +
    +
    + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-donut-chart': UmbDonutChartElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/donut-chart/donut-slice.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/donut-chart/donut-slice.ts new file mode 100644 index 0000000000..b485803986 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/donut-chart/donut-slice.ts @@ -0,0 +1,51 @@ +import { LitElement } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +/** + * This component is used to display a single slice of a donut chart. It only makes sense insice the donut chart + * + * @export + * @class UmbDonutSliceElement + * @fires slice-update - This event is fired when the slice is updated + * @extends {LitElement} + */ +@customElement('umb-donut-slice') +export class UmbDonutSliceElement extends LitElement { + /** + * Number of items that this slice represents + * + * @memberof UmbDonutSliceElement + */ + @property({ type: Number }) + amount = 0; + /** + * Color of the slice. Any valid css color is accepted, custom properties are also supported + * + * @memberof UmbDonutSliceElement + */ + @property() + color = 'red'; + /** + * Name of the slice. This is used to display the name of the slice in the donut chart + * + * @memberof UmbDonutSliceElement + */ + @property() + name = ''; + /** + * Kind of the slice. This is shown on a details box when hovering over the slice + * + * @memberof UmbDonutSliceElement + */ + @property() + kind = ''; + + willUpdate() { + this.dispatchEvent(new CustomEvent('slice-update', { composed: true, bubbles: true })); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-donut-slice': UmbDonutSliceElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/donut-chart/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/donut-chart/index.ts new file mode 100644 index 0000000000..218f52b18f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/donut-chart/index.ts @@ -0,0 +1,2 @@ +export * from './donut-chart'; +export * from './donut-slice'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/dropdown/dropdown.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/dropdown/dropdown.element.ts index ab21ee4f5e..20a72a57d9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/dropdown/dropdown.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/dropdown/dropdown.element.ts @@ -3,6 +3,7 @@ import { css, html, nothing } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { UmbLitElement } from '@umbraco-cms/element'; +// TODO: maybe move this to UI Library. @customElement('umb-dropdown') export class UmbDropdownElement extends UmbLitElement { static styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/empty-state/empty-state.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/empty-state/empty-state.stories.ts new file mode 100644 index 0000000000..5784914c4e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/empty-state/empty-state.stories.ts @@ -0,0 +1,26 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import { html } from 'lit-html'; +import './empty-state.element'; +import type { UmbEmptyStateElement } from './empty-state.element'; + +const meta: Meta = { + title: 'Components/Empty State', + component: 'umb-empty-state', + render: (args) => html` + There are no items to be found`, +}; + +export default meta; +type Story = StoryObj; + +export const Overview: Story = { + args: { + size: 'large', + } +}; + +export const Small: Story = { + args: { + size: 'small', + } +}; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/footer-layout/footer-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/footer-layout/footer-layout.element.ts new file mode 100644 index 0000000000..565142fdeb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/footer-layout/footer-layout.element.ts @@ -0,0 +1,51 @@ +import { css, html, LitElement, PropertyValueMap } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, state } from 'lit/decorators.js'; + +/** + * @element umb-footer-layout + * @description + * @slot default - Slot footer items + * @slot actions - Slot actions + * @export + * @class UmbFooterLayout + * @extends {UmbLitElement} + */ +@customElement('umb-footer-layout') +export class UmbFooterLayout extends LitElement { + static styles = [ + UUITextStyles, + css` + :host { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + height: 54px; /* TODO: missing var(--uui-size-18);*/ + border-top: 1px solid var(--uui-color-border); + background-color: var(--uui-color-surface); + box-sizing: border-box; + } + + #actions { + display: flex; + gap: var(--uui-size-space-2); + margin: 0 var(--uui-size-layout-1); + margin-left: auto; + } + `, + ]; + + render() { + return html` + + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-footer-layout': UmbFooterLayout; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/footer-layout/footer-layout.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/footer-layout/footer-layout.stories.ts new file mode 100644 index 0000000000..dc5fce7e70 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/footer-layout/footer-layout.stories.ts @@ -0,0 +1,20 @@ +import './footer-layout.element'; + +import { Meta, Story } from '@storybook/web-components'; +import { html } from 'lit-html'; + +import type { UmbFooterLayout } from './footer-layout.element'; + +export default { + title: 'Workspaces/Shared/Footer Layout', + component: 'umb-footer-layout', + id: 'umb-footer-layout', +} as Meta; + +export const AAAOverview: Story = () => html` +
    + Footer slotActions slot +
    +
    `; +AAAOverview.storyName = 'Overview'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/header-app/header-app.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/header-app/header-app.stories.ts new file mode 100644 index 0000000000..581f8416eb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/header-app/header-app.stories.ts @@ -0,0 +1,26 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './header-app-button.element'; +import type { UmbHeaderAppButton } from './header-app-button.element'; + +const meta: Meta = { + title: 'Components/Header App Button', + component: 'umb-header-app-button' +}; + +export default meta; +type Story = StoryObj; + +export const Overview: Story = { + args: { + manifest: { + name: 'Some Manifest', + alias: 'someManifestAlias', + type: 'headerApp', + meta: { + label: 'Some Header', + icon: 'umb:home', + pathname: '/some/path' + } + } + } +}; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/history/history-item.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/history/history-item.element.ts new file mode 100644 index 0000000000..1db73bb340 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/history/history-item.element.ts @@ -0,0 +1,97 @@ +import { css, html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property } from 'lit/decorators.js'; +import { UmbLitElement } from '@umbraco-cms/element'; + +@customElement('umb-history-item') +export class UmbHistoryItemElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + :host { + --avatar-size: calc(2em + 4px); + display: block; + } + + #wrapper { + display: flex; + width: 100%; + gap: calc(2 * var(--uui-size-space-5)); + align-items: center; + } + .slots-wrapper { + display: flex; + justify-content: space-between; + align-items: center; + flex: 1; + } + + slot[name='actions'] { + --uui-button-border-radius: 50px 50px 50px 50px; + display: flex; + align-items: center; + --uui-button-height: calc(var(--uui-size-2) * 4); + margin-right: var(--uui-size-2); + } + #actions-container { + opacity: 0; + transition: opacity 120ms; + } + :host(:hover) #actions-container { + opacity: 1; + } + + :host(:hover) #actions-container { + opacity: 1; + } + + .user-info { + display: flex; + align-items: flex-end; + gap: var(--uui-size-space-5); + } + .user-info div { + display: flex; + flex-direction: column; + } + .detail { + font-size: var(--uui-size-4); + color: var(--uui-color-text-alt); + line-height: 1; + } + `, + ]; + + @property({ type: String }) + src?: string; + + @property({ type: String }) + name?: string; + + @property({ type: String }) + detail?: string; + + render() { + return html`
    + +
    + + +
    +
    `; + } +} + +export default UmbHistoryItemElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-history-item': UmbHistoryItemElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/history/history-list.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/history/history-list.element.ts new file mode 100644 index 0000000000..c89a0f39d5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/history/history-list.element.ts @@ -0,0 +1,49 @@ +import { css, html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement } from 'lit/decorators.js'; +import { UmbLitElement } from '@umbraco-cms/element'; + +@customElement('umb-history-list') +export class UmbHistoryListElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + :host { + display: block; + --avatar-size: calc(2em + 4px); + } + + ::slotted(*) { + position: relative; + } + + ::slotted(*:not(:last-child)) { + margin-bottom: calc(2 * var(--uui-size-space-3)); + } + + ::slotted(*:not(:last-child))::before { + content: ''; + border: 1px solid var(--uui-color-border); + position: absolute; + display: block; + height: calc(1.5 * var(--avatar-size)); + top: var(--avatar-size); + left: calc(-1px + var(--avatar-size) / 2); + } + `, + ]; + + render() { + return html`
    + +
    `; + } +} + +export default UmbHistoryListElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-history-list': UmbHistoryListElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/history/history-list.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/history/history-list.stories.ts new file mode 100644 index 0000000000..fdc13cc6e0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/history/history-list.stories.ts @@ -0,0 +1,37 @@ +import './history-list.element'; +import './history-item.element'; + +import { Meta, Story } from '@storybook/web-components'; +import { html } from 'lit-html'; + +import type { UmbHistoryListElement } from './history-list.element'; +import type { UmbHistoryItemElement } from './history-item.element'; + +export default { + title: 'Components/History UI', + component: 'umb-history-list', + id: 'umb-history-list', +} as Meta; + +export const AAAOverview: Story = () => html` + + Default slot + Action slot + + + Default slot + Action slot + + + Default slot + Action slot + +`; +AAAOverview.storyName = 'Overview'; + +export const Node: Story = () => html` + Default slot + Action slot +`; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts index bdc95016b6..04c03284f7 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts @@ -1,14 +1,19 @@ -//TODO: we need to figure out what components should be available for extensions and load them upfront -// TODO: we need to move these files into their respective folders/silos. We then need a way for a silo to globally register a component +import { manifests as debugManifests } from './debug/manifests'; +// TODO: we need to figure out what components should be available for extensions and load them upfront +// TODO: we need to move these files into their respective folders/silos. We then need a way for a silo to globally register a component +import './body-layout/body-layout.element'; +import './footer-layout/footer-layout.element'; import './entity-action/entity-action-list.element'; import './entity-action/entity-action.element'; import './backoffice-frame/backoffice-header.element'; import './backoffice-frame/backoffice-main.element'; import './backoffice-frame/backoffice-modal-container.element'; import './backoffice-frame/backoffice-notification-container.element'; +import './button-with-dropdown/button-with-dropdown.element'; import './code-block/code-block.element'; import './debug/debug.element'; +import './donut-chart'; import './dropdown/dropdown.element'; import './empty-state/empty-state.element'; import './extension-slot/extension-slot.element'; @@ -21,7 +26,7 @@ import './input-language-picker/input-language-picker.element'; import './input-media-picker/input-media-picker.element'; import './input-multi-url-picker/input-multi-url-picker.element'; import './input-slider/input-slider.element'; -import './input-toggle/input-toggle-element'; +import './input-toggle/input-toggle.element'; import './property-type-based-property/property-type-based-property.element'; import './ref-property-editor-ui/ref-property-editor-ui.element'; import './section/section-main/section-main.element'; @@ -31,6 +36,14 @@ import './table/table.element'; import './tree/tree.element'; import './variantable-property/variantable-property.element'; import './workspace/workspace-action-menu/workspace-action-menu.element'; + +import './history/history-list.element'; +import './history/history-item.element'; + import './workspace/workspace-action/workspace-action.element'; import './workspace/workspace-content/workspace-content.element'; import './workspace/workspace-layout/workspace-layout.element'; + +import './workspace/workspace-footer-layout/workspace-footer-layout.element'; + +export const manifests = [...debugManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-checkbox-list/input-checkbox-list.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-checkbox-list/input-checkbox-list.stories.ts new file mode 100644 index 0000000000..d3ed1de046 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-checkbox-list/input-checkbox-list.stories.ts @@ -0,0 +1,28 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './input-checkbox-list.element'; +import type { UmbInputCheckboxListElement } from './input-checkbox-list.element'; + +const meta: Meta = { + title: 'Components/Inputs/Checkbox List', + component: 'umb-input-checkbox-list' +}; + +export default meta; +type Story = StoryObj; + +export const Overview: Story = { + args: { + list: [ + { + key: "isAwesome", + value: "Umbraco is awesome?", + checked: true + }, + { + key: "attendingCodeGarden", + value: "Attending CodeGarden?", + checked: false + }, + ] + } +}; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-color-picker/input-color-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-color-picker/input-color-picker.element.ts index cc2c423039..3764f7ac75 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-color-picker/input-color-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-color-picker/input-color-picker.element.ts @@ -6,6 +6,10 @@ import { UUIColorSwatchesEvent } from '@umbraco-ui/uui'; import { UmbLitElement } from '@umbraco-cms/element'; import type { SwatchDetails } from '@umbraco-cms/models'; +/* + * This wraps the UUI library uui-color-swatches component + * @element umb-input-color-picker +*/ @customElement('umb-input-color-picker') export class UmbInputColorPickerElement extends FormControlMixin(UmbLitElement) { static styles = [UUITextStyles]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-color-picker/input-color-picker.mdx b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-color-picker/input-color-picker.mdx new file mode 100644 index 0000000000..01bf535439 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-color-picker/input-color-picker.mdx @@ -0,0 +1,22 @@ +import { Meta, Title, Primary, Controls, Story, Source } from '@storybook/blocks'; +import * as ColorPickerStories from './input-color-picker.stories'; + + + + + +This color picker allows you to select a color from a palette. + +This shows how we can override the autogen docs from StoryBook to document a component how we want to exactly using [MDX doc blocks](https://storybook.js.org/docs/7.0/web-components/writing-docs/doc-blocks) + +## Stories +### Primary +<Story of={ColorPickerStories.Overview} /> +<Source of={ColorPickerStories.Overview} /> + +## Properties +<Controls of={ColorPickerStories.Overview} /> + +### Without Labels +<Story of={ColorPickerStories.WithoutLabels} /> +<Source of={ColorPickerStories.WithoutLabels} dark /> \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-color-picker/input-color-picker.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-color-picker/input-color-picker.stories.ts new file mode 100644 index 0000000000..daf4592007 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-color-picker/input-color-picker.stories.ts @@ -0,0 +1,62 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './input-color-picker.element'; +import type { UmbInputColorPickerElement } from './input-color-picker.element'; + +const meta: Meta<UmbInputColorPickerElement> = { + title: 'Components/Inputs/Color Picker', + component: 'umb-input-color-picker' +}; + +export default meta; +type Story = StoryObj<UmbInputColorPickerElement>; + +export const Overview: Story = { + args: { + showLabels: true, + swatches: [ + { + label: "Red", + value: "#ff0000" + }, + { + label: "Green", + value: "#00ff00" + } + ] + } +}; + +export const WithoutLabels: Story = { + args: { + showLabels: false, + swatches: [ + { + label: "Red", + value: "#ff0000" + }, + { + label: "Green", + value: "#00ff00" + } + ] + } +}; + +// TODO: This doesn't check the correct swatch when the value is set +// Perhaps a BUG ? +export const WithValueLabels: Story = { + args: { + showLabels: true, + swatches: [ + { + label: "Red", + value: "#ff0000" + }, + { + label: "Green", + value: "#00ff00" + } + ], + value: "#00ff00" + } +}; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-culture-select/input-culture-select.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-culture-select/input-culture-select.stories.ts new file mode 100644 index 0000000000..59084ad6fc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-culture-select/input-culture-select.stories.ts @@ -0,0 +1,56 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './input-culture-select.element'; +import type { UmbInputCultureSelectElement } from './input-culture-select.element'; + +const meta: Meta<UmbInputCultureSelectElement> = { + title: 'Components/Inputs/Culture Select', + component: 'umb-input-culture-select' +}; + +export default meta; +type Story = StoryObj<UmbInputCultureSelectElement>; + +export const Overview: Story = { + args: { + readonly: false, + disabled: false + } +}; + +export const ReadOnly: Story = { + args: { + readonly: true, + disabled: false + } +}; + +export const Disabled: Story = { + args: { + readonly: false, + disabled: true + } +}; + +export const WithValue: Story = { + args: { + readonly: false, + disabled: false, + value: 'da-DK' + } +}; + +export const WithValueAndDisabled: Story = { + args: { + readonly: false, + disabled: true, + value: 'en-US' + } +}; + +export const WithValueAndReadOnly: Story = { + args: { + readonly: true, + disabled: false, + value: 'en-GB' + } +}; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-document-picker/input-document-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-document-picker/input-document-picker.element.ts index 177d949e5b..b7a16996bc 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-document-picker/input-document-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-document-picker/input-document-picker.element.ts @@ -3,9 +3,11 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; import { ifDefined } from 'lit-html/directives/if-defined.js'; import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; -import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '../../../../core/modal'; import { UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from '../../../documents/documents/repository/document.tree.store'; import type { UmbDocumentTreeStore } from '../../../documents/documents/repository/document.tree.store'; +import { UMB_CONFIRM_MODAL_TOKEN } from '../../modals/confirm'; +import { UMB_DOCUMENT_PICKER_MODAL_TOKEN } from '../../../documents/documents/modals/document-picker'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; import { UmbLitElement } from '@umbraco-cms/element'; import type { DocumentTreeItemModel, FolderTreeItemModel } from '@umbraco-cms/backend-api'; import type { UmbObserverController } from '@umbraco-cms/observable-api'; @@ -121,29 +123,27 @@ export class UmbInputDocumentPickerElement extends FormControlMixin(UmbLitElemen private _openPicker() { // We send a shallow copy(good enough as its just an array of keys) of our this._selectedKeys, as we don't want the modal to manipulate our data: - const modalHandler = this._modalContext?.contentPicker({ + const modalHandler = this._modalContext?.open(UMB_DOCUMENT_PICKER_MODAL_TOKEN, { multiple: this.max === 1 ? false : true, selection: [...this._selectedKeys], }); - modalHandler?.onClose().then(({ selection }: any) => { + + modalHandler?.onSubmit().then(({ selection }: any) => { this._setSelection(selection); }); } - private _removeItem(item: FolderTreeItemModel) { - const modalHandler = this._modalContext?.confirm({ + private async _removeItem(item: FolderTreeItemModel) { + const modalHandler = this._modalContext?.open(UMB_CONFIRM_MODAL_TOKEN, { color: 'danger', headline: `Remove ${item.name}?`, content: 'Are you sure you want to remove this item', confirmLabel: 'Remove', }); - modalHandler?.onClose().then(({ confirmed }) => { - if (confirmed) { - const newSelection = this._selectedKeys.filter((value) => value !== item.key); - this._setSelection(newSelection); - } - }); + await modalHandler?.onSubmit(); + const newSelection = this._selectedKeys.filter((value) => value !== item.key); + this._setSelection(newSelection); } private _setSelection(newSelection: Array<string>) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-document-picker/input-document-picker.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-document-picker/input-document-picker.stories.ts new file mode 100644 index 0000000000..14ab53fa24 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-document-picker/input-document-picker.stories.ts @@ -0,0 +1,16 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './input-document-picker.element'; +import type { UmbInputDocumentPickerElement } from './input-document-picker.element'; + +const meta: Meta<UmbInputDocumentPickerElement> = { + title: 'Components/Inputs/Document Picker', + component: 'umb-input-document-picker' +}; + +export default meta; +type Story = StoryObj<UmbInputDocumentPickerElement>; + +export const Overview: Story = { + args: { + } +}; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-eye-dropper/input-eye-dropper.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-eye-dropper/input-eye-dropper.stories.ts new file mode 100644 index 0000000000..ab9d45aaff --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-eye-dropper/input-eye-dropper.stories.ts @@ -0,0 +1,29 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './input-eye-dropper.element'; +import type { UmbInputEyeDropperElement } from './input-eye-dropper.element'; + +const meta: Meta<UmbInputEyeDropperElement> = { + title: 'Components/Inputs/Eye Dropper', + component: 'umb-input-eye-dropper' +}; + +export default meta; +type Story = StoryObj<UmbInputEyeDropperElement>; + +export const Overview: Story = { + args: { + + } +}; + +export const WithOpacity: Story = { + args: { + opacity: true, + } +}; + +export const WithSwatches: Story = { + args: { + swatches: ['#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff'] + } +}; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.element.ts index f35f3d9e77..4b4bcc091d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.element.ts @@ -3,12 +3,14 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; import { ifDefined } from 'lit-html/directives/if-defined.js'; import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; -import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '../../../../core/modal'; import { UmbLanguageRepository } from '../../../settings/languages/repository/language.repository'; +import { UMB_CONFIRM_MODAL_TOKEN } from '../../modals/confirm'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; import { UmbChangeEvent } from '@umbraco-cms/events'; import { UmbLitElement } from '@umbraco-cms/element'; import type { LanguageModel } from '@umbraco-cms/backend-api'; import type { UmbObserverController } from '@umbraco-cms/observable-api'; +import { UMB_LANGUAGE_PICKER_MODAL_TOKEN } from 'src/backoffice/settings/languages/modals/language-picker'; @customElement('umb-input-language-picker') export class UmbInputLanguagePickerElement extends FormControlMixin(UmbLitElement) { @@ -119,30 +121,28 @@ export class UmbInputLanguagePickerElement extends FormControlMixin(UmbLitElemen } private _openPicker() { - const modalHandler = this._modalContext?.languagePicker({ + const modalHandler = this._modalContext?.open(UMB_LANGUAGE_PICKER_MODAL_TOKEN, { multiple: this.max === 1 ? false : true, selection: [...this._selectedIsoCodes], filter: this.filter, }); - modalHandler?.onClose().then(({ selection }: any) => { + modalHandler?.onSubmit().then(({ selection }) => { this._setSelection(selection); }); } private _removeItem(item: LanguageModel) { - const modalHandler = this._modalContext?.confirm({ + const modalHandler = this._modalContext?.open(UMB_CONFIRM_MODAL_TOKEN, { color: 'danger', headline: `Remove ${item.name}?`, content: 'Are you sure you want to remove this item', confirmLabel: 'Remove', }); - modalHandler?.onClose().then(({ confirmed }) => { - if (confirmed) { - const newSelection = this._selectedIsoCodes.filter((value) => value !== item.isoCode); - this._setSelection(newSelection); - } + modalHandler?.onSubmit().then(() => { + const newSelection = this._selectedIsoCodes.filter((value) => value !== item.isoCode); + this._setSelection(newSelection); }); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.stories.ts new file mode 100644 index 0000000000..623c58db4a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.stories.ts @@ -0,0 +1,17 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './input-language-picker.element'; +import type { UmbInputLanguagePickerElement } from './input-language-picker.element'; + +const meta: Meta<UmbInputLanguagePickerElement> = { + title: 'Components/Inputs/Language Picker', + component: 'umb-input-language-picker' +}; + +export default meta; +type Story = StoryObj<UmbInputLanguagePickerElement>; + +export const Overview: Story = { + args: { + + } +}; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-list-base/input-list-base.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-list-base/input-list-base.ts index 0508848787..a1d954be41 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-list-base/input-list-base.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-list-base/input-list-base.ts @@ -1,13 +1,13 @@ import { html } from 'lit'; import { property } from 'lit/decorators.js'; import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar'; -import { UmbPickerModalData } from '../../../../core/modal/layouts/modal-layout-picker-base'; -import { UmbModalContext, UmbModalType, UMB_MODAL_CONTEXT_TOKEN } from '../../../../core/modal'; - -//TODO: These should probably be imported dynamically. -import '../../../../core/modal/layouts/picker-section/picker-layout-section.element'; -import '../../../../core/modal/layouts/picker-user-group/picker-layout-user-group.element'; -import '../../../../core/modal/layouts/picker-user/picker-layout-user.element'; +import { + UmbModalContext, + UmbModalToken, + UmbModalType, + UMB_MODAL_CONTEXT_TOKEN, + UmbPickerModalData, +} from '@umbraco-cms/modal'; import { UmbLitElement } from '@umbraco-cms/element'; /** TODO: Make use of UUI FORM Mixin, to make it easily take part of a form. */ @@ -24,7 +24,8 @@ export class UmbInputListBase extends UmbLitElement { @property({ type: String }) public modalSize: UUIModalSidebarSize = 'small'; - protected pickerLayout?: string; + // TODO: not great that we use any, any here. Investigate if we can have some interface or base modal token for this type. + protected pickerToken?: UmbModalToken<any, any>; private _modalContext?: UmbModalContext; constructor() { @@ -35,17 +36,14 @@ export class UmbInputListBase extends UmbLitElement { } private _openPicker() { - if (!this.pickerLayout) return; + if (!this.pickerToken) return; - const modalHandler = this._modalContext?.open(this.pickerLayout, { - type: this.modalType, - size: this.modalSize, - data: { - multiple: this.multiple, - selection: this.value, - }, + const modalHandler = this._modalContext?.open(this.pickerToken, { + multiple: this.multiple, + selection: this.value, }); - modalHandler?.onClose().then((data: UmbPickerModalData<string>) => { + + modalHandler?.onSubmit().then((data: UmbPickerModalData<string>) => { if (data) { this.value = data.selection; this.selectionUpdated(); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-media-picker/input-media-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-media-picker/input-media-picker.element.ts index b2ea70155f..8778092f1c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-media-picker/input-media-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-media-picker/input-media-picker.element.ts @@ -3,8 +3,10 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; import { ifDefined } from 'lit-html/directives/if-defined.js'; import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; -import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '../../../../core/modal'; import { UmbMediaRepository } from '../../../media/media/repository/media.repository'; +import { UMB_CONFIRM_MODAL_TOKEN } from '../../modals/confirm'; +import { UMB_MEDIA_PICKER_MODAL_TOKEN } from '../../../media/media/modals/media-picker'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; import { UmbLitElement } from '@umbraco-cms/element'; import type { EntityTreeItemModel, FolderTreeItemModel } from '@umbraco-cms/backend-api'; import type { UmbObserverController } from '@umbraco-cms/observable-api'; @@ -135,28 +137,27 @@ export class UmbInputMediaPickerElement extends FormControlMixin(UmbLitElement) private _openPicker() { // We send a shallow copy(good enough as its just an array of keys) of our this._selectedKeys, as we don't want the modal to manipulate our data: - const modalHandler = this._modalContext?.mediaPicker({ + const modalHandler = this._modalContext?.open(UMB_MEDIA_PICKER_MODAL_TOKEN, { multiple: this.max === 1 ? false : true, selection: [...this._selectedKeys], }); - modalHandler?.onClose().then(({ selection }: any) => { + + modalHandler?.onSubmit().then(({ selection }: any) => { this._setSelection(selection); }); } private _removeItem(item: FolderTreeItemModel) { - const modalHandler = this._modalContext?.confirm({ + const modalHandler = this._modalContext?.open(UMB_CONFIRM_MODAL_TOKEN, { color: 'danger', headline: `Remove ${item.name}?`, content: 'Are you sure you want to remove this item', confirmLabel: 'Remove', }); - modalHandler?.onClose().then(({ confirmed }) => { - if (confirmed) { - const newSelection = this._selectedKeys.filter((value) => value !== item.key); - this._setSelection(newSelection); - } + modalHandler?.onSubmit().then(() => { + const newSelection = this._selectedKeys.filter((value) => value !== item.key); + this._setSelection(newSelection); }); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-media-picker/input-media-picker.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-media-picker/input-media-picker.stories.ts new file mode 100644 index 0000000000..c760ca1f09 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-media-picker/input-media-picker.stories.ts @@ -0,0 +1,17 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './input-media-picker.element'; +import type { UmbInputMediaPickerElement } from './input-media-picker.element'; + +const meta: Meta<UmbInputMediaPickerElement> = { + title: 'Components/Inputs/Media Picker', + component: 'umb-input-media-picker' +}; + +export default meta; +type Story = StoryObj<UmbInputMediaPickerElement>; + +export const Overview: Story = { + args: { + + } +}; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-multi-url-picker/input-multi-url-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-multi-url-picker/input-multi-url-picker.element.ts index f627f407e7..be778585ad 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-multi-url-picker/input-multi-url-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-multi-url-picker/input-multi-url-picker.element.ts @@ -3,20 +3,10 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property } from 'lit/decorators.js'; import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar'; +import { UmbLinkPickerLink, UMB_LINK_PICKER_MODAL_TOKEN } from '../../modals/link-picker'; import { UmbLitElement } from '@umbraco-cms/element'; import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; -export interface MultiUrlData { - icon?: string; - name?: string; - published?: boolean; - queryString?: string; - target?: string; - trashed?: boolean; - udi?: string; - url?: string; -} - /** * @element umb-input-multi-url-picker * @fires change - when the value of the input changes @@ -91,11 +81,11 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen overlaySize?: UUIModalSidebarSize; /** - * @type {Array<MultiUrlData>} + * @type {Array<UmbLinkPickerLink>} * @default [] */ @property({ attribute: false }) - set urls(data: Array<MultiUrlData>) { + set urls(data: Array<UmbLinkPickerLink>) { if (!data) return; this._urls = data; super.value = this._urls.map((x) => x.url).join(','); @@ -105,7 +95,7 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen return this._urls; } - private _urls: Array<MultiUrlData> = []; + private _urls: Array<UmbLinkPickerLink> = []; private _modalContext?: UmbModalContext; constructor() { @@ -131,7 +121,7 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen this._dispatchChangeEvent(); } - private _setSelection(selection: MultiUrlData, index?: number) { + private _setSelection(selection: UmbLinkPickerLink, index?: number) { if (index !== undefined && index >= 0) this.urls[index] = selection; else this.urls.push(selection); @@ -143,8 +133,8 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen this.dispatchEvent(new CustomEvent('change', { composed: true, bubbles: true })); } - private _openPicker(data?: MultiUrlData, index?: number) { - const modalHandler = this._modalContext?.linkPicker({ + private _openPicker(data?: UmbLinkPickerLink, index?: number) { + const modalHandler = this._modalContext?.open(UMB_LINK_PICKER_MODAL_TOKEN, { link: { name: data?.name, published: data?.published, @@ -160,8 +150,9 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen overlaySize: this.overlaySize || 'small', }, }); - modalHandler?.onClose().then((newUrl: MultiUrlData) => { + modalHandler?.onSubmit().then((newUrl: UmbLinkPickerLink) => { if (!newUrl) return; + this._setSelection(newUrl, index); }); } @@ -171,7 +162,7 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen <uui-button look="placeholder" label="Add" @click=${this._openPicker}>Add</uui-button>`; } - private _renderItem(link: MultiUrlData, index: number) { + private _renderItem(link: UmbLinkPickerLink, index: number) { return html`<uui-ref-node .name="${link.name || ''}" .detail="${(link.url || '') + (link.queryString || '')}" diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-multi-url-picker/input-multi-url-picker.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-multi-url-picker/input-multi-url-picker.stories.ts new file mode 100644 index 0000000000..4e56a92845 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-multi-url-picker/input-multi-url-picker.stories.ts @@ -0,0 +1,17 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './input-multi-url-picker.element'; +import type { UmbInputMultiUrlPickerElement } from './input-multi-url-picker.element'; + +const meta: Meta<UmbInputMultiUrlPickerElement> = { + title: 'Components/Inputs/Multi URL Picker', + component: 'umb-input-multi-url-picker' +}; + +export default meta; +type Story = StoryObj<UmbInputMultiUrlPickerElement>; + +export const Overview: Story = { + args: { + + } +}; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-number-range/input-number-range.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-number-range/input-number-range.stories.ts new file mode 100644 index 0000000000..f8a473cd6a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-number-range/input-number-range.stories.ts @@ -0,0 +1,23 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './input-number-range.element'; +import type { UmbInputNumberRangeElement } from './input-number-range.element'; + +const meta: Meta<UmbInputNumberRangeElement> = { + title: 'Components/Inputs/Number Range Picker', + component: 'umb-input-number-range' +}; + +export default meta; +type Story = StoryObj<UmbInputNumberRangeElement>; + +export const Overview: Story = { + args: { + } +}; + +export const WithMinMax: Story = { + args: { + minValue:0, + maxValue:40, + } +}; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-radio-button-list/input-radio-button-list.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-radio-button-list/input-radio-button-list.stories.ts new file mode 100644 index 0000000000..17ca8ddd83 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-radio-button-list/input-radio-button-list.stories.ts @@ -0,0 +1,79 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './input-radio-button-list.element'; +import type { UmbInputRadioButtonListElement } from './input-radio-button-list.element'; + +const meta: Meta<UmbInputRadioButtonListElement> = { + title: 'Components/Inputs/Radio Button List', + component: 'umb-input-radio-button-list' +}; + +export default meta; +type Story = StoryObj<UmbInputRadioButtonListElement>; + +export const Overview: Story = { + args: { + list: [ + { + key: '1', + sortOrder: 0, + value: 'One' + }, + { + key: '2', + sortOrder: 1, + value: 'Two' + }, + { + key: '3', + sortOrder: 2, + value: 'Three' + } + ] + } +}; + +export const WithSelectedValue: Story = { + args: { + list: [ + { + key: '1', + sortOrder: 0, + value: 'One' + }, + { + key: '2', + sortOrder: 1, + value: 'Two' + }, + { + key: '3', + sortOrder: 2, + value: 'Three' + } + ], + selected: '2', + value: 'Two' + } +}; + +export const SortOrder: Story = { + args: { + list: [ + { + key: '1', + sortOrder: 4, + value: 'One' + }, + { + key: '2', + sortOrder: 1, + value: 'Two' + }, + { + key: '3', + sortOrder: 2, + value: 'Three' + } + ] + } +}; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-section/input-section.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-section/input-section.element.ts index 70192a1689..a68fe5ee37 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-section/input-section.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-section/input-section.element.ts @@ -2,6 +2,7 @@ import { UUITextStyles } from '@umbraco-ui/uui-css'; import { css, html, nothing } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { UmbInputListBase } from '../input-list-base/input-list-base'; +import { UMB_SECTION_PICKER_MODAL_TOKEN } from '../../modals/section-picker'; import type { ManifestSection } from '@umbraco-cms/models'; import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; @@ -41,7 +42,7 @@ export class UmbInputPickerSectionElement extends UmbInputListBase { connectedCallback(): void { super.connectedCallback(); - this.pickerLayout = 'umb-picker-layout-section'; + this.pickerToken = UMB_SECTION_PICKER_MODAL_TOKEN; this._observeSections(); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-section/input-section.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-section/input-section.stories.ts new file mode 100644 index 0000000000..5144d7e765 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-section/input-section.stories.ts @@ -0,0 +1,44 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './input-section.element'; +import type { UmbInputPickerSectionElement } from './input-section.element'; + +const meta: Meta<UmbInputPickerSectionElement> = { + title: 'Components/Inputs/Section', + component: 'umb-input-section', + argTypes: { + modalType: { + control: 'inline-radio', + options: ['dialog', 'sidebar'], + defaultValue: 'sidebar', + description: 'The type of modal to use when selecting sections', + }, + modalSize:{ + control: 'select', + options: ['small', 'medium', 'large', 'full'], + defaultValue: 'small', + description: 'The size of the modal to use when selecting sections, only applicable to sidebar not dialog', + } + } +}; + +export default meta; +type Story = StoryObj<UmbInputPickerSectionElement>; + +export const Overview: Story = { + args: { + modalType: 'sidebar', + } +}; + +export const WithDialog: Story = { + args: { + modalType: 'dialog' + } +}; + +export const WithFullSidebar: Story = { + args: { + modalType: 'sidebar', + modalSize: 'full' + } +}; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-slider/input-slider.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-slider/input-slider.stories.ts new file mode 100644 index 0000000000..01e52a100c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-slider/input-slider.stories.ts @@ -0,0 +1,50 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './input-slider.element'; +import type { UmbInputSliderElement } from './input-slider.element'; + +const meta: Meta<UmbInputSliderElement> = { + title: 'Components/Inputs/Slider', + component: 'umb-input-slider', + +}; + +export default meta; +type Story = StoryObj<UmbInputSliderElement>; + +export const Overview: Story = { + args: { + min: 0, + max: 100, + step: 10, + initVal1: 20 + } +}; + +export const WithRange: Story = { + args: { + min: 0, + max: 100, + step: 10, + initVal1: 20, + initVal2: 80, + enableRange: true + } +}; + +export const WithSmallStep: Story = { + args: { + min: 0, + max: 5, + step: 1, + initVal1: 4, + } +}; + +export const WithLargeMinMax: Story = { + args: { + min: 0, + max: 100, + step: 1, + initVal1: 86 + } +}; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-toggle/input-toggle-element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-toggle/input-toggle.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-toggle/input-toggle-element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-toggle/input-toggle.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-toggle/input-toggle.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-toggle/input-toggle.stories.ts new file mode 100644 index 0000000000..fe52faabaf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-toggle/input-toggle.stories.ts @@ -0,0 +1,37 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './input-toggle.element'; +import type { UmbInputToggleElement } from './input-toggle.element'; + +const meta: Meta<UmbInputToggleElement> = { + title: 'Components/Inputs/Toggle', + component: 'umb-input-toggle', + +}; + +export default meta; +type Story = StoryObj<UmbInputToggleElement>; + +export const Overview: Story = { + args: { + checked: true, + showLabels: true, + labelOn: 'On', + labelOff: 'Off' + } +}; + +export const WithDifferentLabels: Story = { + args: { + checked: false, + showLabels: true, + labelOn: 'Hide the treasure', + labelOff: 'Show the way to the treasure' + } +}; + +export const WithNoLabels: Story = { + args: { + checked: true, + showLabels: false, + } +}; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-user-group/input-user-group.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-user-group/input-user-group.element.ts index 86053ffd54..8bbe14fe27 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-user-group/input-user-group.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-user-group/input-user-group.element.ts @@ -2,9 +2,13 @@ import { UUITextStyles } from '@umbraco-ui/uui-css'; import { css, html, nothing } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { UmbInputListBase } from '../input-list-base/input-list-base'; -import { UmbUserGroupStore, UMB_USER_GROUP_STORE_CONTEXT_TOKEN } from '../../../users/user-groups/user-group.store'; +import { + UmbUserGroupStore, + UMB_USER_GROUP_STORE_CONTEXT_TOKEN, +} from '../../../users/user-groups/repository/user-group.store'; import type { UserGroupEntity } from '@umbraco-cms/models'; +import { UMB_USER_GROUP_PICKER_MODAL_TOKEN } from 'src/backoffice/users/user-groups/modals/user-group-picker'; @customElement('umb-input-user-group') export class UmbInputPickerUserGroupElement extends UmbInputListBase { @@ -44,7 +48,7 @@ export class UmbInputPickerUserGroupElement extends UmbInputListBase { connectedCallback(): void { super.connectedCallback(); - this.pickerLayout = 'umb-picker-layout-user-group'; + this.pickerToken = UMB_USER_GROUP_PICKER_MODAL_TOKEN; this.consumeContext(UMB_USER_GROUP_STORE_CONTEXT_TOKEN, (usersContext) => { this._userGroupStore = usersContext; this._observeUserGroups(); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-user-group/input-user-group.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-user-group/input-user-group.stories.ts new file mode 100644 index 0000000000..9cacc00a54 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-user-group/input-user-group.stories.ts @@ -0,0 +1,30 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './input-user-group.element'; +import type { UmbInputPickerUserGroupElement } from './input-user-group.element'; + +const meta: Meta<UmbInputPickerUserGroupElement> = { + title: 'Components/Inputs/User Group', + component: 'umb-input-user-group', + argTypes: { + modalType: { + control: 'inline-radio', + options: ['dialog', 'sidebar'], + defaultValue: 'sidebar', + description: 'The type of modal to use when selecting user groups', + }, + modalSize:{ + control: 'select', + options: ['small', 'medium', 'large', 'full'], + defaultValue: 'small', + description: 'The size of the modal to use when selecting user groups, only applicable to sidebar not dialog', + } + } +}; + +export default meta; +type Story = StoryObj<UmbInputPickerUserGroupElement>; + +export const Overview: Story = { + args: { + } +}; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-user/input-user.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-user/input-user.element.ts index cb9e22bd42..28250e8fd2 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-user/input-user.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-user/input-user.element.ts @@ -2,8 +2,9 @@ import { UUITextStyles } from '@umbraco-ui/uui-css'; import { css, html, nothing, PropertyValueMap } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { UmbInputListBase } from '../input-list-base/input-list-base'; -import { UmbUserStore, UMB_USER_STORE_CONTEXT_TOKEN } from '../../../users/users/user.store'; +import { UmbUserStore, UMB_USER_STORE_CONTEXT_TOKEN } from '../../../users/users/repository/user.store'; import type { UserEntity } from '@umbraco-cms/models'; +import { UMB_USER_PICKER_MODAL_TOKEN } from 'src/backoffice/users/users/modals/user-picker'; @customElement('umb-input-user') export class UmbPickerUserElement extends UmbInputListBase { @@ -38,7 +39,7 @@ export class UmbPickerUserElement extends UmbInputListBase { connectedCallback(): void { super.connectedCallback(); - this.pickerLayout = 'umb-picker-layout-user'; + this.pickerToken = UMB_USER_PICKER_MODAL_TOKEN; this.consumeContext(UMB_USER_STORE_CONTEXT_TOKEN, (userStore) => { this._userStore = userStore; this._observeUser(); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-user/input-user.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-user/input-user.stories.ts new file mode 100644 index 0000000000..e1f907ddd1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-user/input-user.stories.ts @@ -0,0 +1,30 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './input-user.element'; +import type { UmbPickerUserElement } from './input-user.element'; + +const meta: Meta<UmbPickerUserElement> = { + title: 'Components/Inputs/User', + component: 'umb-input-user', + argTypes: { + modalType: { + control: 'inline-radio', + options: ['dialog', 'sidebar'], + defaultValue: 'sidebar', + description: 'The type of modal to use when selecting users', + }, + modalSize:{ + control: 'select', + options: ['small', 'medium', 'large', 'full'], + defaultValue: 'small', + description: 'The size of the modal to use when selecting users, only applicable to sidebar not dialog', + } + } +}; + +export default meta; +type Story = StoryObj<UmbPickerUserElement>; + +export const Overview: Story = { + args: { + } +}; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/property-type-based-property/property-type-based-property.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/property-type-based-property/property-type-based-property.element.ts index e5a122d8e6..2e26102210 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/property-type-based-property/property-type-based-property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/property-type-based-property/property-type-based-property.element.ts @@ -48,24 +48,24 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement { private _value?: unknown; /** - * VariantId. A Variant Configuration to identify which the variant of its value. + * propertyVariantId. A VariantID to identify which the variant of this properties value. * @public * @type {UmbVariantId} * @attr * @default undefined */ @property({ type: Object, attribute: false }) - public get variantId(): UmbVariantId | undefined { - return this._variantId; + public get propertyVariantId(): UmbVariantId | undefined { + return this._propertyVariantId; } - public set variantId(value: UmbVariantId | undefined) { - const oldValue = this._variantId; + public set propertyVariantId(value: UmbVariantId | undefined) { + const oldValue = this._propertyVariantId; if (value && oldValue?.equal(value)) return; - this._variantId = value; + this._propertyVariantId = value; this._observeProperty(); - this.requestUpdate('variantId', oldValue); + this.requestUpdate('propertyVariantId', oldValue); } - private _variantId?: UmbVariantId | undefined; + private _propertyVariantId?: UmbVariantId | undefined; private _workspaceContext?: UmbDocumentWorkspaceContext; @@ -83,7 +83,7 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement { this._observePropertyValue?.destroy(); this._observePropertyValue = this.observe( - this._workspaceContext.propertyValueByAlias(this._property.alias, this._variantId), + this._workspaceContext.propertyValueByAlias(this._property.alias, this._propertyVariantId), (value) => { this._value = value; } @@ -113,7 +113,7 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement { description=${ifDefined(this._property?.description || undefined)} property-editor-ui-alias=${ifDefined(this._propertyEditorUiAlias)} .value=${this._value} - .variantId=${this.variantId} + .propertyVariantId=${this.propertyVariantId} .config=${this._dataTypeData}></umb-workspace-property>`; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/ref-property-editor-ui/ref-property-editor-ui.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/ref-property-editor-ui/ref-property-editor-ui.stories.ts index d622e7321d..34d8e5365b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/ref-property-editor-ui/ref-property-editor-ui.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/ref-property-editor-ui/ref-property-editor-ui.stories.ts @@ -1,19 +1,55 @@ -import './ref-property-editor-ui.element'; - -import { Meta, Story } from '@storybook/web-components'; +import { Meta, StoryObj } from '@storybook/web-components'; import { html } from 'lit-html'; - +import './ref-property-editor-ui.element'; import type { UmbRefPropertyEditorUIElement } from './ref-property-editor-ui.element'; -export default { - title: 'Components/Ref Property Editor UI', - component: 'umb-ref-property-editor-ui', - id: 'umb-ref-property-editor-ui', -} as Meta; +const meta: Meta<UmbRefPropertyEditorUIElement> = { + title: 'Components/Ref Property Editor UI', + component: 'umb-ref-property-editor-ui', +}; + +export default meta; +type Story = StoryObj<UmbRefPropertyEditorUIElement>; + +export const Overview: Story = { + args: { + name: "Custom Property Editor UI", + alias: "Umb.PropertyEditorUI.CustomUI", + propertyEditorAlias: "Umbraco.JSON" + } +}; -export const AAAOverview: Story<UmbRefPropertyEditorUIElement> = () => - html` <umb-ref-property-editor-ui - name="Custom Property Editor UI" - alias="Umb.PropertyEditorUI.CustomUI" - property-editor-model-alias="Umbraco.JSON"></umb-ref-property-editor-ui>`; -AAAOverview.storyName = 'Overview'; + +export const WithDetail: Story = { + args: { + name: "Custom Property Editor UI", + alias: "Umb.PropertyEditorUI.CustomUI", + propertyEditorAlias: "Umbraco.JSON", + detail: "With some custom details" + } +}; + +export const WithSlots: Story = { + args: { + name: "Custom Property Editor UI", + alias: "Umb.PropertyEditorUI.CustomUI", + propertyEditorAlias: "Umbraco.JSON", + detail: "With some custom details" + }, + render: (args) => html` + <umb-ref-property-editor-ui + .name=${args.name} + .alias=${args.alias} + .propertyEditorAlias=${args.propertyEditorAlias} + .detail=${args.detail}> + <div slot="tag"><uui-tag color="positive">10</uui-tag></div> + <div slot="actions"> + <uui-action-bar> + <uui-button label="delete" look="primary" color="danger" compact> + <uui-icon name="umb:delete"></uui-icon> + </uui-button> + </uui-action-bar> + </div> + </umb-ref-property-editor-ui> + ` +}; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.context.ts index 74be5bbd8d..b46b979b94 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.context.ts @@ -5,7 +5,6 @@ import { UmbContextToken } from '@umbraco-cms/context-api'; export type ActiveTreeItemType = Entity | undefined; export class UmbSectionContext { - #manifestAlias = new StringState<string | undefined>(undefined); #manifestPathname = new StringState<string | undefined>(undefined); #manifestLabel = new StringState<string | undefined>(undefined); @@ -34,7 +33,7 @@ export class UmbSectionContext { public setManifest(manifest?: ManifestSection) { this.#manifestAlias.next(manifest?.alias); this.#manifestPathname.next(manifest?.meta?.pathname); - this.#manifestLabel.next(manifest ? (manifest.meta?.label || manifest.name) : undefined); + this.#manifestLabel.next(manifest ? manifest.meta?.label || manifest.name : undefined); } /* diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.element.ts index 68236f6226..4d678f7f8c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.element.ts @@ -91,6 +91,7 @@ export class UmbSectionElement extends UmbLitElement { private _createWorkspaceRoutes() { if (!this._workspaces) return; // TODO: find a way to make this reuseable across: + // TODO: Move workspace 'handlers/routes' to the workspace-element. So it becomes local. const workspaceRoutes = this._workspaces?.map((workspace: ManifestWorkspace) => { return [ { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/table/table.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/table/table.stories.ts index b26be13e10..d7ac93c25a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/table/table.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/table/table.stories.ts @@ -1,16 +1,17 @@ -import { Meta, Story } from '@storybook/web-components'; -import { html } from 'lit-html'; -import { v4 as uuidv4 } from 'uuid'; - -import type { UmbTableElement, UmbTableColumn, UmbTableConfig, UmbTableItem } from './table.element'; - +import { Meta, StoryObj } from '@storybook/web-components'; import './table.element'; +import { v4 as uuidv4 } from 'uuid'; +import type { UmbTableElement, UmbTableColumn, UmbTableConfig, UmbTableItem } from './table.element' -export default { - title: 'Components/Table', - component: 'umb-table', - id: 'umb-table', -} as Meta; +const meta: Meta<UmbTableElement> = { + title: 'Components/Table', + component: 'umb-table', +}; + +export default meta; +type Story = StoryObj<UmbTableElement>; + +const today = new Intl.DateTimeFormat('en-US').format(new Date()); const columns: Array<UmbTableColumn> = [ { @@ -23,8 +24,6 @@ const columns: Array<UmbTableColumn> = [ }, ]; -const today = new Intl.DateTimeFormat('en-US').format(new Date()); - const items: Array<UmbTableItem> = [ { key: uuidv4(), @@ -70,11 +69,36 @@ const items: Array<UmbTableItem> = [ }, ]; -const config: UmbTableConfig = { - allowSelection: true, - hideIcon: false, +export const Overview: Story = { + args: { + items: items, + columns: columns, + config: { + allowSelection: true, + hideIcon: false, + } + } }; -export const AAAOverview: Story<UmbTableElement> = () => - html`<umb-table .items=${items} .columns=${columns} .config=${config}></umb-table>`; -AAAOverview.storyName = 'Overview'; + +export const WithDisallowedSelections: Story = { + args: { + items: items, + columns: columns, + config: { + allowSelection: false, + hideIcon: false, + } + } +}; + +export const WithHiddenIcons: Story = { + args: { + items: items, + columns: columns, + config: { + allowSelection: true, + hideIcon: true, + } + } +}; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tooltip-menu/tooltip-menu.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tooltip-menu/tooltip-menu.stories.ts new file mode 100644 index 0000000000..1d963f4047 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tooltip-menu/tooltip-menu.stories.ts @@ -0,0 +1,39 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './tooltip-menu.element'; +import type { UmbTooltipMenuElement, TooltipMenuItem } from './tooltip-menu.element' + +const meta: Meta<UmbTooltipMenuElement> = { + title: 'Components/Tooltip Menu', + component: 'umb-tooltip-menu', +}; + +export default meta; +type Story = StoryObj<UmbTooltipMenuElement>; + +const items: Array<TooltipMenuItem> = [ + { + label: 'Item 1', + icon: 'umb:document', + action: () => alert('Item 1 clicked'), + }, + { + label: 'Item 2', + icon: 'umb:home', + action: () => alert('Item 2 clicked') + } +]; + +export const Overview: Story = { + args: { + items: items, + iconOnly: false, + } +}; + +export const WithIconsOnly: Story = { + args: { + items: items, + iconOnly: true, + } +}; + diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item.stories.ts new file mode 100644 index 0000000000..5b59cbde3b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item.stories.ts @@ -0,0 +1,27 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './tree-item.element'; +import type { UmbTreeItem } from './tree-item.element' + +const meta: Meta<UmbTreeItem> = { + title: 'Components/Tree/Tree Item', + component: 'umb-tree-item', +}; + +export default meta; +type Story = StoryObj<UmbTreeItem>; + +export const Overview: Story = { + args: { + label: 'My Tree Item', + icon: 'umb:home', + hasChildren: false, + } +}; + +export const WithChildren: Story = { + args: { + label: 'My Tree Item', + icon: 'umb:home', + hasChildren: true, + } +}; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.stories.ts new file mode 100644 index 0000000000..9b3c0aa66c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.stories.ts @@ -0,0 +1,19 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './tree.element'; +import type { UmbTreeElement } from './tree.element'; + +const meta: Meta<UmbTreeElement> = { + title: 'Components/Tree/Tree', + component: 'umb-tree', +}; + +export default meta; +type Story = StoryObj<UmbTreeElement>; + +// TODO: This does not display anything - need help +export const Overview: Story = { + args: { + alias: 'Umb.Tree.Documents', + selectable: true, + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/variant-selector/variant-selector.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/variant-selector/variant-selector.element.ts index 280bfd01ce..6baa96b9c7 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/variant-selector/variant-selector.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/variant-selector/variant-selector.element.ts @@ -3,8 +3,11 @@ import { css, html, nothing } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { UUIInputElement, UUIInputEvent } from '@umbraco-ui/uui'; import { ifDefined } from 'lit-html/directives/if-defined.js'; -import { UmbWorkspaceVariantContext } from '../workspace/workspace-variant/workspace-variant.context'; -import { UmbDocumentWorkspaceContext } from '../../../documents/documents/workspace/document-workspace.context'; +import { + UmbWorkspaceVariantContext, + UMB_WORKSPACE_VARIANT_CONTEXT_TOKEN, +} from '../workspace/workspace-variant/workspace-variant.context'; +import { ActiveVariant } from '../workspace/workspace-context/workspace-split-view-manager.class'; import { UmbLitElement } from '@umbraco-cms/element'; import type { DocumentVariantModel } from '@umbraco-cms/backend-api'; @@ -35,6 +38,10 @@ export class UmbVariantSelectorElement extends UmbLitElement { box-sizing: border-box; box-shadow: var(--uui-shadow-depth-3); } + + #variant-close { + white-space: nowrap; + } `, ]; @@ -45,7 +52,10 @@ export class UmbVariantSelectorElement extends UmbLitElement { @state() _variants: Array<DocumentVariantModel> = []; - private _workspaceContext?: UmbDocumentWorkspaceContext; + // TODO: Stop using document context specific ActiveVariant type. + @state() + _activeVariants: Array<ActiveVariant> = []; + private _variantContext?: UmbWorkspaceVariantContext; @state() @@ -66,26 +76,46 @@ export class UmbVariantSelectorElement extends UmbLitElement { constructor() { super(); - // TODO: Figure out how to get the magic string for the workspace context. - this.consumeContext<UmbDocumentWorkspaceContext>('umbWorkspaceContext', (instance) => { - this._workspaceContext = instance; - this._observeVariants(); - }); - - this.consumeContext<UmbWorkspaceVariantContext>('umbWorkspaceVariantContext', (instance) => { + this.consumeContext<UmbWorkspaceVariantContext>(UMB_WORKSPACE_VARIANT_CONTEXT_TOKEN, (instance) => { this._variantContext = instance; + this._observeVariants(); + this._observeActiveVariants(); this._observeVariantContext(); }); } private async _observeVariants() { - if (!this._workspaceContext) return; + if (!this._variantContext) return; - this.observe(this._workspaceContext.variants, (variants) => { - if (variants) { - this._variants = variants; - } - }); + const workspaceContext = this._variantContext.getWorkspaceContext(); + if (workspaceContext) { + this.observe( + workspaceContext.variants, + (variants) => { + if (variants) { + this._variants = variants; + } + }, + '_observeVariants' + ); + } + } + + private async _observeActiveVariants() { + if (!this._variantContext) return; + + const workspaceContext = this._variantContext.getWorkspaceContext(); + if (workspaceContext) { + this.observe( + workspaceContext.splitView.activeVariantsInfo, + (activeVariants) => { + if (activeVariants) { + this._activeVariants = activeVariants; + } + }, + '_observeActiveVariants' + ); + } } private async _observeVariantContext() { @@ -149,16 +179,17 @@ export class UmbVariantSelectorElement extends UmbLitElement { } private _switchVariant(variant: DocumentVariantModel) { - if (variant.culture === undefined || variant.segment === undefined) return; - this._variantContext?.changeVariant(variant.culture, variant.segment); + this._variantContext?.switchVariant(variant); this._close(); } private _openSplitView(variant: DocumentVariantModel) { - if (variant.culture === undefined || variant.segment === undefined) return; - this._workspaceContext?.openSplitView(variant.culture, variant.segment); + this._variantContext?.openSplitView(variant); this._close(); } + private _closeSplitView() { + this._variantContext?.closeSplitView(); + } render() { return html` @@ -166,15 +197,21 @@ export class UmbVariantSelectorElement extends UmbLitElement { ${ this._variants && this._variants.length > 0 ? html` - <div slot="append"> - <uui-button - id="variant-selector-toggle" - @click=${this._toggleVariantSelector} - title=${ifDefined(this._variantTitleName)}> - ${this._variantDisplayName} - <uui-symbol-expand></uui-symbol-expand> - </uui-button> - </div> + <uui-button + slot="append" + id="variant-selector-toggle" + @click=${this._toggleVariantSelector} + title=${ifDefined(this._variantTitleName)}> + ${this._variantDisplayName} + <uui-symbol-expand></uui-symbol-expand> + </uui-button> + ${this._activeVariants.length > 1 + ? html` + <uui-button slot="append" compact id="variant-close" @click=${this._closeSplitView}> + <uui-icon name="remove"></uui-icon> + </uui-button> + ` + : ''} ` : nothing } @@ -190,10 +227,12 @@ export class UmbVariantSelectorElement extends UmbLitElement { (variant) => html`<ul> <li> + <!-- TODO: style this better, most likely not use ul and li --> <uui-button @click=${() => this._switchVariant(variant)}> ${variant.name} ${variant.culture} ${variant.segment} </uui-button> + <!-- TODO: only make this available if not already open --> <uui-button @click=${() => this._openSplitView(variant)}> Split view </uui-button> </li> </ul>` diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/variant-selector/variant-selector.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/variant-selector/variant-selector.stories.ts new file mode 100644 index 0000000000..c5f645add3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/variant-selector/variant-selector.stories.ts @@ -0,0 +1,17 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './variant-selector.element'; +import type { UmbVariantSelectorElement } from './variant-selector.element' + +const meta: Meta<UmbVariantSelectorElement> = { + title: 'Components/Variant Selector', + component: 'umb-variant-selector', +}; + +export default meta; +type Story = StoryObj<UmbVariantSelectorElement>; + +export const Overview: Story = { + args: { + alias: 'myAlias' + } +}; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/variantable-property/variantable-property.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/variantable-property/variantable-property.element.ts index 9a01dbf98e..35cd55ba18 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/variantable-property/variantable-property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/variantable-property/variantable-property.element.ts @@ -2,11 +2,10 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { UmbVariantId } from '../../variants/variant-id.class'; +import { UMB_WORKSPACE_VARIANT_CONTEXT_TOKEN } from '../workspace/workspace-variant/workspace-variant.context'; import type { PropertyTypeViewModelBaseModel } from '@umbraco-cms/backend-api'; import '../workspace-property/workspace-property.element'; import { UmbLitElement } from '@umbraco-cms/element'; -// eslint-disable-next-line import/order -import { UmbWorkspaceVariantContext } from '../workspace/workspace-variant/workspace-variant.context'; @customElement('umb-variantable-property') export class UmbVariantablePropertyElement extends UmbLitElement { @@ -29,7 +28,7 @@ export class UmbVariantablePropertyElement extends UmbLitElement { this._updatePropertyVariantId(); } - private _variantContext?: UmbWorkspaceVariantContext; + private _variantContext?: typeof UMB_WORKSPACE_VARIANT_CONTEXT_TOKEN.TYPE; @state() private _workspaceVariantId?: UmbVariantId; @@ -39,7 +38,7 @@ export class UmbVariantablePropertyElement extends UmbLitElement { constructor() { super(); - this.consumeContext('umbWorkspaceVariantContext', (workspaceContext: UmbWorkspaceVariantContext) => { + this.consumeContext(UMB_WORKSPACE_VARIANT_CONTEXT_TOKEN, (workspaceContext) => { this._variantContext = workspaceContext; this._observeVariantContext(); }); @@ -55,10 +54,10 @@ export class UmbVariantablePropertyElement extends UmbLitElement { private _updatePropertyVariantId() { if (this._workspaceVariantId && this.property) { - const newVariantId = UmbVariantId.Create( - this.property.variesByCulture ? this._workspaceVariantId.culture : null, - this.property.variesBySegment ? this._workspaceVariantId.segment : null - ); + const newVariantId = UmbVariantId.Create({ + culture: this.property.variesByCulture ? this._workspaceVariantId.culture : null, + segment: this.property.variesBySegment ? this._workspaceVariantId.segment : null, + }); if (!this._propertyVariantId || !newVariantId.equal(this._propertyVariantId)) { this._propertyVariantId = newVariantId; } @@ -68,7 +67,7 @@ export class UmbVariantablePropertyElement extends UmbLitElement { render() { return html`<umb-property-type-based-property .property=${this._property} - .variantId=${this._propertyVariantId}></umb-property-type-based-property>`; + .propertyVariantId=${this._propertyVariantId}></umb-property-type-based-property>`; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/variantable-property/variantable-property.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/variantable-property/variantable-property.stories.ts new file mode 100644 index 0000000000..abcf01e0e0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/variantable-property/variantable-property.stories.ts @@ -0,0 +1,32 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './variantable-property.element'; +import type { UmbVariantablePropertyElement } from './variantable-property.element' + +const meta: Meta<UmbVariantablePropertyElement> = { + title: 'Components/Variantable Property', + component: 'umb-variantable-property', +}; + +export default meta; +type Story = StoryObj<UmbVariantablePropertyElement>; + +export const Overview: Story = { + args: { + property: { + name: 'Header', + alias: 'headerAlias', + appearance: { + labelOnTop: false + }, + description: 'This is a description', + variesByCulture: true, + variesBySegment: true, + validation: { + mandatory: true, + mandatoryMessage: 'This is a mandatory message', + } + }, + + } +}; + diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.context.ts index 415450f1a6..5474e323a9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.context.ts @@ -1,8 +1,9 @@ import { UmbVariantId } from '../../variants/variant-id.class'; import { UmbWorkspaceVariableEntityContextInterface } from '../workspace/workspace-context/workspace-variable-entity-context.interface'; +import { UMB_WORKSPACE_VARIANT_CONTEXT_TOKEN } from '../workspace/workspace-variant/workspace-variant.context'; import type { DataTypeModel } from '@umbraco-cms/backend-api'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; -import { ObjectState } from '@umbraco-cms/observable-api'; +import { ObjectState, StringState, UmbObserverController } from '@umbraco-cms/observable-api'; import { UmbContextConsumerController, UmbContextProviderController } from '@umbraco-cms/context-api'; // If we get this from the server then we can consider using TypeScripts Partial<> around the model from the Management-API. @@ -15,6 +16,8 @@ export type WorkspacePropertyData<ValueType> = { }; export class UmbWorkspacePropertyContext<ValueType = unknown> { + #host: UmbControllerHostInterface; + private _providerController: UmbContextProviderController; private _data = new ObjectState<WorkspacePropertyData<ValueType>>({}); @@ -27,9 +30,13 @@ export class UmbWorkspacePropertyContext<ValueType = unknown> { private _variantId?: UmbVariantId; + private _variantDifference = new StringState(undefined); + public readonly variantDifference = this._variantDifference.asObservable(); + private _workspaceContext?: UmbWorkspaceVariableEntityContextInterface; constructor(host: UmbControllerHostInterface) { + this.#host = host; // TODO: Figure out how to get the magic string in a better way. new UmbContextConsumerController<UmbWorkspaceVariableEntityContextInterface>( host, @@ -68,6 +75,11 @@ export class UmbWorkspacePropertyContext<ValueType = unknown> { } public setVariantId(variantId: UmbVariantId | undefined) { this._variantId = variantId; + new UmbContextConsumerController(this.#host, UMB_WORKSPACE_VARIANT_CONTEXT_TOKEN, (variantContext) => { + new UmbObserverController(this.#host, variantContext.variantId, (variantId) => { + this._variantDifference.next(variantId ? this._variantId?.toDifferencesString(variantId) : ''); + }); + }); } public getVariantId() { return this._variantId; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.element.ts index fcafafa42d..c927ef409e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.element.ts @@ -118,20 +118,20 @@ export class UmbWorkspacePropertyElement extends UmbLitElement { } /** - * VariantId. A Variant Configuration to identify which variant its value is stored on. + * PropertyVariantId. A Variant ID to identify which variant its value is stored on. * @public * @type {UmbVariantId} * @attr * @default null */ @property({ type: Object, attribute: false }) - public set variantId(value: UmbVariantId | undefined) { + public set propertyVariantId(value: UmbVariantId | undefined) { this._propertyContext.setVariantId(value); - this._variantDisplayName = value?.toString(); + //this._variantDisplayName = value?.toString(); } @state() - private _variantDisplayName?: string; + private _variantDifference?: string; // TODO: make interface for UMBPropertyEditorElement @state() @@ -168,6 +168,9 @@ export class UmbWorkspacePropertyElement extends UmbLitElement { this.observe(this._propertyContext.description, (description) => { this._description = description; }); + this.observe(this._propertyContext.variantDifference, (variantDifference) => { + this._variantDifference = variantDifference; + }); } private _onPropertyEditorChange = (e: CustomEvent) => { @@ -236,7 +239,9 @@ export class UmbWorkspacePropertyElement extends UmbLitElement { label="${ifDefined(this._label)}" description="${ifDefined(this._description)}"> ${this._renderPropertyActionMenu()} - <p slot="description">${this._variantDisplayName}</p> + ${this._variantDifference + ? html`<uui-tag look="secondary" slot="description">${this._variantDifference}</uui-tag>` + : ''} <div slot="editor">${this._element}</div> </umb-workspace-property-layout> `; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-action-menu/workspace-action-menu.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-action-menu/workspace-action-menu.element.ts index 17675b5864..1cf0cd994d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-action-menu/workspace-action-menu.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-action-menu/workspace-action-menu.element.ts @@ -1,9 +1,9 @@ import { UUITextStyles } from '@umbraco-ui/uui-css'; import { css, html } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; -import { ifDefined } from 'lit-html/directives/if-defined.js'; -import { UmbLitElement } from '@umbraco-cms/element'; +import { customElement, state } from 'lit/decorators.js'; +import { UmbWorkspaceEntityContextInterface } from '../workspace-context/workspace-entity-context.interface'; import { UmbExecutedEvent } from '@umbraco-cms/events'; +import { UmbLitElement } from '@umbraco-cms/element'; @customElement('umb-workspace-action-menu') export class UmbWorkspaceActionMenuElement extends UmbLitElement { @@ -23,20 +23,40 @@ export class UmbWorkspaceActionMenuElement extends UmbLitElement { height: 100%; box-sizing: border-box; box-shadow: var(--uui-shadow-depth-3); - width: 500px; + width: 250px; + position: absolute; + right: 5px; + height: auto; } `, ]; - @property({ type: String }) - public unique?: string; - - @property({ type: String, attribute: 'entity-type' }) - public entityType?: string; - @state() private _actionMenuIsOpen = false; + private _workspaceContext?: UmbWorkspaceEntityContextInterface; + + @state() + _entityKey?: string; + + @state() + _entityType?: string; + + constructor() { + super(); + + this.consumeContext<UmbWorkspaceEntityContextInterface>('umbWorkspaceContext', (context) => { + this._workspaceContext = context; + this._observeInfo(); + }); + } + + private _observeInfo() { + if (!this._workspaceContext) return; + this._entityKey = this._workspaceContext.getEntityKey(); + this._entityType = this._workspaceContext.getEntityType(); + } + #close() { this._actionMenuIsOpen = false; } @@ -55,18 +75,20 @@ export class UmbWorkspaceActionMenuElement extends UmbLitElement { } #renderActionsMenu() { - return html` + return this._entityKey + ? html` <uui-popover id="action-menu-popover" .open=${this._actionMenuIsOpen} @close=${this.#close}> <uui-button slot="trigger" label="Actions" @click=${this.#open}></uui-button> <div id="action-menu-dropdown" slot="popover"> <uui-scroll-container> - <umb-entity-action-list @executed=${this.#onActionExecuted} entity-type=${ifDefined( - this.entityType - )} unique=${ifDefined(this.unique)}></umb-entity-action-list> + <umb-entity-action-list @executed=${this.#onActionExecuted} entity-type=${this._entityType as string} unique=${ + this._entityKey + }></umb-entity-action-list> </uui-scroll-container> </div> </uui-popover> - </div>`; + </div>` + : ''; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/views/info/workspace-view-content-info.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/views/info/workspace-view-content-info.element.ts index 3299a3d858..3a884755fb 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/views/info/workspace-view-content-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/views/info/workspace-view-content-info.element.ts @@ -1,26 +1,191 @@ -import { css, html } from 'lit'; +import { css, html, nothing } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { repeat } from 'lit/directives/repeat.js'; import { customElement, state } from 'lit/decorators.js'; +import { UUIPaginationEvent } from '@umbraco-ui/uui'; import { UmbWorkspaceEntityContextInterface } from '../../../workspace-context/workspace-entity-context.interface'; import type { DocumentModel } from '@umbraco-cms/backend-api'; import { UmbLitElement } from '@umbraco-cms/element'; +interface HistoryNode { + userId?: number; + userAvatars?: []; + userName?: string; + timestamp?: string; + comment?: string; + entityType?: string; + logType?: HistoryLogType; + nodeId?: string; + parameters?: string; +} + +type HistoryLogType = 'Publish' | 'Save' | 'Unpublish' | 'ContentVersionEnableCleanup' | 'ContentVersionPreventCleanup'; + @customElement('umb-workspace-view-content-info') export class UmbWorkspaceViewContentInfoElement extends UmbLitElement { static styles = [ UUITextStyles, css` :host { - display: block; + display: grid; + gap: var(--uui-size-layout-1); margin: var(--uui-size-layout-1); + grid-template-columns: 1fr 350px; + } + + div.container { + display: flex; + flex-direction: column; + gap: var(--uui-size-layout-1); + } + + //General section + + #general-section { + display: flex; + flex-direction: column; + } + + .general-item { + display: flex; + flex-direction: column; + gap: var(--uui-size-space-1); + } + + .general-item:not(:last-child) { + margin-bottom: var(--uui-size-space-6); + } + + // Link section + + #link-section { + display: flex; + flex-direction: column; + text-align: left; + } + + .link-item { + padding: var(--uui-size-space-4) var(--uui-size-space-6); + display: grid; + grid-template-columns: 75px 1fr; + color: inherit; + text-decoration: none; + } + + .link-language { + color: var(--uui-color-divider-emphasis); + } + + .link-content.italic { + font-style: italic; + } + + .link-item uui-icon { + margin-right: var(--uui-size-space-2); + vertical-align: middle; + } + + .link-item.with-href { + cursor: pointer; + } + + .link-item.with-href:hover { + background: var(--uui-color-divider); + } + + //History section + + uui-tag uui-icon { + margin-right: var(--uui-size-space-1); + } + + .log-type { + display: flex; + gap: var(--uui-size-space-2); + } + uui-pagination { + display: inline-block; + } + .pagination { + display: flex; + justify-content: center; + margin-top: var(--uui-size-space-4); } `, ]; + @state() + private _historyList: HistoryNode[] = [ + { + userId: -1, + userAvatars: [], + userName: 'Lone Iversen', + timestamp: 'December 5, 2022 2:59 PM', + comment: undefined, + entityType: 'Document', + logType: 'Save', + nodeId: '1058', + parameters: undefined, + }, + { + userId: -1, + userAvatars: [], + userName: 'Lone Iversen', + timestamp: 'December 5, 2022 2:59 PM', + comment: undefined, + entityType: 'Document', + logType: 'Unpublish', + nodeId: '1058', + parameters: undefined, + }, + { + userId: -1, + userAvatars: [], + userName: 'Lone Iversen', + timestamp: 'December 5, 2022 2:59 PM', + comment: undefined, + entityType: 'Document', + logType: 'Publish', + nodeId: '1058', + parameters: undefined, + }, + + { + userId: -1, + userAvatars: [], + userName: 'Lone Iversen', + timestamp: 'December 5, 2022 2:59 PM', + comment: undefined, + entityType: 'Document', + logType: 'Save', + nodeId: '1058', + parameters: undefined, + }, + + { + userId: -1, + userAvatars: [], + userName: 'Lone Iversen', + timestamp: 'December 5, 2022 2:59 PM', + comment: undefined, + entityType: 'Document', + logType: 'Save', + nodeId: '1058', + parameters: undefined, + }, + ]; + + @state() + private _total?: number; + + @state() + private _currentPage = 1; + @state() private _nodeName = ''; private _workspaceContext?: UmbWorkspaceEntityContextInterface<DocumentModel>; + private itemsPerPage = 10; constructor() { super(); @@ -36,6 +201,7 @@ export class UmbWorkspaceViewContentInfoElement extends UmbLitElement { if (!this._workspaceContext) return; this._nodeName = 'TBD, with variants this is not as simple.'; + /* this.observe(this._workspaceContext.name, (name) => { this._nodeName = name || ''; @@ -43,8 +209,123 @@ export class UmbWorkspaceViewContentInfoElement extends UmbLitElement { */ } + #onPageChange(event: UUIPaginationEvent) { + if (this._currentPage === event.target.current) return; + this._currentPage = event.target.current; + //TODO: Run endpoint to get next history parts + } + render() { - return html`<div>Info Workspace View for ${this._nodeName}</div>`; + return html`<div class="container"> + <uui-box headline="Links" style="--uui-box-default-padding: 0;"> ${this.#renderLinksSection()} </uui-box> + <uui-box headline="History"> + <umb-history-list> + ${repeat( + this._historyList, + (item) => item.timestamp, + (item) => this.#renderHistory(item) + )} + </umb-history-list> + ${this.#renderHistoryPagination()} + </uui-box> + </div> + <div class="container"> + <uui-box headline="General" id="general-section">${this.#renderGeneralSection()}</uui-box> + </div>`; + } + + #renderLinksSection() { + //repeat + return html`<div id="link-section"> + <a href="http://google.com" target="_blank" class="link-item with-href"> + <span class="link-language">da-DK</span> + <span class="link-content"> <uui-icon name="umb:out"></uui-icon>google.com </span> + </a> + <div class="link-item"> + <span class="link-language">en-EN</span> + <span class="link-content italic"> This document is published but is not in the cache</span> + </div> + </div>`; + } + + #renderGeneralSection() { + return html` + <div class="general-item"> + <strong>Status</strong> + <span><uui-tag color="positive" look="primary" label="Published">Published</uui-tag></span> + </div> + <div class="general-item"> + <strong>Created Date</strong> + <span>...</span> + </div> + <div class="general-item"> + <strong>Document Type</strong> + <span>document type picker?</span> + </div> + <div class="general-item"> + <strong>Template</strong> + <span>template picker?</span> + </div> + <div class="general-item"> + <strong>Id</strong> + <span>...</span> + </div> + `; + } + + #renderHistory(history: HistoryNode) { + return html` <umb-history-item .name="${history.userName}" .detail="${history.timestamp}"> + <span class="log-type">${this.#renderTag(history.logType)} ${this.#renderTagDescription(history.logType)}</span> + <uui-button label="Rollback" look="secondary" slot="actions"> + <uui-icon name="umb:undo"></uui-icon> Rollback + </uui-button> + </umb-history-item>`; + } + + #renderHistoryPagination() { + if (!this._total) return nothing; + + const totalPages = Math.ceil(this._total / this.itemsPerPage); + + if (totalPages <= 1) return nothing; + + return html`<div class="pagination"> + <uui-pagination .total=${totalPages} @change="${this.#onPageChange}"></uui-pagination> + </div>`; + } + + #renderTag(type?: HistoryLogType) { + switch (type) { + case 'Publish': + return html`<uui-tag look="primary" color="positive" label="Publish">Publish</uui-tag>`; + case 'Unpublish': + return html`<uui-tag look="primary" color="warning" label="Unpublish">Unpublish</uui-tag>`; + case 'Save': + return html`<uui-tag look="primary" label="Save">Save</uui-tag>`; + case 'ContentVersionEnableCleanup': + return html`<uui-tag look="secondary" label="Content Version Enable Cleanup">Save</uui-tag>`; + case 'ContentVersionPreventCleanup': + return html`<uui-tag look="secondary" label="Content Version Prevent Cleanup">Save</uui-tag>`; + default: + return 'Could not detech log type'; + } + } + + #renderTagDescription(type?: HistoryLogType, params?: string) { + switch (type) { + case 'Publish': + return html`Content published`; + case 'Unpublish': + return html`Content unpublished`; + case 'Save': + return html`Content saved`; + case 'ContentVersionEnableCleanup': + return html`Cleanup enabled for version: ${params}`; + case 'ContentVersionPreventCleanup': + return html`Cleanup disabled for version: ${params}`; + default: + return 'Could not detech log type'; + } } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/workspace-content.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/workspace-content.element.ts index 6f68c9653d..4614ff8618 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/workspace-content.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/workspace-content.element.ts @@ -48,9 +48,7 @@ export class UmbWorkspaceContentElement extends UmbLitElement { render() { return html` <umb-workspace-layout alias=${this.alias}> - <div id="header" slot="header"> - <umb-variant-selector></umb-variant-selector> - </div> + <div id="header" slot="header">TODO: MISSING INPUT COMPONENT</div> <slot name="action-menu" slot="action-menu"></slot> diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-entity-context.interface.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-entity-context.interface.ts index 754c57ca46..cc11f9ff1c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-entity-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-entity-context.interface.ts @@ -1,3 +1,4 @@ +import { Observable } from 'rxjs'; import type { UmbWorkspaceContextInterface } from './workspace-context.interface'; export interface UmbWorkspaceEntityContextInterface<T = unknown> extends UmbWorkspaceContextInterface<T> { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-split-view-manager.class.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-split-view-manager.class.ts new file mode 100644 index 0000000000..d0e38cab9d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-split-view-manager.class.ts @@ -0,0 +1,101 @@ +import { UmbVariantId } from '../../../variants/variant-id.class'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { ArrayState } from '@umbraco-cms/observable-api'; + +export type ActiveVariant = { + index: number; + culture: string | null; + segment: string | null; +}; + +/** + * @export + * @class UmbWorkspaceSplitViewManager + * @description - Class managing the split view state for a workspace context. + */ +export class UmbWorkspaceSplitViewManager { + #host: UmbControllerHostInterface; + + #activeVariantsInfo = new ArrayState<ActiveVariant>([], (x) => x.index); + public readonly activeVariantsInfo = this.#activeVariantsInfo.asObservable(); + + constructor(host: UmbControllerHostInterface) { + this.#host = host; + } + + private _routeBase?: string; + public getWorkspaceRoute(): string | undefined { + return this._routeBase; + } + public setWorkspaceRoute(route: string | undefined) { + this._routeBase = route; + } + + setActiveVariant(index: number, culture: string | null, segment: string | null) { + this.#activeVariantsInfo.appendOne({ index, culture, segment }); + } + + getActiveVariants() { + return this.#activeVariantsInfo.getValue(); + } + + public removeActiveVariant(index: number) { + if (this.getActiveVariants().length > 1) { + this.#activeVariantsInfo.removeOne(index); + } + } + + public activeVariantByIndex(index: number) { + return this.#activeVariantsInfo.getObservablePart((data) => data[index] || undefined); + } + + public switchVariant(index: number, variantId: UmbVariantId) { + // TODO: remember current path and extend url with it. + // TODO: construct URl with all active routes: + // TODO: use method for generating variant url: + const workspaceRoute = this.getWorkspaceRoute(); + if (workspaceRoute) { + const activeVariants = this.getActiveVariants(); + if (activeVariants && index < activeVariants.length) { + const newVariants = [...activeVariants]; + newVariants[index] = { index, culture: variantId.culture, segment: variantId.segment }; + + const variantPart: string = newVariants.map((v) => new UmbVariantId(v).toString()).join('_&_'); + + history.pushState(null, '', `${workspaceRoute}/${variantPart}`); + return true; + } + } + return false; + } + + public openSplitView(newVariant: UmbVariantId) { + // TODO: remember current path and extend url with it. + // TODO: construct URl with all active routes: + // TODO: use method for generating variant url: + + const currentVariant = this.getActiveVariants()[0]; + const workspaceRoute = this.getWorkspaceRoute(); + if (currentVariant && workspaceRoute) { + history.pushState(null, '', `${workspaceRoute}/${new UmbVariantId(currentVariant)}_&_${newVariant.toString()}`); + return true; + } + return false; + } + + public closeSplitView(index: number) { + const workspaceRoute = this.getWorkspaceRoute(); + if (workspaceRoute) { + const activeVariants = this.getActiveVariants(); + if (activeVariants && index < activeVariants.length) { + const newVariants = activeVariants.filter((x) => x.index !== index); + + const variantPart: string = newVariants.map((v) => new UmbVariantId(v).toString()).join('_&_'); + + history.pushState(null, '', `${workspaceRoute}/${variantPart}`); + return true; + } + } + return false; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-variable-entity-context.interface.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-variable-entity-context.interface.ts index d54ee128e7..7167156eea 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-variable-entity-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-variable-entity-context.interface.ts @@ -1,12 +1,19 @@ import type { Observable } from 'rxjs'; import { UmbVariantId } from '../../../variants/variant-id.class'; import type { UmbWorkspaceEntityContextInterface } from './workspace-entity-context.interface'; -import type { ValueViewModelBaseModel } from '@umbraco-cms/backend-api'; +import { UmbWorkspaceSplitViewManager } from './workspace-split-view-manager.class'; +import type { ValueViewModelBaseModel, VariantViewModelBaseModel } from '@umbraco-cms/backend-api'; export interface UmbWorkspaceVariableEntityContextInterface<T = unknown> extends UmbWorkspaceEntityContextInterface<T> { + variants: Observable<Array<VariantViewModelBaseModel>>; + + splitView: UmbWorkspaceSplitViewManager; + getName(variantId?: UmbVariantId): void; setName(name: string, variantId?: UmbVariantId): void; + getVariant(variantId: UmbVariantId): VariantViewModelBaseModel | undefined; + propertyDataByAlias(alias: string, variantId?: UmbVariantId): Observable<ValueViewModelBaseModel | undefined>; propertyValueByAlias(alias: string, variantId?: UmbVariantId): Observable<any | undefined>; getPropertyValue(alias: string, variantId?: UmbVariantId): void; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-footer-layout/workspace-footer-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-footer-layout/workspace-footer-layout.element.ts new file mode 100644 index 0000000000..6af6b320fa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-footer-layout/workspace-footer-layout.element.ts @@ -0,0 +1,76 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { css, html } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import type { ManifestWorkspaceAction } from '@umbraco-cms/models'; + +import { UmbLitElement } from '@umbraco-cms/element'; + +/** + * @element umb-workspace-footer-layout + * @description Uses the alias to insert extensions that targets this workspace-alias. + * @slot - Slot for workspace footer items + * @slot actions - Slot for workspace actions + * @export + * @class UmbWorkspaceFooterLayout + * @extends {UmbLitElement} + */ +// TODO: stop naming this something with layout. as its not just an layout. it hooks up with extensions. +@customElement('umb-workspace-footer-layout') +export class UmbWorkspaceFooterLayout extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + :host { + display: block; + width: 100%; + } + + umb-extension-slot[slot='actions'] { + display: flex; + gap: var(--uui-size-space-2); + } + `, + ]; + + private _alias = ''; + /** + * Alias of the workspace. The Layout will render the workspace actions that are registered for this workspace alias. + * @public + * @type {string} + * @attr + * @default '' + */ + @property() + public get alias() { + return this._alias; + } + public set alias(value) { + const oldValue = this._alias; + this._alias = value; + if (oldValue !== this._alias) { + this.requestUpdate('alias', oldValue); + } + } + + // TODO: Some event/callback from umb-extension-slot that can be utilized to hide the footer, if empty. + render() { + return html` + <umb-footer-layout> + <slot></slot> + <slot name="actions" slot="actions"></slot> + <umb-extension-slot + slot="actions" + type="workspaceAction" + .filter=${(extension: ManifestWorkspaceAction) => extension.meta.workspaces.includes(this.alias)} + default-element="umb-workspace-action"></umb-extension-slot> + <slot name="actions" slot="actions"></slot> + </umb-footer-layout> + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-workspace-footer-layout': UmbWorkspaceFooterLayout; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-footer-layout/workspace-footer-layout.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-footer-layout/workspace-footer-layout.stories.ts new file mode 100644 index 0000000000..b673c8b304 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-footer-layout/workspace-footer-layout.stories.ts @@ -0,0 +1,19 @@ +import '../workspace-layout/workspace-layout.element'; +import './workspace-footer-layout.element'; + +import { Meta, Story } from '@storybook/web-components'; +import { html } from 'lit-html'; + +import type { UmbWorkspaceFooterLayout } from './workspace-footer-layout.element'; + +export default { + title: 'Workspaces/Shared/Footer Layout', + component: 'umb-workspace-footer-layout', + id: 'umb-workspace-footer-layout', +} as Meta; + +export const AAAOverview: Story<UmbWorkspaceFooterLayout> = () => html` <umb-workspace-footer-layout> + <div><uui-button color="" look="placeholder">Footer slot</uui-button></div> + <div slot="actions"><uui-button color="" look="placeholder">Actions slot</uui-button></div> +</umb-workspace-footer-layout>`; +AAAOverview.storyName = 'Overview'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-layout/workspace-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-layout/workspace-layout.element.ts index 2dbd3b9e08..8abacffa60 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-layout/workspace-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-layout/workspace-layout.element.ts @@ -6,11 +6,7 @@ import { repeat } from 'lit/directives/repeat.js'; import type { UmbRouterSlotInitEvent, UmbRouterSlotChangeEvent, IRoutingInfo } from '@umbraco-cms/router'; import { createExtensionElement, umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; -import type { - ManifestWorkspaceAction, - ManifestWorkspaceView, - ManifestWorkspaceViewCollection, -} from '@umbraco-cms/models'; +import type { ManifestWorkspaceView, ManifestWorkspaceViewCollection } from '@umbraco-cms/models'; import '../../body-layout/body-layout.element'; import '../../extension-slot/extension-slot.element'; @@ -19,15 +15,16 @@ import { UmbLitElement } from '@umbraco-cms/element'; /** * @element umb-workspace-layout * @description - * @slot icon - Slot for rendering the icon - * @slot name - Slot for rendering the name - * @slot footer - Slot for rendering the workspace footer - * @slot actions - Slot for rendering the workspace actions + * @slot icon - Slot for icon + * @slot name - Slot for name + * @slot footer - Slot for workspace footer + * @slot actions - Slot for workspace footer actions * @slot default - slot for main content * @export * @class UmbWorkspaceLayout * @extends {UmbLitElement} */ +// TODO: stop naming this something with layout. as its not just an layout. it hooks up with extensions. @customElement('umb-workspace-layout') export class UmbWorkspaceLayout extends UmbLitElement { static styles = [ @@ -59,6 +56,12 @@ export class UmbWorkspaceLayout extends UmbLitElement { @property() public headline = ''; + @property() + public hideNavigation = false; + + @property() + public enforceNoFooter = false; + private _alias = ''; /** * Alias of the workspace. The Layout will render the workspace views that are registered for this workspace alias. @@ -130,10 +133,14 @@ export class UmbWorkspaceLayout extends UmbLitElement { }; }); - this._routes.push({ - path: '**', - redirectTo: `view/${this._workspaceViews[0].meta.pathname}`, - }); + // If we have a post fix then we need to add a direct from the empty url of the split-view-index: + const firstView = this._workspaceViews[0]; + if (firstView) { + this._routes.push({ + path: ``, + redirectTo: `view/${firstView.meta.pathname}`, + }); + } } } @@ -145,16 +152,21 @@ export class UmbWorkspaceLayout extends UmbLitElement { <slot name="action-menu" slot="action-menu"></slot> ${this.#renderRoutes()} <slot></slot> - <slot name="footer" slot="footer"></slot> - ${this.#renderWorkspaceActions()} - <slot name="actions" slot="actions"></slot> + ${this.enforceNoFooter + ? '' + : html` + <umb-workspace-footer-layout alias=${this.alias}> + <slot name="footer"></slot> + <slot name="actions" slot="actions"></slot> + </umb-workspace-footer-layout> + `} </umb-body-layout> `; } #renderViews() { return html` - ${this._workspaceViews.length > 1 + ${!this.hideNavigation && this._workspaceViews.length > 1 ? html` <uui-tab-group slot="tabs"> ${repeat( @@ -193,16 +205,6 @@ export class UmbWorkspaceLayout extends UmbLitElement { : nothing} `; } - - #renderWorkspaceActions() { - return html` - <umb-extension-slot - slot="actions" - type="workspaceAction" - .filter=${(extension: ManifestWorkspaceAction) => extension.meta.workspaces.includes(this.alias)} - default-element="umb-workspace-action"></umb-extension-slot> - `; - } } declare global { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-variant/workspace-variant.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-variant/workspace-variant.context.ts index cdc5b0be3e..9d2d06494d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-variant/workspace-variant.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-variant/workspace-variant.context.ts @@ -1,9 +1,8 @@ -import { - ActiveVariant, - UmbDocumentWorkspaceContext, -} from '../../../../documents/documents/workspace/document-workspace.context'; +import { UmbDocumentWorkspaceContext } from '../../../../documents/documents/workspace/document-workspace.context'; import { UmbVariantId } from '../../../variants/variant-id.class'; -import { UmbContextConsumerController, UmbContextProviderController } from '@umbraco-cms/context-api'; +import { UmbWorkspaceVariableEntityContextInterface } from '../workspace-context/workspace-variable-entity-context.interface'; +import { ActiveVariant } from '../workspace-context/workspace-split-view-manager.class'; +import { UmbContextConsumerController, UmbContextProviderController, UmbContextToken } from '@umbraco-cms/context-api'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { ClassState, NumberState, ObjectState, UmbObserverController } from '@umbraco-cms/observable-api'; import { DocumentVariantModel } from '@umbraco-cms/backend-api'; @@ -13,7 +12,10 @@ import { DocumentVariantModel } from '@umbraco-cms/backend-api'; export class UmbWorkspaceVariantContext { #host: UmbControllerHostInterface; - #workspaceContext?: UmbDocumentWorkspaceContext; + #workspaceContext?: UmbWorkspaceVariableEntityContextInterface; + public getWorkspaceContext() { + return this.#workspaceContext; + } #index = new NumberState(undefined); index = this.#index.asObservable(); @@ -33,7 +35,7 @@ export class UmbWorkspaceVariantContext { constructor(host: UmbControllerHostInterface) { this.#host = host; - new UmbContextProviderController(host, 'umbWorkspaceVariantContext', this); + new UmbContextProviderController(host, UMB_WORKSPACE_VARIANT_CONTEXT_TOKEN.toString(), this); // How do we ensure this connects to a document workspace context? and not just any other context? (We could start providing workspace contexts twice, under the general name and under a specific name) // TODO: Figure out if this is the best way to consume the context or if it can be strongly typed with an UmbContextToken @@ -47,8 +49,23 @@ export class UmbWorkspaceVariantContext { }); } - private _setVariantId(culture: string | null, segment: string | null) { - const variantId = UmbVariantId.Create(culture, segment); + public switchVariant(variant: DocumentVariantModel) { + const index = this.#index.value; + if (index === undefined) return; + this.#workspaceContext?.splitView.switchVariant(index, new UmbVariantId(variant)); + } + + public closeSplitView() { + const index = this.#index.value; + if (index === undefined) return; + this.#workspaceContext?.splitView.closeSplitView(index); + } + + public openSplitView(variant: DocumentVariantModel) { + this.#workspaceContext?.splitView.openSplitView(new UmbVariantId(variant)); + } + + private _setVariantId(variantId: UmbVariantId) { this.#variantId.next(variantId); return variantId; } @@ -62,10 +79,10 @@ export class UmbWorkspaceVariantContext { this._currentVariantObserver?.destroy(); this._currentVariantObserver = new UmbObserverController( this.#host, - this.#workspaceContext.activeVariantInfoByIndex(index), + this.#workspaceContext.splitView.activeVariantByIndex(index), async (activeVariantInfo) => { if (!activeVariantInfo) return; - const variantId = this._setVariantId(activeVariantInfo.culture, activeVariantInfo.segment); + const variantId = this._setVariantId(UmbVariantId.Create(activeVariantInfo)); const currentVariant = await this.#workspaceContext?.getVariant(variantId); this.#currentVariant.next(currentVariant); }, @@ -76,14 +93,12 @@ export class UmbWorkspaceVariantContext { public changeVariant(culture: string | null, segment: string | null) { const index = this.#index.getValue(); if (index === undefined) return; - this.#workspaceContext?.setActiveVariant(index, culture, segment); + this.#workspaceContext?.splitView.setActiveVariant(index, culture, segment); } - /* public getSplitViewIndex() { return this.#index.getValue(); } - */ public setSplitViewIndex(index: number) { this.#index.next(index); } @@ -105,3 +120,7 @@ export class UmbWorkspaceVariantContext { } */ } + +export const UMB_WORKSPACE_VARIANT_CONTEXT_TOKEN = new UmbContextToken<UmbWorkspaceVariantContext>( + 'umbWorkspaceVariantContext' +); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-variant/workspace-variant.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-variant/workspace-variant.element.ts index 50d5dbea12..1bf14943c6 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-variant/workspace-variant.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-variant/workspace-variant.element.ts @@ -1,6 +1,6 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; +import { customElement, property, state } from 'lit/decorators.js'; import '../workspace-layout/workspace-layout.element'; @@ -26,14 +26,14 @@ export class UmbWorkspaceVariantContentElement extends UmbLitElement { height: 100%; } + :host(:not(:last-child)) { + border-right: 1px solid var(--uui-color-border); + } + #header { margin: 0 var(--uui-size-layout-1); flex: 1 1 auto; } - - #footer { - margin: 0 var(--uui-size-layout-1); - } `, ]; @@ -41,24 +41,34 @@ export class UmbWorkspaceVariantContentElement extends UmbLitElement { @property() alias!: string; - // Use this for any sub url routing, or maybe we should use the culture + segment for this. + @property({ type: Boolean }) + displayNavigation = false; + @property({ type: Number }) public set splitViewIndex(index: number) { + this._splitViewIndex = index; this.variantContext.setSplitViewIndex(index); } + @state() + private _splitViewIndex = 0; + variantContext = new UmbWorkspaceVariantContext(this); render() { return html` - <umb-workspace-layout alias=${this.alias}> + <umb-workspace-layout + .splitViewIndex=${this._splitViewIndex.toString()} + alias=${this.alias} + .hideNavigation=${!this.displayNavigation} + .enforceNoFooter=${true}> <div id="header" slot="header"> <umb-variant-selector></umb-variant-selector> </div> - + ${this.displayNavigation + ? html`<umb-workspace-action-menu slot="action-menu"></umb-workspace-action-menu>` + : ''} <slot name="action-menu" slot="action-menu"></slot> - - <div id="footer" slot="footer">Breadcrumbs</div> </umb-workspace-layout> `; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/index.ts index c2b8157f67..f308f04e5b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/index.ts @@ -1,23 +1,22 @@ -import './components'; - +import { manifests as componentManifests } from './components'; import { manifests as propertyActionManifests } from './property-actions/manifests'; -import { manifests as propertyEditorModelManifests } from './property-editors/models/manifests'; -import { manifests as propertyEditorUIManifests } from './property-editors/uis/manifests'; +import { manifests as propertyEditorManifests } from './property-editors/manifests'; import { manifests as collectionViewManifests } from './collection/views/manifests'; +import { manifests as modalManifests } from './modals/manifests'; import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; import { ManifestTypes } from '@umbraco-cms/extensions-registry'; +export const manifests = [ + ...componentManifests, + ...propertyActionManifests, + ...propertyEditorManifests, + ...collectionViewManifests, + ...modalManifests, +]; + const registerExtensions = (manifests: Array<ManifestTypes>) => { - manifests.forEach((manifest) => { - if (umbExtensionsRegistry.isRegistered(manifest.alias)) return; - umbExtensionsRegistry.register(manifest); - }); + manifests.forEach((manifest) => umbExtensionsRegistry.register(manifest)); }; -registerExtensions([ - ...propertyActionManifests, - ...propertyEditorModelManifests, - ...propertyEditorUIManifests, - ...collectionViewManifests, -]); +registerExtensions(manifests); diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/confirm/modal-layout-confirm.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/confirm/confirm-modal.element.ts similarity index 51% rename from src/Umbraco.Web.UI.Client/src/core/modal/layouts/confirm/modal-layout-confirm.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/confirm/confirm-modal.element.ts index cdeab181b7..fb8f4cc781 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/confirm/modal-layout-confirm.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/confirm/confirm-modal.element.ts @@ -1,25 +1,26 @@ -import { html, TemplateResult } from 'lit'; +import { html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { customElement } from 'lit/decorators.js'; -import { UmbModalLayoutElement } from '../modal-layout.element'; +import { customElement, property } from 'lit/decorators.js'; +import { UmbConfirmModalData, UmbConfirmModalResult } from '.'; +import { UmbLitElement } from '@umbraco-cms/element'; +import { UmbModalHandler } from '@umbraco-cms/modal'; -export interface UmbModalConfirmData { - headline: string; - content: TemplateResult | string; - color?: 'positive' | 'danger'; - confirmLabel?: string; -} - -@customElement('umb-modal-layout-confirm') -export class UmbModalLayoutConfirmElement extends UmbModalLayoutElement<UmbModalConfirmData> { +@customElement('umb-confirm-modal') +export class UmbConfirmModalElement extends UmbLitElement { static styles = [UUITextStyles]; + @property({ attribute: false }) + modalHandler?: UmbModalHandler<UmbConfirmModalData, UmbConfirmModalResult>; + + @property({ type: Object }) + data?: UmbConfirmModalData; + private _handleConfirm() { - this.modalHandler?.close({ confirmed: true }); + this.modalHandler?.submit(); } private _handleCancel() { - this.modalHandler?.close({ confirmed: false }); + this.modalHandler?.reject(); } render() { @@ -40,8 +41,10 @@ export class UmbModalLayoutConfirmElement extends UmbModalLayoutElement<UmbModal } } +export default UmbConfirmModalElement; + declare global { interface HTMLElementTagNameMap { - 'umb-modal-layout-confirm': UmbModalLayoutConfirmElement; + 'umb-confirm-modal': UmbConfirmModalElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/confirm/modal-layout-confirm.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/confirm/confirm-modal.stories.ts similarity index 57% rename from src/Umbraco.Web.UI.Client/src/core/modal/layouts/confirm/modal-layout-confirm.stories.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/confirm/confirm-modal.stories.ts index 93d8ba7c2a..8a33def986 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/confirm/modal-layout-confirm.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/confirm/confirm-modal.stories.ts @@ -1,40 +1,41 @@ -import './modal-layout-confirm.element'; +import './confirm-modal.element'; import { Meta, Story } from '@storybook/web-components'; import { html } from 'lit'; -import type { UmbModalLayoutConfirmElement, UmbModalConfirmData } from './modal-layout-confirm.element'; +import type { UmbConfirmModalElement } from './confirm-modal.element'; +import type { UmbConfirmModalData } from './'; export default { title: 'API/Modals/Layouts/Confirm', - component: 'umb-modal-layout-confirm', - id: 'modal-layout-confirm', + component: 'umb-confirm-modal', + id: 'umb-confirm-modal', } as Meta; -const positiveData: UmbModalConfirmData = { +const positiveData: UmbConfirmModalData = { headline: 'Publish with descendants', content: html`Publish <b>This example</b> and all content items underneath and thereby making their content publicly available.`, confirmLabel: 'Publish', }; -export const Positive: Story<UmbModalLayoutConfirmElement> = () => html` +export const Positive: Story<UmbConfirmModalElement> = () => html` <!-- TODO: figure out if generics are allowed for properties: https://github.com/runem/lit-analyzer/issues/149 https://github.com/runem/lit-analyzer/issues/163 --> - <umb-modal-layout-confirm .data=${positiveData as any}></umb-modal-layout-confirm> + <umb-confirm-modal .data=${positiveData as any}></umb-confirm-modal> `; -const dangerData: UmbModalConfirmData = { +const dangerData: UmbConfirmModalData = { color: 'danger', headline: 'Delete', content: html`Delete <b>This example</b> and all items underneath.`, confirmLabel: 'Delete', }; -export const Danger: Story<UmbModalLayoutConfirmElement> = () => html` +export const Danger: Story<UmbConfirmModalElement> = () => html` <!-- TODO: figure out if generics are allowed for properties: https://github.com/runem/lit-analyzer/issues/149 https://github.com/runem/lit-analyzer/issues/163 --> - <umb-modal-layout-confirm .data=${dangerData as any}></umb-modal-layout-confirm> + <umb-confirm-modal .data=${dangerData as any}></umb-confirm-modal> `; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/confirm/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/confirm/index.ts new file mode 100644 index 0000000000..eb7a773cc3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/confirm/index.ts @@ -0,0 +1,18 @@ +import type { TemplateResult } from 'lit'; +import { UmbModalToken } from '@umbraco-cms/modal'; + +export interface UmbConfirmModalData { + headline: string; + content: TemplateResult | string; + color?: 'positive' | 'danger'; + confirmLabel?: string; +} + +export type UmbConfirmModalResult = undefined; + +export const UMB_CONFIRM_MODAL_TOKEN = new UmbModalToken<UmbConfirmModalData, UmbConfirmModalResult>( + 'Umb.Modal.Confirm', + { + type: 'dialog', + } +); diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/icon-picker/modal-layout-icon-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/icon-picker-modal.element.ts similarity index 88% rename from src/Umbraco.Web.UI.Client/src/core/modal/layouts/icon-picker/modal-layout-icon-picker.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/icon-picker-modal.element.ts index 1fbfcfba62..a7de5856ed 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/icon-picker/modal-layout-icon-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/icon-picker-modal.element.ts @@ -4,18 +4,15 @@ import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; -import { UmbModalLayoutElement } from '../modal-layout.element'; import icons from '../../../../../public-assets/icons/icons.json'; - -export interface UmbModalIconPickerData { - multiple: boolean; - selection: string[]; -} +import { UmbIconPickerModalData, UmbIconPickerModalResult } from '.'; +import { UmbModalBaseElement } from '@umbraco-cms/modal'; // TODO: Make use of UmbPickerLayoutBase -@customElement('umb-modal-layout-icon-picker') -export class UmbModalLayoutIconPickerElement extends UmbModalLayoutElement<UmbModalIconPickerData> { +// TODO: to prevent element extension we need to move the Picker logic into a separate class we can reuse across all pickers +@customElement('umb-icon-picker-modal') +export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPickerModalData, UmbIconPickerModalResult> { static styles = [ UUITextStyles, css` @@ -137,11 +134,11 @@ export class UmbModalLayoutIconPickerElement extends UmbModalLayoutElement<UmbMo } private _close() { - this.modalHandler?.close(); + this.modalHandler?.reject(); } private _save() { - this.modalHandler?.close({ color: this._currentColor, icon: this._currentIcon }); + this.modalHandler?.submit({ color: this._currentColor, icon: this._currentIcon }); } private _onColorChange(e: UUIColorSwatchesEvent) { @@ -210,10 +207,10 @@ export class UmbModalLayoutIconPickerElement extends UmbModalLayoutElement<UmbMo } } -export default UmbModalLayoutIconPickerElement; +export default UmbIconPickerModalElement; declare global { interface HTMLElementTagNameMap { - 'umb-modal-layout-icon-picker': UmbModalLayoutIconPickerElement; + 'umb-icon-picker-modal': UmbIconPickerModalElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/icon-picker-modal.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/icon-picker-modal.stories.ts new file mode 100644 index 0000000000..2facbfb42a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/icon-picker-modal.stories.ts @@ -0,0 +1,26 @@ +import '../../components/body-layout/body-layout.element'; +import './icon-picker-modal.element'; + +import { Meta, Story } from '@storybook/web-components'; +import { html } from 'lit'; + +import type { UmbIconPickerModalElement } from './icon-picker-modal.element'; +import { UmbIconPickerModalData } from '.'; + +export default { + title: 'API/Modals/Layouts/Icon Picker', + component: 'umb-icon-picker-modal', + id: 'umb-icon-picker-modal', +} as Meta; + +const data: UmbIconPickerModalData = { + multiple: true, + selection: [], +}; + +export const Overview: Story<UmbIconPickerModalElement> = () => html` + <!-- TODO: figure out if generics are allowed for properties: + https://github.com/runem/lit-analyzer/issues/149 + https://github.com/runem/lit-analyzer/issues/163 --> + <umb-icon-picker-modal .data=${data as any}></umb-icon-picker-modal> +`; diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/icon-picker/modal-layout-icon-picker.test.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/icon-picker-modal.test.ts similarity index 54% rename from src/Umbraco.Web.UI.Client/src/core/modal/layouts/icon-picker/modal-layout-icon-picker.test.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/icon-picker-modal.test.ts index 1fedae1a79..6831f4a0c9 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/icon-picker/modal-layout-icon-picker.test.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/icon-picker-modal.test.ts @@ -1,16 +1,16 @@ import { expect, fixture, html } from '@open-wc/testing'; -import { UmbModalLayoutIconPickerElement } from './modal-layout-icon-picker.element'; +import { UmbIconPickerModalElement } from './icon-picker-modal.element'; import { defaultA11yConfig } from '@umbraco-cms/test-utils'; -describe('umb-modal-layout-icon-picker', () => { - let element: UmbModalLayoutIconPickerElement; +describe('umb-icon-picker-modal', () => { + let element: UmbIconPickerModalElement; beforeEach(async () => { - element = await fixture(html` <umb-modal-layout-icon-picker></umb-modal-layout-icon-picker> `); + element = await fixture(html` <umb-icon-picker-modal></umb-icon-picker-modal> `); }); it('is defined with its own instance', () => { - expect(element).to.be.instanceOf(UmbModalLayoutIconPickerElement); + expect(element).to.be.instanceOf(UmbIconPickerModalElement); }); // TODO: Reinstate this test when the a11y audit is fixed on uui-color-picker diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/index.ts new file mode 100644 index 0000000000..158a3a6a19 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/index.ts @@ -0,0 +1,19 @@ +import { UmbModalToken } from '@umbraco-cms/modal'; + +export interface UmbIconPickerModalData { + multiple: boolean; + selection: string[]; +} + +export interface UmbIconPickerModalResult { + color: string | undefined; + icon: string | undefined; +} + +export const UMB_ICON_PICKER_MODAL_TOKEN = new UmbModalToken<UmbIconPickerModalData, UmbIconPickerModalResult>( + 'Umb.Modal.IconPicker', + { + type: 'sidebar', + size: 'small', + } +); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/link-picker/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/link-picker/index.ts new file mode 100644 index 0000000000..ecbd202e43 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/link-picker/index.ts @@ -0,0 +1,35 @@ +import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar'; +import { UmbModalToken } from '@umbraco-cms/modal'; + +export interface UmbLinkPickerModalData { + link: UmbLinkPickerLink; + config: UmbLinkPickerConfig; +} + +export type UmbLinkPickerModalResult = UmbLinkPickerLink; + +export interface UmbLinkPickerLink { + icon?: string | null; + name?: string | null; + published?: boolean | null; + queryString?: string | null; + target?: string | null; + trashed?: boolean | null; + udi?: string | null; + url?: string | null; +} + +// TODO: investigate: this looks more like a property editor configuration. Is this used in the correct way? +export interface UmbLinkPickerConfig { + hideAnchor?: boolean; + ignoreUserStartNodes?: boolean; + overlaySize?: UUIModalSidebarSize; +} + +export const UMB_LINK_PICKER_MODAL_TOKEN = new UmbModalToken<UmbLinkPickerModalData, UmbLinkPickerModalResult>( + 'Umb.Modal.LinkPicker', + { + type: 'sidebar', + size: 'small', + } +); diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/link-picker/modal-layout-link-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/link-picker/link-picker-modal.element.ts similarity index 82% rename from src/Umbraco.Web.UI.Client/src/core/modal/layouts/link-picker/modal-layout-link-picker.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/link-picker/link-picker-modal.element.ts index a67c8fd4d6..73b31ccc82 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/link-picker/modal-layout-link-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/link-picker/link-picker-modal.element.ts @@ -2,35 +2,13 @@ import { css, html, nothing } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, query, state } from 'lit/decorators.js'; import { UUIBooleanInputEvent, UUIInputElement } from '@umbraco-ui/uui'; -import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar'; -import { UmbModalLayoutElement } from '../modal-layout.element'; -import { UmbTreeElement } from '../../../../backoffice/shared/components/tree/tree.element'; +import { UmbTreeElement } from '../../components/tree/tree.element'; +import { UmbLinkPickerConfig, UmbLinkPickerLink, UmbLinkPickerModalData, UmbLinkPickerModalResult } from '.'; +import { UmbModalBaseElement } from '@umbraco-cms/modal'; import { buildUdi, getKeyFromUdi } from '@umbraco-cms/utils'; -export interface UmbModalLinkPickerData { - link: LinkPickerData; - config: LinkPickerConfig; -} - -export interface LinkPickerData { - icon?: string | null; - name?: string | null; - published?: boolean | null; - queryString?: string | null; - target?: string | null; - trashed?: boolean | null; - udi?: string | null; - url?: string | null; -} - -export interface LinkPickerConfig { - hideAnchor?: boolean; - ignoreUserStartNodes?: boolean; - overlaySize?: UUIModalSidebarSize; -} - -@customElement('umb-modal-layout-link-picker') -export class UmbModalLayoutLinkPickerElement extends UmbModalLayoutElement<UmbModalLinkPickerData> { +@customElement('umb-link-picker-modal') +export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPickerModalData, UmbLinkPickerModalResult> { static styles = [ UUITextStyles, css` @@ -69,7 +47,7 @@ export class UmbModalLayoutLinkPickerElement extends UmbModalLayoutElement<UmbMo _selectedKey?: string; @state() - _link: LinkPickerData = { + _link: UmbLinkPickerLink = { icon: null, name: null, published: true, @@ -81,7 +59,7 @@ export class UmbModalLayoutLinkPickerElement extends UmbModalLayoutElement<UmbMo }; @state() - _layout: LinkPickerConfig = { + _layout: UmbLinkPickerConfig = { hideAnchor: false, ignoreUserStartNodes: false, }; @@ -127,11 +105,11 @@ export class UmbModalLayoutLinkPickerElement extends UmbModalLayoutElement<UmbMo } private _submit() { - this.modalHandler?.close(this._link); + this.modalHandler?.submit(this._link); } private _close() { - this.modalHandler?.close(); + this.modalHandler?.reject(); } render() { @@ -217,8 +195,10 @@ export class UmbModalLayoutLinkPickerElement extends UmbModalLayoutElement<UmbMo } } +export default UmbLinkPickerModalElement; + declare global { interface HTMLElementTagNameMap { - 'umb-modal-layout-link-picker': UmbModalLayoutLinkPickerElement; + 'umb-link-picker-modal': UmbLinkPickerModalElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/link-picker/link-picker-modal.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/link-picker/link-picker-modal.stories.ts new file mode 100644 index 0000000000..6408f338a7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/link-picker/link-picker-modal.stories.ts @@ -0,0 +1,20 @@ +import '../../components/body-layout/body-layout.element'; +import './link-picker-modal.element'; + +import { Meta, Story } from '@storybook/web-components'; +import { html } from 'lit'; + +import type { UmbLinkPickerModalElement } from './link-picker-modal.element'; + +export default { + title: 'API/Modals/Layouts/Link Picker', + component: 'umb-link-picker-modal', + id: 'umb-link-picker-modal', +} as Meta; + +export const Overview: Story<UmbLinkPickerModalElement> = () => html` + <!-- TODO: figure out if generics are allowed for properties: + https://github.com/runem/lit-analyzer/issues/149 + https://github.com/runem/lit-analyzer/issues/163 --> + <umb-link-picker-modal></umb-link-picker-modal> +`; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/manifests.ts new file mode 100644 index 0000000000..f2f6a9f749 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/manifests.ts @@ -0,0 +1,36 @@ +import type { ManifestModal } from '@umbraco-cms/extensions-registry'; + +const modals: Array<ManifestModal> = [ + { + type: 'modal', + alias: 'Umb.Modal.Confirm', + name: 'Confirm Modal', + loader: () => import('./confirm/confirm-modal.element'), + }, + { + type: 'modal', + alias: 'Umb.Modal.IconPicker', + name: 'Icon Picker Modal', + loader: () => import('./icon-picker/icon-picker-modal.element'), + }, + { + type: 'modal', + alias: 'Umb.Modal.LinkPicker', + name: 'Link Picker Modal', + loader: () => import('./link-picker/link-picker-modal.element'), + }, + { + type: 'modal', + alias: 'Umb.Modal.PropertySettings', + name: 'Property Settings Modal', + loader: () => import('./property-settings/property-settings-modal.element'), + }, + { + type: 'modal', + alias: 'Umb.Modal.SectionPicker', + name: 'Section Picker Modal', + loader: () => import('./section-picker/section-picker-modal.element'), + }, +]; + +export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/property-settings/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/property-settings/index.ts new file mode 100644 index 0000000000..c3719e1789 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/property-settings/index.ts @@ -0,0 +1,26 @@ +import { UmbModalToken } from '@umbraco-cms/modal'; + +// TODO: add interface for data +// PropertyTypeViewModelBaseModel + +export interface UmbPropertySettingsModalResult { + label: string; + alias: string; + description: string; + propertyEditorUI?: string; + labelOnTop: boolean; + validation: { + mandatory: boolean; + mandatoryMessage: string; + pattern: string; + patternMessage: string; + }; +} + +export const UMB_PROPERTY_SETTINGS_MODAL_TOKEN = new UmbModalToken<undefined, UmbPropertySettingsModalResult>( + 'Umb.Modal.PropertySettings', + { + type: 'sidebar', + size: 'small', + } +); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/property-settings/property-settings-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/property-settings/property-settings-modal.element.ts new file mode 100644 index 0000000000..9aa4b3f305 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/property-settings/property-settings-modal.element.ts @@ -0,0 +1,435 @@ +import { UUIBooleanInputEvent, UUIInputEvent, UUISelectEvent } from '@umbraco-ui/uui'; +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html, nothing } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { UMB_PROPERTY_EDITOR_UI_PICKER_MODAL_TOKEN } from '../../property-editors/modals/property-editor-ui-picker'; +import { UmbPropertySettingsModalResult } from '.'; +import { UmbModalContext, UmbModalBaseElement, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; +import { ManifestPropertyEditorUI } from '@umbraco-cms/extensions-registry'; +import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; + +@customElement('umb-property-settings-modal') +export class UmbPropertySettingsModalElement extends UmbModalBaseElement<undefined, UmbPropertySettingsModalResult> { + static styles = [ + UUITextStyles, + css` + :host { + color: var(--uui-color-text); + } + #content { + padding: var(--uui-size-space-6); + } + #appearances { + display: flex; + gap: var(--uui-size-space-6); + max-width: 350px; + margin: 0 auto; + } + .appearance { + position: relative; + display: flex; + border: 2px solid var(--uui-color-border-standalone); + padding: var(--uui-size-space-4) var(--uui-size-space-5); + align-items: center; + border-radius: 6px; + opacity: 0.8; + flex-direction: column; + justify-content: space-between; + gap: var(--uui-size-space-3); + } + .appearance-label { + font-size: 0.8rem; + line-height: 1; + } + .appearance.selected .appearance-label { + font-weight: bold; + } + .appearance:not(.selected):hover { + border-color: var(--uui-color-border-emphasis); + cursor: pointer; + opacity: 1; + } + .appearance.selected { + border-color: var(--uui-color-selected); + opacity: 1; + } + .appearance.selected::after { + content: ''; + position: absolute; + inset: 0; + border-radius: 6px; + opacity: 0.1; + background-color: var(--uui-color-selected); + } + .appearance.left { + flex-grow: 1; + } + .appearance.top { + flex-shrink: 1; + } + .appearance svg { + display: flex; + width: 100%; + color: var(--uui-color-text); + } + hr { + border: none; + border-top: 1px solid var(--uui-color-divider); + margin-top: var(--uui-size-space-6); + margin-bottom: var(--uui-size-space-5); + } + uui-input { + width: 100%; + } + #alias-lock { + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + } + #alias-lock uui-icon { + margin-bottom: 2px; + } + #property-editor-ui-picker { + width: 100%; + --uui-button-padding-top-factor: 4; + --uui-button-padding-bottom-factor: 4; + } + .container { + display: flex; + flex-direction: column; + } + uui-form, + form { + display: block; + height: 100%; + } + `, + ]; + + @state() private _selectedPropertyEditorUI?: ManifestPropertyEditorUI; + @state() private _selectedPropertyEditorUIAlias = ''; + + @state() private _appearanceIsTop = false; + @state() private _mandatory = false; + + //TODO: Should these options come from the server? + @state() private _customValidationOptions = [ + { + name: 'No validation', + value: 'no-validation', + selected: true, + }, + { + name: 'Validate as an email address', + value: 'email', + validation: '[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+', + }, + { + name: 'Validate as a number', + value: 'number', + validation: '^[0-9]*$', + }, + { + name: 'Validate as an URL', + value: 'url', + validation: 'https?://[a-zA-Z0-9-.]+.[a-zA-Z]{2,}', + }, + { + name: '...or enter a custom validation', + value: 'custom', + }, + ]; + @state() private _customValidation = this._customValidationOptions[0]; + + @state() private _aliasLocked = true; + @state() private _name = ''; + @state() private _alias = ''; + + #modalContext?: UmbModalContext; + + constructor() { + super(); + + this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => { + this.#modalContext = instance; + }); + + this.#observePropertyEditorUI(); + } + + #observePropertyEditorUI() { + if (!this._selectedPropertyEditorUIAlias) return; + + this.observe( + umbExtensionsRegistry.getByTypeAndAlias('propertyEditorUI', this._selectedPropertyEditorUIAlias), + (propertyEditorUI) => { + if (!propertyEditorUI) return; + + this._selectedPropertyEditorUI = propertyEditorUI; + } + ); + } + + #onCustomValidationChange(event: UUISelectEvent) { + const value = event.target.value; + + this._customValidation = + this._customValidationOptions.find((option) => option.value === value) ?? this._customValidationOptions[0]; + } + + #onMandatoryChange(event: UUIBooleanInputEvent) { + const value = event.target.checked; + this._mandatory = value; + } + + #onClose() { + this.modalHandler?.reject(); + } + + #onSubmit(event: SubmitEvent) { + event.preventDefault(); + + const form = event.target as HTMLFormElement; + if (!form) return; + + const isValid = form.checkValidity(); + if (!isValid) return; + + const formData = new FormData(form); + + const label = this._name || ''; + const alias = this._alias || ''; + const description = formData.get('description')?.toString() || ''; + const propertyEditorUI = this._selectedPropertyEditorUIAlias || undefined; + const labelOnTop = this._appearanceIsTop; + const mandatory = this._mandatory; + const mandatoryMessage = formData.get('mandatory-message')?.toString() || ''; + const pattern = formData.get('pattern')?.toString() || ''; + const patternMessage = formData.get('pattern-message')?.toString() || ''; + + this.modalHandler?.submit({ + label, + alias, + description, + propertyEditorUI, + labelOnTop, + validation: { + mandatory, + mandatoryMessage, + pattern, + patternMessage, + }, + }); + } + + #onNameChange(event: UUIInputEvent) { + //TODO: Generate alias + this._name = event.target.value.toString(); + if (this._aliasLocked) { + this._alias = this.#generateAlias(this._name); + } + } + + // TODO: move this to a helper so we can reuse it across the app + #generateAlias(text: string) { + //replace all spaces characters with a dash and remove all non-alphanumeric characters, except underscore. Allow a maximum of 1 dashes or underscores in a row. + return text + .replace(/\s+/g, '-') + .replace(/[^a-zA-Z0-9_-]+/g, '') + .replace(/[-_]{2,}/g, (match) => match[0]) + .toLowerCase(); + } + + #onAliasChange(event: UUIInputEvent) { + const alias = this.#generateAlias(event.target.value.toString()); + if (!this._aliasLocked) { + this._alias = alias; + } else { + event.target.value = this._alias; + } + } + + #onAppearanceChange(event: MouseEvent) { + const target = event.target as HTMLElement; + const alreadySelected = target.classList.contains(this._appearanceIsTop ? 'top' : 'left'); + + if (alreadySelected) return; + + this._appearanceIsTop = !this._appearanceIsTop; + } + + #onOpenPropertyEditorUIPicker() { + const modalHandler = this.#modalContext?.open(UMB_PROPERTY_EDITOR_UI_PICKER_MODAL_TOKEN, { + selection: [], + }); + + if (!modalHandler) return; + + modalHandler?.onSubmit().then(({ selection }) => { + if (selection.length === 0) return; + // TODO: we might should set the alias to null or empty string, if no selection. + this._selectedPropertyEditorUIAlias = selection[0]; + this.#observePropertyEditorUI(); + }); + } + + /* TODO: + From Github comment: We should not re-generate the alias when it gets locked again. + Generally the auto generation is not determined by the lock, but wether it has been changed or saved. + The experience in existing backoffice is: we only generate an alias when a property is new, once it has been saved it should never change unless the user actively does so. + On new properties, the alias auto-generates until the user has made a change to it. */ + #onToggleAliasLock() { + this._aliasLocked = !this._aliasLocked; + + if (this._aliasLocked) { + this._alias = this.#generateAlias(this._name); + } + } + + render() { + return html` + <uui-form> + <form @submit="${this.#onSubmit}"> + <umb-workspace-layout headline="Property settings"> + <div id="content"> + <uui-box> + <div class="container"> + <uui-input + name="name" + @input=${this.#onNameChange} + .value=${this._name} + placeholder="Enter a name..."> + </uui-input> + <uui-input + name="alias" + @input=${this.#onAliasChange} + .value=${this._alias} + placeholder="Enter alias..." + ?disabled=${this._aliasLocked}> + <div @click=${this.#onToggleAliasLock} @keydown=${() => ''} id="alias-lock" slot="prepend"> + <uui-icon name=${this._aliasLocked ? 'umb:lock' : 'umb:unlocked'}></uui-icon> + </div> + </uui-input> + <uui-textarea name="description" placeholder="Enter description..."></uui-textarea> + </div> + ${this.#renderPropertyUIPicker()} + <hr /> + <div class="container"> + <b>Validation</b> + ${this.#renderMandatory()} + <p style="margin-bottom: 0">Custom validation</p> + ${this.#renderCustomValidation()} + </div> + <hr /> + <div class="container"> + <b style="margin-bottom: var(--uui-size-space-3)">Appearance</b> + <div id="appearances">${this.#renderAlignLeftIcon()} ${this.#renderAlignTopIcon()}</div> + </div> + </uui-box> + </div> + <div slot="actions"> + <uui-button label="Close" @click=${this.#onClose}></uui-button> + <uui-button label="Submit" look="primary" color="positive" type="submit"></uui-button> + </div> + </umb-workspace-layout> + </form> + </uui-form> + `; + } + + #renderAlignLeftIcon() { + return html`<div + @click=${this.#onAppearanceChange} + @keydown=${() => ''} + class="appearance left ${this._appearanceIsTop ? '' : 'selected'}"> + <svg width="260" height="32" viewBox="0 0 260 60" fill="none" xmlns="http://www.w3.org/2000/svg"> + <rect width="89" height="14" rx="7" fill="currentColor" /> + <rect x="121" width="139" height="10" rx="5" fill="currentColor" fill-opacity="0.4" /> + <rect x="121" y="46" width="108" height="10" rx="5" fill="currentColor" fill-opacity="0.4" /> + <rect x="121" y="23" width="139" height="10" rx="5" fill="currentColor" fill-opacity="0.4" /> + </svg> + <label class="appearance-label"> Label on the left </label> + </div>`; + } + + #renderAlignTopIcon() { + return html` + <div + @click=${this.#onAppearanceChange} + @keydown=${() => ''} + class="appearance top ${this._appearanceIsTop ? 'selected' : ''}"> + <svg width="139" height="48" viewBox="0 0 139 90" fill="none" xmlns="http://www.w3.org/2000/svg"> + <rect width="89" height="14" rx="7" fill="currentColor" /> + <rect y="30" width="139" height="10" rx="5" fill="currentColor" fill-opacity="0.4" /> + <rect y="76" width="108" height="10" rx="5" fill="currentColor" fill-opacity="0.4" /> + <rect y="53" width="139" height="10" rx="5" fill="currentColor" fill-opacity="0.4" /> + </svg> + <label class="appearance-label"> Label on top </label> + </div> + `; + } + + #renderMandatory() { + return html`<div style="display: flex; justify-content: space-between"> + <label for="mandatory">Field is mandatory</label> + <uui-toggle @change=${this.#onMandatoryChange} id="mandatory" slot="editor"></uui-toggle> + </div> + ${this._mandatory + ? html`<uui-input + name="mandatory-message" + style="margin-top: var(--uui-size-space-1)" + id="mandatory-message" + placeholder="Enter a custom validation error message (optional)"></uui-input>` + : ''}`; + } + + #renderPropertyUIPicker() { + return this._selectedPropertyEditorUI + ? html` + <umb-ref-property-editor-ui + name=${this._selectedPropertyEditorUI.meta.label} + alias=${this._selectedPropertyEditorUI.alias} + property-editor-model-alias=${this._selectedPropertyEditorUI.meta.propertyEditorModel} + border> + <uui-icon name="${this._selectedPropertyEditorUI.meta.icon}" slot="icon"></uui-icon> + <uui-action-bar slot="actions"> + <uui-button label="Change" @click=${this.#onOpenPropertyEditorUIPicker}></uui-button> + </uui-action-bar> + </umb-ref-property-editor-ui> + ` + : html` + <uui-button + id="property-editor-ui-picker" + label="Select Property Editor" + look="placeholder" + color="default" + @click=${this.#onOpenPropertyEditorUIPicker}></uui-button> + `; + } + + #renderCustomValidation() { + return html`<uui-select + style="margin-top: var(--uui-size-space-1)" + @change=${this.#onCustomValidationChange} + .options=${this._customValidationOptions}></uui-select> + + ${this._customValidation.value !== 'no-validation' + ? html` + <uui-input + name="pattern" + style="margin-bottom: var(--uui-size-space-1); margin-top: var(--uui-size-space-5);" + value=${this._customValidation.validation ?? ''}></uui-input> + <uui-textarea name="pattern-message"></uui-textarea> + ` + : nothing} `; + } +} + +export default UmbPropertySettingsModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-property-settings-modal': UmbPropertySettingsModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/section-picker/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/section-picker/index.ts new file mode 100644 index 0000000000..a7dd0eac98 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/section-picker/index.ts @@ -0,0 +1,11 @@ +import { UmbModalToken } from '@umbraco-cms/modal'; + +export interface UmbSectionPickerModalData { + multiple: boolean; + selection: string[]; +} + +export const UMB_SECTION_PICKER_MODAL_TOKEN = new UmbModalToken<UmbSectionPickerModalData>('Umb.Modal.SectionPicker', { + type: 'sidebar', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-section/picker-layout-section.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/section-picker/section-picker-modal.element.ts similarity index 89% rename from src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-section/picker-layout-section.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/section-picker/section-picker-modal.element.ts index ff0470d6c7..6bef6233da 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-section/picker-layout-section.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/section-picker/section-picker-modal.element.ts @@ -1,12 +1,12 @@ import { UUITextStyles } from '@umbraco-ui/uui-css'; import { css, html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; -import { UmbModalLayoutPickerBase } from '../modal-layout-picker-base'; +import { UmbModalElementPickerBase } from '@umbraco-cms/modal'; import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; import type { ManifestSection } from '@umbraco-cms/models'; -@customElement('umb-picker-layout-section') -export class UmbPickerLayoutSectionElement extends UmbModalLayoutPickerBase<ManifestSection> { +@customElement('umb-section-picker-modal') +export class UmbSectionPickerModalElement extends UmbModalElementPickerBase<ManifestSection> { static styles = [ UUITextStyles, css` @@ -91,8 +91,10 @@ export class UmbPickerLayoutSectionElement extends UmbModalLayoutPickerBase<Mani } } +export default UmbSectionPickerModalElement; + declare global { interface HTMLElementTagNameMap { - 'umb-picker-layout-section': UmbPickerLayoutSectionElement; + 'umb-section-picker-modal': UmbSectionPickerModalElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-section/picker-layout-section.test.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/section-picker/section-picker.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-section/picker-layout-section.test.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/section-picker/section-picker.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-creator/property-creator.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-creator/property-creator.element.ts new file mode 100644 index 0000000000..dacc3558f0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-creator/property-creator.element.ts @@ -0,0 +1,42 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { UMB_PROPERTY_SETTINGS_MODAL_TOKEN } from '../modals/property-settings'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; +import { UmbLitElement } from '@umbraco-cms/element'; + +@customElement('umb-property-creator') +export class UmbPropertyCreatorElement extends UmbLitElement { + static styles = [UUITextStyles, css``]; + + #modalContext?: UmbModalContext; + + constructor() { + super(); + + this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => (this.#modalContext = instance)); + } + + #onAddProperty() { + const modalHandler = this.#modalContext?.open(UMB_PROPERTY_SETTINGS_MODAL_TOKEN); + + modalHandler?.onSubmit().then((result) => { + console.log('result', result); + }); + } + + render() { + return html` + <div>added properties goes here:</div> + <uui-button look="outline" @click=${this.#onAddProperty}> Add property </uui-button> + `; + } +} + +export default UmbPropertyCreatorElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-property-creator': UmbPropertyCreatorElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/manifests.ts new file mode 100644 index 0000000000..38777fdc5c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/manifests.ts @@ -0,0 +1,5 @@ +import { manifests as propertyEditorModelManifests } from './models/manifests'; +import { manifests as propertyEditorUIManifests } from './uis/manifests'; +import { manifests as modalManifests } from './modals/manifests'; + +export const manifests = [...propertyEditorModelManifests, ...propertyEditorUIManifests, ...modalManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/modals/manifests.ts new file mode 100644 index 0000000000..34fdaa64db --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/modals/manifests.ts @@ -0,0 +1,12 @@ +import type { ManifestModal } from '@umbraco-cms/extensions-registry'; + +const modals: Array<ManifestModal> = [ + { + type: 'modal', + alias: 'Umb.Modal.PropertyEditorUIPicker', + name: 'Property Editor UI Picker Modal', + loader: () => import('./property-editor-ui-picker/property-editor-ui-picker-modal.element'), + }, +]; + +export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/modals/property-editor-ui-picker/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/modals/property-editor-ui-picker/index.ts new file mode 100644 index 0000000000..74d4587f2e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/modals/property-editor-ui-picker/index.ts @@ -0,0 +1,18 @@ +import { UmbModalToken } from '@umbraco-cms/modal'; + +export interface UmbPropertyEditorUIPickerModalData { + selection?: Array<string>; + submitLabel?: string; +} + +export type UmbPropertyEditorUIPickerModalResult = { + selection: Array<string>; +}; + +export const UMB_PROPERTY_EDITOR_UI_PICKER_MODAL_TOKEN = new UmbModalToken< + UmbPropertyEditorUIPickerModalData, + UmbPropertyEditorUIPickerModalResult +>('Umb.Modal.PropertyEditorUIPicker', { + type: 'sidebar', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/property-editor-ui-picker/modal-layout-property-editor-ui-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/modals/property-editor-ui-picker/property-editor-ui-picker-modal.element.ts similarity index 88% rename from src/Umbraco.Web.UI.Client/src/core/modal/layouts/property-editor-ui-picker/modal-layout-property-editor-ui-picker.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/modals/property-editor-ui-picker/property-editor-ui-picker-modal.element.ts index c5004b4497..c679bfb670 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/property-editor-ui-picker/modal-layout-property-editor-ui-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/modals/property-editor-ui-picker/property-editor-ui-picker-modal.element.ts @@ -4,23 +4,17 @@ import { customElement, property, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; import { groupBy } from 'lodash-es'; import type { UUIInputEvent } from '@umbraco-ui/uui'; -import type { UmbModalHandler } from '../../modal-handler'; +import { UmbPropertyEditorUIPickerModalData, UmbPropertyEditorUIPickerModalResult } from '.'; +import type { UmbModalHandler } from '@umbraco-cms/modal'; import type { ManifestPropertyEditorUI } from '@umbraco-cms/models'; import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; import { UmbLitElement } from '@umbraco-cms/element'; -export interface UmbModalPropertyEditorUIPickerData { - selection?: Array<string>; - submitLabel?: string; -} - interface GroupedPropertyEditorUIs { [key: string]: Array<ManifestPropertyEditorUI>; } - -// TODO: make use of UmbPickerLayoutBase -@customElement('umb-modal-layout-property-editor-ui-picker') -export class UmbModalLayoutPropertyEditorUIPickerElement extends UmbLitElement { +@customElement('umb-property-editor-ui-picker-modal') +export class UmbPropertyEditorUIPickerModalElement extends UmbLitElement { static styles = [ UUITextStyles, css` @@ -85,11 +79,8 @@ export class UmbModalLayoutPropertyEditorUIPickerElement extends UmbLitElement { `, ]; - @property({ attribute: false }) - modalHandler?: UmbModalHandler; - @property({ type: Object }) - data?: UmbModalPropertyEditorUIPickerData; + data?: UmbPropertyEditorUIPickerModalData; @state() private _groupedPropertyEditorUIs: GroupedPropertyEditorUIs = {}; @@ -145,11 +136,14 @@ export class UmbModalLayoutPropertyEditorUIPickerElement extends UmbLitElement { } private _close() { - this.modalHandler?.close(); + this.modalHandler?.reject(); } + @property({ attribute: false }) + modalHandler?: UmbModalHandler<UmbPropertyEditorUIPickerModalData, UmbPropertyEditorUIPickerModalResult>; + private _submit() { - this.modalHandler?.close({ selection: this._selection }); + this.modalHandler?.submit({ selection: this._selection }); } render() { @@ -198,8 +192,10 @@ export class UmbModalLayoutPropertyEditorUIPickerElement extends UmbLitElement { } } +export default UmbPropertyEditorUIPickerModalElement; + declare global { interface HTMLElementTagNameMap { - 'umb-modal-layout-property-editor-ui-picker': UmbModalLayoutPropertyEditorUIPickerElement; + 'umb-property-editor-ui-picker-modal': UmbPropertyEditorUIPickerModalElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/modals/property-editor-ui-picker/property-editor-ui-picker-modal.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/modals/property-editor-ui-picker/property-editor-ui-picker-modal.stories.ts new file mode 100644 index 0000000000..16d9f48b9d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/modals/property-editor-ui-picker/property-editor-ui-picker-modal.stories.ts @@ -0,0 +1,19 @@ +import { Meta, Story } from '@storybook/web-components'; +import { html } from 'lit'; +import type { UmbPropertyEditorUIPickerModalElement } from './property-editor-ui-picker-modal.element'; +import type { UmbPropertyEditorUIPickerModalData } from './'; + +import './property-editor-ui-picker-modal.element'; +import '../../../components/body-layout/body-layout.element'; + +export default { + title: 'API/Modals/Layouts/Property Editor UI Picker', + component: 'umb-property-editor-ui-picker-modal', + id: 'umb-property-editor-ui-picker-modal', +} as Meta; + +const data: UmbPropertyEditorUIPickerModalData = { selection: [] }; + +export const Overview: Story<UmbPropertyEditorUIPickerModalElement> = () => html` + <umb-property-editor-ui-picker-modal .data=${data as any}></umb-property-editor-ui-picker-modal> +`; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/block-grid/property-editor-ui-block-grid-inner-test.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/block-grid/property-editor-ui-block-grid-inner-test.element.ts new file mode 100644 index 0000000000..4142444310 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/block-grid/property-editor-ui-block-grid-inner-test.element.ts @@ -0,0 +1,87 @@ +import { html } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { IRoute, IRoutingInfo } from 'router-slot'; +import { UmbLitElement } from '@umbraco-cms/element'; +import { UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/router'; + +/** + * @element umb-property-editor-ui-block-grid-inner-test + */ +@customElement('umb-property-editor-ui-block-grid-inner-test') +export class UmbPropertyEditorUIBlockGridInnerTestElement extends UmbLitElement { + static styles = [UUITextStyles]; + + @property({ type: String }) + public name = ''; + + @state() + private _routerPath: string | undefined; + + @state() + private _activePath: string | undefined; + + @state() + private _routes: IRoute[] = [ + { + path: 'inner-1', + component: () => { + return import('./property-editor-ui-block-grid-inner-test.element'); + }, + setup: (component: Promise<HTMLElement> | HTMLElement, info: IRoutingInfo) => { + console.log('block route inner', info); + if (component instanceof HTMLElement) { + (component as any).name = 'inner-1'; + } + }, + }, + { + path: 'inner-2', + //pathMatch: 'full', + component: () => { + return import('./property-editor-ui-block-grid-inner-test.element'); + }, + setup: (component: Promise<HTMLElement> | HTMLElement, info: IRoutingInfo) => { + console.log('block route inner', info); + if (component instanceof HTMLElement) { + (component as any).name = 'inner-2'; + } + }, + }, + ]; + + render() { + return html`<div> + inner: ${this.name} + + <uui-tab-group slot="tabs"> + <uui-tab + label="INNER TAB 1" + href="${this._routerPath}/inner-1" + .active=${this._routerPath + '/inner-1' === this._activePath}></uui-tab> + <uui-tab + label="INNER TAB 2" + href="${this._routerPath}/inner-2" + .active=${this._routerPath + '/inner-2' === this._activePath}></uui-tab> + </uui-tab-group> + + <umb-router-slot + id="router-slot" + .routes="${this._routes}" + @init=${(event: UmbRouterSlotInitEvent) => { + this._routerPath = event.target.absoluteRouterPath; + }} + @change=${(event: UmbRouterSlotChangeEvent) => { + this._activePath = event.target.localActiveViewPath; + }}></umb-router-slot> + </div>`; + } +} + +export default UmbPropertyEditorUIBlockGridInnerTestElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-property-editor-ui-block-grid-inner-test': UmbPropertyEditorUIBlockGridInnerTestElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/block-grid/property-editor-ui-block-grid.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/block-grid/property-editor-ui-block-grid.element.ts index 7325189bc8..a7821c5f42 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/block-grid/property-editor-ui-block-grid.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/block-grid/property-editor-ui-block-grid.element.ts @@ -1,7 +1,11 @@ import { html } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; +import { customElement, property, state } from 'lit/decorators.js'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { IRoute, IRoutingInfo } from 'router-slot'; +import { UMB_WORKSPACE_VARIANT_CONTEXT_TOKEN } from '../../../../shared/components/workspace/workspace-variant/workspace-variant.context'; +import { UmbVariantId } from '../../../../shared/variants/variant-id.class'; import { UmbLitElement } from '@umbraco-cms/element'; +import { UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/router'; /** * @element umb-property-editor-ui-block-grid @@ -10,14 +14,101 @@ import { UmbLitElement } from '@umbraco-cms/element'; export class UmbPropertyEditorUIBlockGridElement extends UmbLitElement { static styles = [UUITextStyles]; + private _variantContext?: typeof UMB_WORKSPACE_VARIANT_CONTEXT_TOKEN.TYPE; + @property() value = ''; @property({ type: Array, attribute: false }) public config = []; + @state() + private _routes: IRoute[] = []; + + @state() + private _routerPath: string | undefined; + + @state() + private _activePath: string | undefined; + + @state() + private _variantId?: UmbVariantId; + + constructor() { + super(); + + this.consumeContext(UMB_WORKSPACE_VARIANT_CONTEXT_TOKEN, (context) => { + this._variantContext = context; + this.observe(this._variantContext?.variantId, (variantId) => { + this._variantId = variantId; + this.setupRoutes(); + }); + }); + } + + setupRoutes() { + this._routes = []; + if (this._variantId !== undefined) { + this._routes = [ + { + path: this._variantId.toString() + '/modal-1', + component: () => { + return import('./property-editor-ui-block-grid-inner-test.element'); + }, + setup: (component: Promise<HTMLElement> | HTMLElement, info: IRoutingInfo) => { + console.log('block route', info); + if (component instanceof HTMLElement) { + (component as any).name = 'block-grid-1'; + } + }, + }, + { + path: this._variantId.toString() + '/modal-2', + //pathMatch: 'full', + component: () => { + return import('./property-editor-ui-block-grid-inner-test.element'); + }, + setup: (component: Promise<HTMLElement> | HTMLElement, info: IRoutingInfo) => { + console.log('block route', info); + if (component instanceof HTMLElement) { + (component as any).name = 'block-grid-2'; + } + }, + }, + ]; + } + } + render() { - return html`<div>umb-property-editor-ui-block-grid</div>`; + return this._variantId + ? html`<div> + umb-property-editor-ui-block-grid, inner routing test: + + <uui-tab-group slot="tabs"> + <uui-tab + label="TAB 1" + href="${this._routerPath + '/' + this._variantId.toString()}/modal-1" + .active=${this._routerPath + '/' + this._variantId.toString() + '/modal-1' === + this._activePath}></uui-tab> + <uui-tab + label="TAB 2" + href="${this._routerPath + '/' + this._variantId.toString()}/modal-2" + .active=${this._routerPath + '/' + this._variantId.toString() + '/modal-2' === + this._activePath}></uui-tab> + </uui-tab-group> + + <umb-router-slot + id="router-slot" + .routes="${this._routes}" + @init=${(event: UmbRouterSlotInitEvent) => { + this._routerPath = event.target.absoluteRouterPath; + }} + @change=${(event: UmbRouterSlotChangeEvent) => { + this._activePath = event.target.localActiveViewPath; + }}> + </umb-router-slot> + </div>` + : 'loading...'; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/date-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/date-picker/manifests.ts index 51139cd0ea..0a4b96eb04 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/date-picker/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/date-picker/manifests.ts @@ -15,15 +15,25 @@ export const manifest: ManifestPropertyEditorUI = { { alias: 'format', label: 'Date format', - description: 'If left empty then the format is YYYY-MM-DD. (see momentjs.com for supported formats)', + description: 'If left empty then the format is YYYY-MM-DD', propertyEditorUI: 'Umb.PropertyEditorUI.TextBox', }, + { + alias: 'offsetTime', + label: 'Offset time', + description: 'When enabled the time displayed will be offset with the servers timezone, this is useful for scenarios like scheduled publishing when an editor is in a different timezone than the hosted server', + propertyEditorUI: 'Umb.PropertyEditorUI.Toggle', + }, ], defaultData: [ { alias: 'format', value: 'YYYY-MM-DD HH:mm:ss', }, + { + alias: 'offsetTime', + value: false + } ], }, }, diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/date-picker/property-editor-ui-date-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/date-picker/property-editor-ui-date-picker.element.ts index 3ebaf9d8a0..db7efd7f55 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/date-picker/property-editor-ui-date-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/date-picker/property-editor-ui-date-picker.element.ts @@ -1,7 +1,10 @@ import { html } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; +import { customElement, property, state } from 'lit/decorators.js'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { InputType } from '@umbraco-ui/uui'; +import { UmbPropertyValueChangeEvent } from '../..'; import { UmbLitElement } from '@umbraco-cms/element'; +import { PropertyEditorConfigDefaultData } from '@umbraco-cms/extensions-registry'; /** * @element umb-property-editor-ui-date-picker @@ -10,14 +13,72 @@ import { UmbLitElement } from '@umbraco-cms/element'; export class UmbPropertyEditorUIDatePickerElement extends UmbLitElement { static styles = [UUITextStyles]; + private _value?: Date; + private _valueString?: string; + @property() - value = ''; + set value(value: string | undefined) { + if (value) { + const d = new Date(value); + this._value = d; + this._valueString = `${d.getFullYear()}-${ + d.getMonth() + 1 + }-${d.getDate()}T${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}`; + } else { + this._value = undefined; + this._valueString = undefined; + } + } + + get value() { + return this._valueString; + } + + private _onInput(e: InputEvent) { + const dateField = e.target as HTMLInputElement; + this.value = dateField.value; + this.dispatchEvent(new UmbPropertyValueChangeEvent()); + } + + private _format?: string; + + @state() + private _inputType: InputType = 'datetime-local'; + + private _offsetTime?: boolean; @property({ type: Array, attribute: false }) - public config = []; + public set config(config: Array<PropertyEditorConfigDefaultData>) { + const oldVal = this._inputType; + + // Format string prevalue/config + this._format = config.find((x) => x.alias === 'format')?.value; + const pickTime = this._format?.includes('H') || this._format?.includes('m'); + if (pickTime) { + this._inputType = 'datetime-local'; + } else { + this._inputType = 'date'; + } + + // Based on the type of format string change the UUI-input type + const timeFormatPattern = /^h{1,2}:m{1,2}(:s{1,2})?\s?a?$/gim; + if (this._format?.toLowerCase().match(timeFormatPattern)) { + this._inputType = 'time'; + } + + // TODO: Warren - Need to deal with offSetTime prevalue/config + // Currently the date picker in uui-iinput does not change based on this config + this._offsetTime = config.find((x) => x.alias === 'offsetTime')?.value; + + this.requestUpdate('_inputType', oldVal); + } render() { - return html`<div>umb-property-editor-ui-date-picker</div>`; + return html` <uui-input + .type=${this._inputType} + @input=${this._onInput} + .value=${this._valueString} + label="Pick a date or time"></uui-input>`; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/date-picker/property-editor-ui-date-picker.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/date-picker/property-editor-ui-date-picker.stories.ts index 783f397430..b3f5beae7e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/date-picker/property-editor-ui-date-picker.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/date-picker/property-editor-ui-date-picker.stories.ts @@ -8,8 +8,51 @@ export default { title: 'Property Editor UIs/Date Picker', component: 'umb-property-editor-ui-date-picker', id: 'umb-property-editor-ui-date-picker', -} as Meta; + args: { + config: [ + { + alias: 'format', + value: 'YYYY-MM-DD HH:mm:ss' + } + ] + } +} as Meta<UmbPropertyEditorUIDatePickerElement>; -export const AAAOverview: Story<UmbPropertyEditorUIDatePickerElement> = () => - html`<umb-property-editor-ui-date-picker></umb-property-editor-ui-date-picker>`; -AAAOverview.storyName = 'Overview'; +const Template: Story<UmbPropertyEditorUIDatePickerElement> = ({config, value}) => html`<umb-property-editor-ui-date-picker .config=${config} .value=${value}></umb-property-editor-ui-date-picker>`; + +export const Overview = Template.bind({}); + +export const WithDateValue = Template.bind({}); +WithDateValue.args = { + value: '2021-01-24 15:20' +}; + +export const WithFormat = Template.bind({}); +WithFormat.args = { + config: [ + { + alias: 'format', + value: 'dd/MM/yyyy HH:mm:ss' + } + ] +}; + +export const TimeOnly = Template.bind({}); +TimeOnly.args = { + config: [ + { + alias: 'format', + value: 'HH:mm:ss' + } + ] +}; + +export const DateOnly = Template.bind({}); +DateOnly.args = { + config: [ + { + alias: 'format', + value: 'dd/MM/yyyy' + } + ] +}; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/date-picker/property-editor-ui-date-picker.test.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/date-picker/property-editor-ui-date-picker.test.ts index 48583babe9..91f298f819 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/date-picker/property-editor-ui-date-picker.test.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/date-picker/property-editor-ui-date-picker.test.ts @@ -1,21 +1,42 @@ import { expect, fixture, html } from '@open-wc/testing'; +import { UUIInputElement } from '@umbraco-ui/uui'; import { UmbPropertyEditorUIDatePickerElement } from './property-editor-ui-date-picker.element'; import { defaultA11yConfig } from '@umbraco-cms/test-utils'; describe('UmbPropertyEditorUIDatePickerElement', () => { - let element: UmbPropertyEditorUIDatePickerElement; + let element: UmbPropertyEditorUIDatePickerElement; + let inputElement: UUIInputElement; - beforeEach(async () => { - element = await fixture( - html` <umb-property-editor-ui-date-picker></umb-property-editor-ui-date-picker> ` - ); - }); + beforeEach(async () => { + element = await fixture(html` <umb-property-editor-ui-date-picker></umb-property-editor-ui-date-picker> `); + inputElement = element.shadowRoot?.querySelector('uui-input') as UUIInputElement; + }); - it('is defined with its own instance', () => { - expect(element).to.be.instanceOf(UmbPropertyEditorUIDatePickerElement); - }); + it('is defined with its own instance', () => { + expect(element).to.be.instanceOf(UmbPropertyEditorUIDatePickerElement); + }); - it('passes the a11y audit', async () => { - await expect(element).shadowDom.to.be.accessible(defaultA11yConfig); - }); + it('should have an input element', () => { + expect(inputElement).to.exist; + }); + + it('should show a datetime-local input by default', () => { + expect(inputElement.type).to.equal('datetime-local'); + }); + + it('should show a type=date field if the format only contains a date', async () => { + element.config = [{ alias: 'format', value: 'YYYY-MM-dd' }]; + await element.updateComplete; + expect(inputElement.type).to.equal('date'); + }); + + it('should show a type=time field if the format only contains a time', async () => { + element.config = [{ alias: 'format', value: 'HH:mm' }]; + await element.updateComplete; + expect(inputElement.type).to.equal('time'); + }); + + it('passes the a11y audit', async () => { + await expect(element).shadowDom.to.be.accessible(defaultA11yConfig); + }); }); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/document-picker/property-editor-ui-document-picker.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/document-picker/property-editor-ui-document-picker.stories.ts index 59321bab75..a9eda5c776 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/document-picker/property-editor-ui-document-picker.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/document-picker/property-editor-ui-document-picker.stories.ts @@ -2,21 +2,12 @@ import { Meta, Story } from '@storybook/web-components'; import { html } from 'lit-html'; import type { UmbPropertyEditorUIContentPickerElement } from './property-editor-ui-document-picker.element'; -import { UmbModalContext } from 'src/core/modal'; import './property-editor-ui-document-picker.element'; -import '../../../components/backoffice-frame/backoffice-modal-container.element'; export default { title: 'Property Editor UIs/Content Picker', component: 'umb-property-editor-ui-document-picker', id: 'umb-property-editor-ui-document-picker', - decorators: [ - (story) => - html`<umb-context-provider key="UmbModalContext" .value=${new UmbModalContext()}> - ${story()} - <umb-backoffice-modal-container></umb-backoffice-modal-container> - </umb-context-provider>`, - ], } as Meta; export const AAAOverview: Story<UmbPropertyEditorUIContentPickerElement> = () => diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/icon-picker/property-editor-ui-icon-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/icon-picker/property-editor-ui-icon-picker.element.ts index ef101ab3cb..6d996a29ef 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/icon-picker/property-editor-ui-icon-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/icon-picker/property-editor-ui-icon-picker.element.ts @@ -1,7 +1,8 @@ import { html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property } from 'lit/decorators.js'; -import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '../../../../../core/modal'; +import { UMB_ICON_PICKER_MODAL_TOKEN } from '../../../modals/icon-picker'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; import { UmbLitElement } from '@umbraco-cms/element'; /** @@ -27,7 +28,7 @@ export class UmbPropertyEditorUIIconPickerElement extends UmbLitElement { } private _openModal() { - this._modalContext?.iconPicker(); + this._modalContext?.open(UMB_ICON_PICKER_MODAL_TOKEN); } render() { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/icon-picker/property-editor-ui-icon-picker.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/icon-picker/property-editor-ui-icon-picker.stories.ts index d9a0a21ff6..b48f69befe 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/icon-picker/property-editor-ui-icon-picker.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/icon-picker/property-editor-ui-icon-picker.stories.ts @@ -3,7 +3,7 @@ import { html } from 'lit-html'; import type { UmbPropertyEditorUIIconPickerElement } from './property-editor-ui-icon-picker.element'; import './property-editor-ui-icon-picker.element'; -import type { UmbModalLayoutIconPickerElement } from 'src/core/modal/layouts/icon-picker/modal-layout-icon-picker.element'; +import type { UmbIconPickerModalElement } from 'src/backoffice/shared/modals/icon-picker/icon-picker-modal.element'; export default { title: 'Property Editor UIs/Icon Picker', @@ -11,8 +11,8 @@ export default { id: 'umb-property-editor-ui-icon-picker', } as Meta; -export const AAAOverview: Story<UmbModalLayoutIconPickerElement> = () => - html`<umb-modal-layout-icon-picker></umb-modal-layout-icon-picker>`; +export const AAAOverview: Story<UmbIconPickerModalElement> = () => + html`<umb-icon-picker-modal></umb-icon-picker-modal>`; AAAOverview.storyName = 'Overview'; AAAOverview.decorators = [ (story) => diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/multi-url-picker/property-editor-ui-multi-url-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/multi-url-picker/property-editor-ui-multi-url-picker.element.ts index 59d0abd9e4..5b79e28ab6 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/multi-url-picker/property-editor-ui-multi-url-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/multi-url-picker/property-editor-ui-multi-url-picker.element.ts @@ -2,10 +2,8 @@ import { html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar'; -import { - UmbInputMultiUrlPickerElement, - MultiUrlData, -} from '../../../../shared/components/input-multi-url-picker/input-multi-url-picker.element'; +import { UmbInputMultiUrlPickerElement } from '../../../../shared/components/input-multi-url-picker/input-multi-url-picker.element'; +import { UmbLinkPickerLink } from '../../../../shared/modals/link-picker'; import { UmbLitElement } from '@umbraco-cms/element'; import { DataTypePropertyModel } from '@umbraco-cms/backend-api'; @@ -18,7 +16,7 @@ export class UmbPropertyEditorUIMultiUrlPickerElement extends UmbLitElement { static styles = [UUITextStyles]; @property({ type: Array }) - value: MultiUrlData[] = []; + value: UmbLinkPickerLink[] = []; @property({ type: Array, attribute: false }) public set config(config: DataTypePropertyModel[]) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/multiple-text-string/input-multiple-text-string-item/input-multiple-text-string-item.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/multiple-text-string/input-multiple-text-string-item/input-multiple-text-string-item.element.ts index 5a9751bce4..44ce7f16a8 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/multiple-text-string/input-multiple-text-string-item/input-multiple-text-string-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/multiple-text-string/input-multiple-text-string-item/input-multiple-text-string-item.element.ts @@ -4,9 +4,10 @@ import { customElement, property, query } from 'lit/decorators.js'; import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; import { UUIInputEvent } from '@umbraco-ui/uui-input'; import { UUIInputElement } from '@umbraco-ui/uui'; -import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '../../../../../../core/modal'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; import { UmbChangeEvent, UmbInputEvent, UmbDeleteEvent } from '@umbraco-cms/events'; import { UmbLitElement } from '@umbraco-cms/element'; +import { UMB_CONFIRM_MODAL_TOKEN } from 'src/backoffice/shared/modals/confirm'; /** * @element umb-input-multiple-text-string-item @@ -65,17 +66,15 @@ export class UmbInputMultipleTextStringItemElement extends FormControlMixin(UmbL } #onDelete() { - const modalHandler = this._modalContext?.confirm({ + const modalHandler = this._modalContext?.open(UMB_CONFIRM_MODAL_TOKEN, { headline: `Delete ${this.value || 'item'}`, content: 'Are you sure you want to delete this item?', color: 'danger', confirmLabel: 'Delete', }); - modalHandler?.onClose().then(({ confirmed }: any) => { - if (confirmed) { - this.dispatchEvent(new UmbDeleteEvent()); - } + modalHandler?.onSubmit().then(() => { + this.dispatchEvent(new UmbDeleteEvent()); }); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tiny-mce/config/configuration/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tiny-mce/config/configuration/manifests.ts index 853af4e856..5363e9faec 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tiny-mce/config/configuration/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tiny-mce/config/configuration/manifests.ts @@ -2,8 +2,8 @@ import type { ManifestPropertyEditorUI } from '@umbraco-cms/models'; export const manifest: ManifestPropertyEditorUI = { type: 'propertyEditorUI', - alias: 'Umb.PropertyEditorUI.TinyMCE', - name: 'TinyMCE Property Editor UI', + alias: 'Umb.PropertyEditorUI.TinyMCE.Config', + name: 'Tiny MCE Configuration Property Editor UI', loader: () => import('./property-editor-ui-tiny-mce-configuration.element'), meta: { label: 'Rich Text Editor Configuration', diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/toggle/property-editor-ui-toggle.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/toggle/property-editor-ui-toggle.element.ts index eded572828..1e411f549a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/toggle/property-editor-ui-toggle.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/toggle/property-editor-ui-toggle.element.ts @@ -1,7 +1,7 @@ import { html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; -import { UmbInputToggleElement } from '../../../components/input-toggle/input-toggle-element'; +import { UmbInputToggleElement } from '../../../components/input-toggle/input-toggle.element'; import { UmbLitElement } from '@umbraco-cms/element'; import { DataTypePropertyModel } from '@umbraco-cms/backend-api'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/variants/variant-id.class.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/variants/variant-id.class.ts index 876e58ad14..d8e07d72d0 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/variants/variant-id.class.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/variants/variant-id.class.ts @@ -1,15 +1,14 @@ export class UmbVariantId { - public static Create(culture: string | null, segment: string | null) { - return Object.freeze(new UmbVariantId(culture, segment)); + public static Create(variantData: { culture?: string | null; segment?: string | null }): UmbVariantId { + return Object.freeze(new UmbVariantId(variantData)); } public readonly culture: string | null = null; public readonly segment: string | null = null; - // prettier-ignore - constructor(culture: string | null, segment: string | null) { - this.culture = culture || null; - this.segment = segment || null; + constructor(variantData: { culture?: string | null; segment?: string | null }) { + this.culture = variantData.culture || null; + this.segment = variantData.segment || null; } public compare(obj: { culture?: string | null; segment?: string | null }): boolean { @@ -23,4 +22,18 @@ export class UmbVariantId { public toString(): string { return (this.culture || 'invariant') + (this.segment ? `_${this.segment}` : ''); } + + public toDifferencesString(variantId: UmbVariantId): string { + let r = ''; + + if (variantId.culture !== this.culture) { + r = 'Invariant'; + } + + if (variantId.segment !== this.segment) { + r = (r !== '' ? ' ' : '') + 'Unsegmented'; + } + + return r; + } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/index.ts index 1a8e8d3ed7..a869c8a5fb 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/index.ts @@ -3,11 +3,10 @@ import { manifests as templateManifests } from './templates/manifests'; import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; import { ManifestTypes } from '@umbraco-cms/extensions-registry'; +export const manifests = [...menuManifests, ...templateManifests]; + const registerExtensions = (manifests: Array<ManifestTypes>) => { - manifests.forEach((manifest) => { - if (umbExtensionsRegistry.isRegistered(manifest.alias)) return; - umbExtensionsRegistry.register(manifest); - }); + manifests.forEach((manifest) => umbExtensionsRegistry.register(manifest)); }; -registerExtensions([...menuManifests, ...templateManifests]); +registerExtensions(manifests); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/entity-actions/manifests.ts index b5ee6cf27b..eead35cbed 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/entity-actions/manifests.ts @@ -1,3 +1,4 @@ +import { TEMPLATE_REPOSITORY_ALIAS } from '../repository/manifests'; import { UmbCreateEntityAction } from './create/create.action'; import { UmbDeleteEntityAction } from '@umbraco-cms/entity-action'; import { ManifestEntityAction } from 'libs/extensions-registry/entity-action.models'; @@ -12,7 +13,7 @@ const entityActions: Array<ManifestEntityAction> = [ icon: 'umb:add', label: 'Create', api: UmbCreateEntityAction, - repositoryAlias: 'Umb.Repository.Templates', + repositoryAlias: TEMPLATE_REPOSITORY_ALIAS, }, }, { @@ -24,7 +25,7 @@ const entityActions: Array<ManifestEntityAction> = [ icon: 'umb:trash', label: 'Delete', api: UmbDeleteEntityAction, - repositoryAlias: 'Umb.Repository.Templates', + repositoryAlias: TEMPLATE_REPOSITORY_ALIAS, }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/manifests.ts index 93f9f06346..ba3295e7e1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/manifests.ts @@ -1,13 +1,33 @@ import { UmbTemplateRepository } from '../repository/template.repository'; +import { UmbTemplateTreeStore } from './template.tree.store'; +import { UmbTemplateStore } from './template.store'; import { ManifestRepository } from 'libs/extensions-registry/repository.models'; +import { ManifestStore, ManifestTreeStore } from '@umbraco-cms/extensions-registry'; -export const TEMPLATE_REPOSITORY_ALIAS = 'Umb.Repository.Templates'; +export const TEMPLATE_REPOSITORY_ALIAS = 'Umb.Repository.Template'; const repository: ManifestRepository = { type: 'repository', alias: TEMPLATE_REPOSITORY_ALIAS, - name: 'Documents Repository', + name: 'Template Repository', class: UmbTemplateRepository, }; -export const manifests = [repository]; +export const TEMPLATE_STORE_ALIAS = 'Umb.Store.Template'; +export const TEMPLATE_TREE_STORE_ALIAS = 'Umb.Store.TemplateTree'; + +const store: ManifestStore = { + type: 'store', + alias: TEMPLATE_STORE_ALIAS, + name: 'Template Store', + class: UmbTemplateStore, +}; + +const treeStore: ManifestTreeStore = { + type: 'treeStore', + alias: TEMPLATE_TREE_STORE_ALIAS, + name: 'Template Tree Store', + class: UmbTemplateTreeStore, +}; + +export const manifests = [repository, store, treeStore]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/data/sources/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/sources/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/data/sources/index.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/sources/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/sources/template.detail.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/sources/template.detail.server.data.ts similarity index 88% rename from src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/sources/template.detail.server.data.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/sources/template.detail.server.data.ts index 37911d0722..0d6f39e347 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/sources/template.detail.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/sources/template.detail.server.data.ts @@ -1,8 +1,16 @@ import { v4 as uuid } from 'uuid'; -import { TemplateDetailDataSource } from '.'; import { ProblemDetailsModel, TemplateModel, TemplateResource } from '@umbraco-cms/backend-api'; import type { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { tryExecuteAndNotify } from '@umbraco-cms/resources'; +import type { DataSourceResponse } from '@umbraco-cms/models'; + +export interface TemplateDetailDataSource { + createScaffold(): Promise<DataSourceResponse<TemplateModel>>; + get(key: string): Promise<DataSourceResponse<TemplateModel>>; + insert(template: TemplateModel): Promise<DataSourceResponse>; + update(template: TemplateModel): Promise<DataSourceResponse>; + delete(key: string): Promise<DataSourceResponse>; +} /** * A data source for the Template detail that fetches data from the server diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/data/sources/template.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/sources/template.tree.server.data.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/data/sources/template.tree.server.data.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/sources/template.tree.server.data.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.repository.ts index 46a8c1fd19..3c8cf551b1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.repository.ts @@ -1,10 +1,7 @@ -import { UmbTemplateDetailServerDataSource } from '../workspace/data/sources/template.detail.server.data'; -import { TemplateTreeServerDataSource } from '../tree/data/sources/template.tree.server.data'; -import { - UmbTemplateDetailStore, - UMB_TEMPLATE_DETAIL_STORE_CONTEXT_TOKEN, -} from '../workspace/data/template.detail.store'; -import { UmbTemplateTreeStore, UMB_TEMPLATE_TREE_STORE_CONTEXT_TOKEN } from '../tree/data/template.tree.store'; +import { UmbTemplateDetailServerDataSource } from './sources/template.detail.server.data'; +import { TemplateTreeServerDataSource } from './sources/template.tree.server.data'; +import { UmbTemplateStore, UMB_TEMPLATE_STORE_CONTEXT_TOKEN } from './template.store'; +import { UmbTemplateTreeStore, UMB_TEMPLATE_TREE_STORE_CONTEXT_TOKEN } from './template.tree.store'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/notification'; import { UmbContextConsumerController } from '@umbraco-cms/context-api'; @@ -12,10 +9,6 @@ import { ProblemDetailsModel, TemplateModel } from '@umbraco-cms/backend-api'; import { UmbDetailRepository } from 'libs/repository/detail-repository.interface'; import { UmbTreeRepository } from 'libs/repository/tree-repository.interface'; -// Move to documentation / JSdoc -/* We need to create a new instance of the repository from within the element context. We want the notifications to be displayed in the right context. */ -// element -> context -> repository -> (store) -> data source -// All methods should be async and return a promise. Some methods might return an observable as part of the promise response. export class UmbTemplateRepository implements UmbTreeRepository, UmbDetailRepository<TemplateModel> { #init; #host: UmbControllerHostInterface; @@ -24,7 +17,7 @@ export class UmbTemplateRepository implements UmbTreeRepository, UmbDetailReposi #detailDataSource: UmbTemplateDetailServerDataSource; #treeStore?: UmbTemplateTreeStore; - #detailStore?: UmbTemplateDetailStore; + #store?: UmbTemplateStore; #notificationContext?: UmbNotificationContext; @@ -40,8 +33,8 @@ export class UmbTemplateRepository implements UmbTreeRepository, UmbDetailReposi this.#treeStore = instance; }), - new UmbContextConsumerController(this.#host, UMB_TEMPLATE_DETAIL_STORE_CONTEXT_TOKEN, (instance) => { - this.#detailStore = instance; + new UmbContextConsumerController(this.#host, UMB_TEMPLATE_STORE_CONTEXT_TOKEN, (instance) => { + this.#store = instance; }), new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { @@ -134,7 +127,7 @@ export class UmbTemplateRepository implements UmbTreeRepository, UmbDetailReposi const { data, error } = await this.#detailDataSource.get(key); if (data) { - this.#detailStore?.append(data); + this.#store?.append(data); } return { data, error }; @@ -158,7 +151,7 @@ export class UmbTemplateRepository implements UmbTreeRepository, UmbDetailReposi // TODO: we currently don't use the detail store for anything. // Consider to look up the data before fetching from the server - this.#detailStore?.append(template); + this.#store?.append(template); // TODO: Update tree store with the new item? or ask tree to request the new item? return { error }; @@ -181,7 +174,7 @@ export class UmbTemplateRepository implements UmbTreeRepository, UmbDetailReposi // TODO: we currently don't use the detail store for anything. // Consider to look up the data before fetching from the server // Consider notify a workspace if a template is updated in the store while someone is editing it. - this.#detailStore?.append(template); + this.#store?.append(template); this.#treeStore?.updateItem(template.key, { name: template.name }); // TODO: would be nice to align the stores on methods/methodNames. @@ -207,7 +200,7 @@ export class UmbTemplateRepository implements UmbTreeRepository, UmbDetailReposi // TODO: we currently don't use the detail store for anything. // Consider to look up the data before fetching from the server. // Consider notify a workspace if a template is deleted from the store while someone is editing it. - this.#detailStore?.remove([key]); + this.#store?.remove([key]); this.#treeStore?.removeItem(key); // TODO: would be nice to align the stores on methods/methodNames. diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/template.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.store.ts similarity index 62% rename from src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/template.detail.store.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.store.ts index 8fa732967e..0051164f79 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/template.detail.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.store.ts @@ -6,26 +6,26 @@ import type { UmbControllerHostInterface } from '@umbraco-cms/controller'; /** * @export - * @class UmbTemplateDetailStore + * @class UmbTemplateStore * @extends {UmbStoreBase} - * @description - Data Store for Template Details + * @description - Data Store for Templates */ -export class UmbTemplateDetailStore extends UmbStoreBase { +export class UmbTemplateStore extends UmbStoreBase { #data = new ArrayState<TemplateModel>([], (x) => x.key); /** - * Creates an instance of UmbTemplateDetailStore. + * Creates an instance of UmbTemplateStore. * @param {UmbControllerHostInterface} host - * @memberof UmbTemplateDetailStore + * @memberof UmbTemplateStore */ constructor(host: UmbControllerHostInterface) { - super(host, UMB_TEMPLATE_DETAIL_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_TEMPLATE_STORE_CONTEXT_TOKEN.toString()); } /** * Append a template to the store * @param {Template} template - * @memberof UmbTemplateDetailStore + * @memberof UmbTemplateStore */ append(template: TemplateModel) { this.#data.append([template]); @@ -34,13 +34,11 @@ export class UmbTemplateDetailStore extends UmbStoreBase { /** * Removes templates in the store with the given uniques * @param {string[]} uniques - * @memberof UmbTemplateDetailStore + * @memberof UmbTemplateStore */ remove(uniques: string[]) { this.#data.remove(uniques); } } -export const UMB_TEMPLATE_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbTemplateDetailStore>( - 'UmbTemplateDetailStore' -); +export const UMB_TEMPLATE_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbTemplateStore>('UmbTemplateStore'); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/data/template.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.tree.store.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/tree/data/template.tree.store.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.tree.store.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/sources/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/sources/index.ts deleted file mode 100644 index 04bda18352..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/sources/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { TemplateModel } from '@umbraco-cms/backend-api'; -import type { DataSourceResponse } from '@umbraco-cms/models'; - -export interface TemplateDetailDataSource { - createScaffold(): Promise<DataSourceResponse<TemplateModel>>; - get(key: string): Promise<DataSourceResponse<TemplateModel>>; - insert(template: TemplateModel): Promise<DataSourceResponse>; - update(template: TemplateModel): Promise<DataSourceResponse>; - delete(key: string): Promise<DataSourceResponse>; -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/template.detail.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/template.detail.repository.ts deleted file mode 100644 index 3971b37f8a..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/data/template.detail.repository.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { UmbTemplateTreeStore, UMB_TEMPLATE_TREE_STORE_CONTEXT_TOKEN } from '../../tree/data/template.tree.store'; -import { UmbTemplateDetailStore, UMB_TEMPLATE_DETAIL_STORE_CONTEXT_TOKEN } from './template.detail.store'; -import { UmbTemplateDetailServerDataSource } from './sources/template.detail.server.data'; -import type { ProblemDetailsModel, TemplateModel } from '@umbraco-cms/backend-api'; -import { UmbContextConsumerController } from '@umbraco-cms/context-api'; -import type { UmbControllerHostInterface } from '@umbraco-cms/controller'; -import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/notification'; - -// Move to documentation / JSdoc -/* We need to create a new instance of the repository from within the element context. We want the notifications to be displayed in the right context. */ -// element -> context -> repository -> (store) -> data source -// All methods should be async and return a promise. Some methods might return an observable as part of the promise response. -export class UmbTemplateDetailRepository { - #host: UmbControllerHostInterface; - #dataSource: UmbTemplateDetailServerDataSource; - #detailStore?: UmbTemplateDetailStore; - #treeStore?: UmbTemplateTreeStore; - #notificationContext?: UmbNotificationContext; - #initResolver?: () => void; - #initialized = false; - - constructor(host: UmbControllerHostInterface) { - this.#host = host; - - // TODO: figure out how spin up get the correct data source - this.#dataSource = new UmbTemplateDetailServerDataSource(this.#host); - - // TODO: should we allow promises so each method can request the context when it needs it instead of initializing it upfront? - new UmbContextConsumerController(this.#host, UMB_TEMPLATE_DETAIL_STORE_CONTEXT_TOKEN, (instance) => { - this.#detailStore = instance; - this.#checkIfInitialized(); - }); - - new UmbContextConsumerController(this.#host, UMB_TEMPLATE_TREE_STORE_CONTEXT_TOKEN, (instance) => { - this.#treeStore = instance; - this.#checkIfInitialized(); - }); - - new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { - this.#notificationContext = instance; - this.#checkIfInitialized(); - }); - } - - #init() { - // TODO: This would only works with one user of this method. If two, the first one would be forgotten, but maybe its alright for now as I guess this is temporary. - return new Promise<void>((resolve) => { - this.#initialized ? resolve() : (this.#initResolver = resolve); - }); - } - - #checkIfInitialized() { - if (this.#detailStore && this.#detailStore && this.#notificationContext) { - this.#initialized = true; - this.#initResolver?.(); - } - } - - async createScaffold(parentKey: string | null) { - await this.#init(); - - // TODO: should we show a notification if the parent key is missing? - // TODO: What is the parentKey used for? - // Investigate what is best for Acceptance testing, cause in that perspective a thrown error might be the best choice? - if (!parentKey) { - const error: ProblemDetailsModel = { title: 'Parent key is missing' }; - return { data: undefined, error }; - } - - return this.#dataSource.createScaffold(); - } - - async get(key: string) { - await this.#init(); - - // TODO: should we show a notification if the key is missing? - // Investigate what is best for Acceptance testing, cause in that perspective a thrown error might be the best choice? - if (!key) { - const error: ProblemDetailsModel = { title: 'Key is missing' }; - return { error }; - } - - return this.#dataSource.get(key); - } - - // Could potentially be named differently. could be: Create - async insert(template: TemplateModel) { - await this.#init(); - - // TODO: should we show a notification if the template is missing? - // Investigate what is best for Acceptance testing, cause in that perspective a thrown error might be the best choice? - if (!template) { - const error: ProblemDetailsModel = { title: 'Template is missing' }; - return { error }; - } - - const { error } = await this.#dataSource.insert(template); - - if (!error) { - const notification = { data: { message: `Template created` } }; - this.#notificationContext?.peek('positive', notification); - } - - // TODO: we currently don't use the detail store for anything. - // Consider to look up the data before fetching from the server - this.#detailStore?.append(template); - // TODO: Update tree store with the new item? - - // TODO: fix type issue: - return { error } as any; - } - - // Could potentially be named differently. could be: Save - async update(template: TemplateModel) { - await this.#init(); - - // TODO: should we show a notification if the template is missing? - // Investigate what is best for Acceptance testing, cause in that perspective a thrown error might be the best choice? - if (!template || !template.key) { - const error: ProblemDetailsModel = { title: 'Template is missing' }; - return { error }; - } - - const { error } = await this.#dataSource.update(template); - - if (!error) { - const notification = { data: { message: `Template saved` } }; - this.#notificationContext?.peek('positive', notification); - } - - // TODO: we currently don't use the detail store for anything. - // Consider to look up the data before fetching from the server - // Consider notify a workspace if a template is updated in the store while someone is editing it. - this.#detailStore?.append(template); - this.#treeStore?.updateItem(template.key, { name: template.name }); - // TODO: would be nice to align the stores on methods/methodNames. - - return { error }; - } - - async delete(key: string) { - await this.#init(); - - // TODO: should we show a notification if the key is missing? - if (!key) { - const error: ProblemDetailsModel = { title: 'Key is missing' }; - return { error }; - } - - const { error } = await this.#dataSource.delete(key); - - if (!error) { - const notification = { data: { message: `Template deleted` } }; - this.#notificationContext?.peek('positive', notification); - } - - // TODO: we currently don't use the detail store for anything. - // Consider to look up the data before fetching from the server. - // Consider notify a workspace if a template is deleted from the store while someone is editing it. - this.#detailStore?.remove([key]); - this.#treeStore?.removeItem(key); - // TODO: would be nice to align the stores on methods/methodNames. - - return { error }; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/template-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/template-workspace.context.ts index 35f7d27dfd..16886f30de 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/template-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/template-workspace.context.ts @@ -1,24 +1,17 @@ -import { UmbTemplateDetailRepository } from './data/template.detail.repository'; +import { UmbTemplateRepository } from '../repository/template.repository'; +import { UmbWorkspaceContext } from '../../../shared/components/workspace/workspace-context/workspace-context'; import { createObservablePart, DeepState } from '@umbraco-cms/observable-api'; import { TemplateModel } from '@umbraco-cms/backend-api'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; -import { UmbContextProviderController } from '@umbraco-cms/context-api'; - -export class UmbTemplateWorkspaceContext { - #host: UmbControllerHostInterface; - #templateDetailRepo: UmbTemplateDetailRepository; +export class UmbTemplateWorkspaceContext extends UmbWorkspaceContext<UmbTemplateRepository> { #data = new DeepState<TemplateModel | undefined>(undefined); data = this.#data.asObservable(); name = createObservablePart(this.#data, (data) => data?.name); content = createObservablePart(this.#data, (data) => data?.content); - isNew = false; - constructor(host: UmbControllerHostInterface) { - this.#host = host; - this.#templateDetailRepo = new UmbTemplateDetailRepository(this.#host); - new UmbContextProviderController(this.#host, 'umbWorkspaceContext', this); + super(host, new UmbTemplateRepository(host)); } getData() { @@ -34,16 +27,17 @@ export class UmbTemplateWorkspaceContext { } async load(entityKey: string) { - const { data } = await this.#templateDetailRepo.get(entityKey); + const { data } = await this.repository.requestByKey(entityKey); if (data) { + this.setIsNew(false); this.#data.next(data); } } async createScaffold(parentKey: string | null) { - this.isNew = true; - const { data } = await this.#templateDetailRepo.createScaffold(parentKey); + const { data } = await this.repository.createScaffold(parentKey); if (!data) return; + this.setIsNew(true); this.#data.next(data); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dashboards/dictionary/dashboard-translation-dictionary.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dashboards/dictionary/dashboard-translation-dictionary.element.ts index cc2ddddd6e..38b5625580 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dashboards/dictionary/dashboard-translation-dictionary.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dashboards/dictionary/dashboard-translation-dictionary.element.ts @@ -4,7 +4,10 @@ import { customElement, state } from 'lit/decorators.js'; import { when } from 'lit-html/directives/when.js'; import { UmbTableConfig, UmbTableColumn, UmbTableItem } from '../../../../backoffice/shared/components/table'; import { UmbDictionaryRepository } from '../../dictionary/repository/dictionary.repository'; -import { UmbCreateDictionaryModalResultData } from '../../dictionary/entity-actions/create/create-dictionary-modal-layout.element'; +import { + UmbCreateDictionaryModalResult, + UMB_CREATE_DICTIONARY_MODAL_TOKEN, +} from '../../dictionary/entity-actions/create/'; import { UmbLitElement } from '@umbraco-cms/element'; import { DictionaryOverviewModel, LanguageModel } from '@umbraco-cms/backend-api'; import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; @@ -156,13 +159,10 @@ export class UmbDashboardTranslationDictionaryElement extends UmbLitElement { // TODO: what to do if modal service is not available? if (!this.#modalContext) return; - const modalHandler = this.#modalContext?.open('umb-create-dictionary-modal-layout', { - type: 'sidebar', - data: { unique: null }, - }); + const modalHandler = this.#modalContext?.open(UMB_CREATE_DICTIONARY_MODAL_TOKEN, { unique: null }); // TODO: get type from modal result - const { name }: UmbCreateDictionaryModalResultData = await modalHandler.onClose(); + const { name } = await modalHandler.onSubmit(); if (!name) return; const result = await this.#repo?.create({ $type: '', name, parentKey: null, translations: [], key: '' }); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/create/create-dictionary-modal-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/create/create-dictionary-modal-layout.element.ts index 0ad2d98afd..4b462e515f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/create/create-dictionary-modal-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/create/create-dictionary-modal-layout.element.ts @@ -1,38 +1,32 @@ import { html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, query } from 'lit/decorators.js'; -import { Observable } from 'rxjs'; import { when } from 'lit-html/directives/when.js'; -import { UmbModalLayoutElement } from '@umbraco-cms/modal'; +import { UmbCreateDictionaryModalData, UmbCreateDictionaryModalResult } from '.'; +import { UmbModalBaseElement } from '@umbraco-cms/modal'; -export interface UmbCreateDictionaryModalData { - unique: string | null; - parentName: Observable<string | undefined> -} - -export interface UmbCreateDictionaryModalResultData { - name?: string; -} - -@customElement('umb-create-dictionary-modal-layout') -export class UmbCreateDictionaryModalLayoutElement extends UmbModalLayoutElement<UmbCreateDictionaryModalData> { +@customElement('umb-create-dictionary-modal') +export class UmbCreateDictionaryModalLayoutElement extends UmbModalBaseElement< + UmbCreateDictionaryModalData, + UmbCreateDictionaryModalResult +> { static styles = [UUITextStyles]; @query('#form') private _form!: HTMLFormElement; - #parentName?: string; + #parentName?: string; - connectedCallback() { - super.connectedCallback(); + connectedCallback() { + super.connectedCallback(); - if (this.data?.parentName) { - this.observe(this.data.parentName, (value) => this.#parentName = value); - } - } + if (this.data?.parentName) { + this.observe(this.data.parentName, (value) => (this.#parentName = value)); + } + } #handleCancel() { - this.modalHandler?.close({}); + this.modalHandler?.reject(); } #submitForm() { @@ -47,38 +41,40 @@ export class UmbCreateDictionaryModalLayoutElement extends UmbModalLayoutElement const formData = new FormData(form); - this.modalHandler?.close({ + this.modalHandler?.submit({ name: formData.get('name') as string, - }); + }); } render() { - return html` <umb-body-layout headline="Create"> - ${when(this.#parentName, () => html`<p>Create a dictionary item under <b>${this.#parentName}</b></p>`)} - <uui-form> - <form id="form" name="form" @submit=${this.#handleSubmit}> - <uui-form-layout-item> - <uui-label for="nameinput" slot="label" required>Name</uui-label> - <div> - <uui-input - type="text" - id="nameinput" - name="name" - label="name" - required - required-message="Name is required"></uui-input> - </div> - </uui-form-layout-item> - </form> - </uui-form> - <uui-button slot="actions" type="button" label="Close" @click=${this.#handleCancel}></uui-button> - <uui-button slot="actions" type="button" label="Create" look="primary" @click=${this.#submitForm}></uui-button> - </umb-body-layout>`; + return html` <umb-body-layout headline="Create"> + ${when(this.#parentName, () => html`<p>Create a dictionary item under <b>${this.#parentName}</b></p>`)} + <uui-form> + <form id="form" name="form" @submit=${this.#handleSubmit}> + <uui-form-layout-item> + <uui-label for="nameinput" slot="label" required>Name</uui-label> + <div> + <uui-input + type="text" + id="nameinput" + name="name" + label="name" + required + required-message="Name is required"></uui-input> + </div> + </uui-form-layout-item> + </form> + </uui-form> + <uui-button slot="actions" type="button" label="Close" @click=${this.#handleCancel}></uui-button> + <uui-button slot="actions" type="button" label="Create" look="primary" @click=${this.#submitForm}></uui-button> + </umb-body-layout>`; } } +export default UmbCreateDictionaryModalLayoutElement; + declare global { interface HTMLElementTagNameMap { - 'umb-create-dictionary-modal-layout': UmbCreateDictionaryModalLayoutElement; + 'umb-create-dictionary-modal': UmbCreateDictionaryModalLayoutElement; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/create/create.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/create/create.action.ts index 2560f5b2ea..2de1ddc2e9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/create/create.action.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/create/create.action.ts @@ -4,7 +4,7 @@ import { UmbSectionSidebarContext, UMB_SECTION_SIDEBAR_CONTEXT_TOKEN, } from '../../../../../backoffice/shared/components/section/section-sidebar/section-sidebar.context'; -import type { UmbCreateDictionaryModalResultData } from './create-dictionary-modal-layout.element'; +import { UMB_CREATE_DICTIONARY_MODAL_TOKEN } from '.'; import { UmbEntityActionBase } from '@umbraco-cms/entity-action'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbContextConsumerController } from '@umbraco-cms/context-api'; @@ -38,13 +38,13 @@ export default class UmbCreateDictionaryEntityAction extends UmbEntityActionBase // TODO: how can we get the current entity detail in the modal? Passing the observable // feels a bit hacky. Works, but hacky. - const modalHandler = this.#modalContext?.open('umb-create-dictionary-modal-layout', { - type: 'sidebar', - data: { unique: this.unique, parentName: this.#sectionSidebarContext.headline }, + const modalHandler = this.#modalContext?.open(UMB_CREATE_DICTIONARY_MODAL_TOKEN, { + unique: this.unique, + parentName: this.#sectionSidebarContext.headline, }); // TODO: get type from modal result - const { name }: UmbCreateDictionaryModalResultData = await modalHandler.onClose(); + const { name } = await modalHandler.onSubmit(); if (!name) return; const result = await this.repository?.create({ diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/create/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/create/index.ts new file mode 100644 index 0000000000..32139c6bd6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/create/index.ts @@ -0,0 +1,21 @@ +import { Observable } from 'rxjs'; +import { UmbModalToken } from '@umbraco-cms/modal'; + +// TODO: add interface for data +// PropertyTypeViewModelBaseModel +export interface UmbCreateDictionaryModalData { + unique: string | null; + parentName?: Observable<string | undefined>; +} + +export interface UmbCreateDictionaryModalResult { + name?: string; +} + +export const UMB_CREATE_DICTIONARY_MODAL_TOKEN = new UmbModalToken< + UmbCreateDictionaryModalData, + UmbCreateDictionaryModalResult +>('Umb.Modal.CreateDictionary', { + type: 'sidebar', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/export/export-dictionary-modal-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/export/export-dictionary-modal-layout.element.ts index 690cea3993..bce5dc282a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/export/export-dictionary-modal-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/export/export-dictionary-modal-layout.element.ts @@ -1,25 +1,21 @@ import { html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, query } from 'lit/decorators.js'; -import { UmbModalLayoutElement } from '@umbraco-cms/modal'; - -export interface UmbExportDictionaryModalData { - unique: string | null; -} - -export interface UmbExportDictionaryModalResultData { - includeChildren?: boolean; -} +import { UmbExportDictionaryModalData, UmbExportDictionaryModalResult } from '.'; +import { UmbModalBaseElement } from '@umbraco-cms/modal'; @customElement('umb-export-dictionary-modal-layout') -export class UmbExportDictionaryModalLayoutElement extends UmbModalLayoutElement<UmbExportDictionaryModalData> { +export class UmbExportDictionaryModalLayoutElement extends UmbModalBaseElement< + UmbExportDictionaryModalData, + UmbExportDictionaryModalResult +> { static styles = [UUITextStyles]; @query('#form') private _form!: HTMLFormElement; #handleClose() { - this.modalHandler?.close({}); + this.modalHandler?.reject(); } #submitForm() { @@ -34,7 +30,7 @@ export class UmbExportDictionaryModalLayoutElement extends UmbModalLayoutElement const formData = new FormData(form); - this.modalHandler?.close({ includeChildren: (formData.get('includeDescendants') as string) === 'on' }); + this.modalHandler?.submit({ includeChildren: (formData.get('includeDescendants') as string) === 'on' }); } render() { @@ -53,6 +49,8 @@ export class UmbExportDictionaryModalLayoutElement extends UmbModalLayoutElement } } +export default UmbExportDictionaryModalLayoutElement; + declare global { interface HTMLElementTagNameMap { 'umb-export-dictionary-modal-layout': UmbExportDictionaryModalLayoutElement; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/export/export.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/export/export.action.ts index 8b50b1b2a9..de6d5bcc4c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/export/export.action.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/export/export.action.ts @@ -1,6 +1,6 @@ import { UUITextStyles } from '@umbraco-ui/uui-css'; import { UmbDictionaryRepository } from '../../repository/dictionary.repository'; -import type { UmbExportDictionaryModalResultData } from './export-dictionary-modal-layout.element'; +import { UMB_EXPORT_DICTIONARY_MODAL_TOKEN } from '.'; import { UmbEntityActionBase } from '@umbraco-cms/entity-action'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbContextConsumerController } from '@umbraco-cms/context-api'; @@ -25,13 +25,10 @@ export default class UmbExportDictionaryEntityAction extends UmbEntityActionBase // TODO: what to do if modal service is not available? if (!this.#modalContext) return; - const modalHandler = this.#modalContext?.open('umb-export-dictionary-modal-layout', { - type: 'sidebar', - data: { unique: this.unique }, - }); + const modalHandler = this.#modalContext?.open(UMB_EXPORT_DICTIONARY_MODAL_TOKEN, { unique: this.unique }); // TODO: get type from modal result - const { includeChildren }: UmbExportDictionaryModalResultData = await modalHandler.onClose(); + const { includeChildren } = await modalHandler.onSubmit(); if (includeChildren === undefined) return; const result = await this.repository?.export(this.unique, includeChildren); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/export/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/export/index.ts new file mode 100644 index 0000000000..891165f3c0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/export/index.ts @@ -0,0 +1,17 @@ +import { UmbModalToken } from '@umbraco-cms/modal'; + +export interface UmbExportDictionaryModalData { + unique: string | null; +} + +export interface UmbExportDictionaryModalResult { + includeChildren?: boolean; +} + +export const UMB_EXPORT_DICTIONARY_MODAL_TOKEN = new UmbModalToken< + UmbExportDictionaryModalData, + UmbExportDictionaryModalResult +>('Umb.Modal.ExportDictionary', { + type: 'sidebar', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/import/import-dictionary-modal-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/import/import-dictionary-modal-layout.element.ts index a2fe92a240..b178a42ac3 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/import/import-dictionary-modal-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/import/import-dictionary-modal-layout.element.ts @@ -5,20 +5,15 @@ import { when } from 'lit-html/directives/when.js'; import { repeat } from 'lit/directives/repeat.js'; import { UmbTreeElement } from '../../../../shared/components/tree/tree.element'; import { UmbDictionaryRepository } from '../../repository/dictionary.repository'; +import { UmbImportDictionaryModalData, UmbImportDictionaryModalResult } from '.'; import { DictionaryUploadModel } from '@umbraco-cms/backend-api'; -import { UmbModalLayoutElement } from '@umbraco-cms/modal'; - -export interface UmbImportDictionaryModalData { - unique: string | null; -} - -export interface UmbImportDictionaryModalResultData { - fileName?: string; - parentKey?: string; -} +import { UmbModalBaseElement } from '@umbraco-cms/modal'; @customElement('umb-import-dictionary-modal-layout') -export class UmbImportDictionaryModalLayoutElement extends UmbModalLayoutElement<UmbImportDictionaryModalData> { +export class UmbImportDictionaryModalLayoutElement extends UmbModalBaseElement< + UmbImportDictionaryModalData, + UmbImportDictionaryModalResult +> { static styles = [ UUITextStyles, css` @@ -51,14 +46,14 @@ export class UmbImportDictionaryModalLayoutElement extends UmbModalLayoutElement async #importDictionary() { if (!this._uploadedDictionary?.fileName) return; - this.modalHandler?.close({ + this.modalHandler?.submit({ fileName: this._uploadedDictionary.fileName, parentKey: this._selection[0], }); } #handleClose() { - this.modalHandler?.close({}); + this.modalHandler?.reject(); } #submitForm() { @@ -138,7 +133,12 @@ export class UmbImportDictionaryModalLayoutElement extends UmbModalLayoutElement selectable></umb-tree> <uui-button slot="actions" type="button" label="Cancel" @click=${this.#handleClose}></uui-button> - <uui-button slot="actions" type="button" label="Import" look="primary" @click=${this.#importDictionary}></uui-button> + <uui-button + slot="actions" + type="button" + label="Import" + look="primary" + @click=${this.#importDictionary}></uui-button> `; } @@ -156,6 +156,8 @@ export class UmbImportDictionaryModalLayoutElement extends UmbModalLayoutElement } } +export default UmbImportDictionaryModalLayoutElement; + declare global { interface HTMLElementTagNameMap { 'umb-import-dictionary-modal-layout': UmbImportDictionaryModalLayoutElement; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/import/import.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/import/import.action.ts index 98a14e6e9e..b8d5d411b2 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/import/import.action.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/import/import.action.ts @@ -1,6 +1,6 @@ import { UUITextStyles } from '@umbraco-ui/uui-css'; import { UmbDictionaryRepository } from '../../repository/dictionary.repository'; -import type { UmbImportDictionaryModalResultData } from './import-dictionary-modal-layout.element'; +import { UMB_IMPORT_DICTIONARY_MODAL_TOKEN } from '.'; import { UmbEntityActionBase } from '@umbraco-cms/entity-action'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbContextConsumerController } from '@umbraco-cms/context-api'; @@ -25,13 +25,10 @@ export default class UmbImportDictionaryEntityAction extends UmbEntityActionBase // TODO: what to do if modal service is not available? if (!this.#modalContext) return; - const modalHandler = this.#modalContext?.open('umb-import-dictionary-modal-layout', { - type: 'sidebar', - data: { unique: this.unique }, - }); + const modalHandler = this.#modalContext?.open(UMB_IMPORT_DICTIONARY_MODAL_TOKEN, { unique: this.unique }); // TODO: get type from modal result - const { fileName, parentKey }: UmbImportDictionaryModalResultData = await modalHandler.onClose(); + const { fileName, parentKey } = await modalHandler.onSubmit(); if (!fileName) return; const result = await this.repository?.import(fileName, parentKey); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/import/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/import/index.ts new file mode 100644 index 0000000000..41f2b60917 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/import/index.ts @@ -0,0 +1,20 @@ +import { UmbModalToken } from '@umbraco-cms/modal'; + +// TODO: add interface for data +// PropertyTypeViewModelBaseModel +export interface UmbImportDictionaryModalData { + unique: string | null; +} + +export interface UmbImportDictionaryModalResult { + fileName?: string; + parentKey?: string; +} + +export const UMB_IMPORT_DICTIONARY_MODAL_TOKEN = new UmbModalToken< + UmbImportDictionaryModalData, + UmbImportDictionaryModalResult +>('Umb.Modal.ImportDictionary', { + type: 'sidebar', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/manifests.ts index 176e975a92..7e75f56e94 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/manifests.ts @@ -4,7 +4,7 @@ import UmbImportDictionaryEntityAction from './import/import.action'; import UmbExportDictionaryEntityAction from './export/export.action'; import UmbCreateDictionaryEntityAction from './create/create.action'; import { UmbDeleteEntityAction, UmbMoveEntityAction } from '@umbraco-cms/entity-action'; -import type { ManifestEntityAction } from '@umbraco-cms/models'; +import type { ManifestEntityAction, ManifestModal } from '@umbraco-cms/models'; const entityType = 'dictionary-item'; const repositoryAlias = DICTIONARY_REPOSITORY_ALIAS; @@ -90,4 +90,25 @@ const entityActions: Array<ManifestEntityAction> = [ }, ]; -export const manifests = [...entityActions]; +const modals: Array<ManifestModal> = [ + { + type: 'modal', + alias: 'Umb.Modal.CreateDictionary', + name: 'Create Dictionary Modal', + loader: () => import('./create/create-dictionary-modal-layout.element'), + }, + { + type: 'modal', + alias: 'Umb.Modal.ExportDictionary', + name: 'Export Dictionary Modal', + loader: () => import('./export/export-dictionary-modal-layout.element'), + }, + { + type: 'modal', + alias: 'Umb.Modal.ImportDictionary', + name: 'Import Dictionary Modal', + loader: () => import('./import/import-dictionary-modal-layout.element'), + }, +]; + +export const manifests = [...entityActions, ...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts index 745e1ad8b4..2618c22ffd 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts @@ -1,7 +1,7 @@ import { DictionaryTreeServerDataSource } from './sources/dictionary.tree.server.data'; import { UmbDictionaryTreeStore, UMB_DICTIONARY_TREE_STORE_CONTEXT_TOKEN } from './dictionary.tree.store'; import { UmbDictionaryDetailServerDataSource } from './sources/dictionary.detail.server.data'; -import { UmbDictionaryDetailStore, UMB_DICTIONARY_DETAIL_STORE_CONTEXT_TOKEN } from './dictionary.detail.store'; +import { UmbDictionaryStore, UMB_DICTIONARY_STORE_CONTEXT_TOKEN } from './dictionary.store'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbContextConsumerController } from '@umbraco-cms/context-api'; import { RepositoryTreeDataSource, UmbDetailRepository, UmbTreeRepository } from '@umbraco-cms/repository'; @@ -18,7 +18,7 @@ export class UmbDictionaryRepository implements UmbTreeRepository, UmbDetailRepo #treeStore?: UmbDictionaryTreeStore; #detailSource: UmbDictionaryDetailServerDataSource; - #detailStore?: UmbDictionaryDetailStore; + #detailStore?: UmbDictionaryStore; #notificationContext?: UmbNotificationContext; @@ -30,7 +30,7 @@ export class UmbDictionaryRepository implements UmbTreeRepository, UmbDetailRepo this.#detailSource = new UmbDictionaryDetailServerDataSource(this.#host); this.#init = Promise.all([ - new UmbContextConsumerController(this.#host, UMB_DICTIONARY_DETAIL_STORE_CONTEXT_TOKEN, (instance) => { + new UmbContextConsumerController(this.#host, UMB_DICTIONARY_STORE_CONTEXT_TOKEN, (instance) => { this.#detailStore = instance; }), diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.store.ts similarity index 63% rename from src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.detail.store.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.store.ts index e9a7962335..bf72879c7e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.detail.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.store.ts @@ -6,17 +6,15 @@ import type { DictionaryDetails } from '@umbraco-cms/models'; /** * @export - * @class UmbDictionaryDetailStore + * @class UmbDictionaryStore * @extends {UmbStoreBase} - * @description - Details Data Store for Dictionary + * @description - Data Store for Dictionary */ -export class UmbDictionaryDetailStore - extends UmbStoreBase -{ +export class UmbDictionaryStore extends UmbStoreBase { #data = new ArrayState<DictionaryDetails>([], (x) => x.key); constructor(host: UmbControllerHostInterface) { - super(host, UMB_DICTIONARY_DETAIL_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_DICTIONARY_STORE_CONTEXT_TOKEN.toString()); } append(dictionary: DictionaryDetails) { @@ -28,6 +26,4 @@ export class UmbDictionaryDetailStore } } -export const UMB_DICTIONARY_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbDictionaryDetailStore>( - 'UmbDictionaryDetailStore' -); +export const UMB_DICTIONARY_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbDictionaryStore>('UmbDictionaryStore'); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.tree.store.ts index 8a3dcb7310..743235f01b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.tree.store.ts @@ -9,7 +9,6 @@ import { UmbControllerHostInterface } from '@umbraco-cms/controller'; * @description - Tree Data Store for Dictionary */ export class UmbDictionaryTreeStore extends UmbTreeStoreBase { - /** * Creates an instance of UmbDictionaryTreeStore. * @param {UmbControllerHostInterface} host diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/manifests.ts index 3e900b29a5..0309458660 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/manifests.ts @@ -1,5 +1,8 @@ import { UmbDictionaryRepository } from '../repository/dictionary.repository'; +import { UmbDictionaryTreeStore } from './dictionary.tree.store'; +import { UmbDictionaryStore } from './dictionary.store'; import { ManifestRepository } from 'libs/extensions-registry/repository.models'; +import { ManifestStore, ManifestTreeStore } from '@umbraco-cms/extensions-registry'; export const DICTIONARY_REPOSITORY_ALIAS = 'Umb.Repository.Dictionary'; @@ -10,4 +13,21 @@ const repository: ManifestRepository = { class: UmbDictionaryRepository, }; -export const manifests = [repository]; +export const DICTIONARY_STORE_ALIAS = 'Umb.Store.Dictionary'; +export const DICTIONARY_TREE_STORE_ALIAS = 'Umb.Store.DictionaryTree'; + +const store: ManifestStore = { + type: 'store', + alias: DICTIONARY_STORE_ALIAS, + name: 'Dictionary Store', + class: UmbDictionaryStore, +}; + +const treeStore: ManifestTreeStore = { + type: 'treeStore', + alias: DICTIONARY_TREE_STORE_ALIAS, + name: 'Dictionary Tree Store', + class: UmbDictionaryTreeStore, +}; + +export const manifests = [repository, store, treeStore]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/index.ts index 669dd79c55..7712683eb5 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/index.ts @@ -3,11 +3,10 @@ import { manifests as dictionaryManifests } from './dictionary/manifests'; import type { ManifestTypes } from '@umbraco-cms/models'; import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; +export const manifests = [...translationSectionManifests, ...dictionaryManifests]; + const registerExtensions = (manifests: Array<ManifestTypes>) => { - manifests.forEach((manifest) => { - if (umbExtensionsRegistry.isRegistered(manifest.alias)) return; - umbExtensionsRegistry.register(manifest); - }); + manifests.forEach((manifest) => umbExtensionsRegistry.register(manifest)); }; -registerExtensions([...translationSectionManifests, ...dictionaryManifests]); +registerExtensions(manifests); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/current-user-header-app.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/current-user-header-app.element.ts index 1f8451c341..974928025b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/current-user-header-app.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/current-user-header-app.element.ts @@ -2,6 +2,7 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, CSSResultGroup, html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { UmbCurrentUserStore, UMB_CURRENT_USER_STORE_CONTEXT_TOKEN } from './current-user.store'; +import { UMB_CURRENT_USER_MODAL_TOKEN } from './modals/current-user'; import type { UserDetails } from '@umbraco-cms/models'; import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; import { UmbLitElement } from '@umbraco-cms/element'; @@ -46,7 +47,7 @@ export class UmbCurrentUserHeaderApp extends UmbLitElement { } private _handleUserClick() { - this._modalContext?.userSettings(); + this._modalContext?.open(UMB_CURRENT_USER_MODAL_TOKEN); } render() { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/manifests.ts index 63d58c00a4..acff072136 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/manifests.ts @@ -1,3 +1,4 @@ +import { manifests as modalManifests } from './modals/manifests'; import type { ManifestHeaderApp, ManifestUserDashboard } from '@umbraco-cms/models'; export const userDashboards: Array<ManifestUserDashboard> = [ @@ -29,4 +30,4 @@ export const headerApps: Array<ManifestHeaderApp> = [ }, ]; -export const manifests = [...userDashboards, ...headerApps]; +export const manifests = [...userDashboards, ...headerApps, ...modalManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout-change-password.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/modals/change-password/change-password-modal.element.ts similarity index 88% rename from src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout-change-password.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/modals/change-password/change-password-modal.element.ts index 927af7e900..2440cb743a 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout-change-password.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/modals/change-password/change-password-modal.element.ts @@ -1,15 +1,12 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, CSSResultGroup, html, nothing } from 'lit'; import { customElement, property } from 'lit/decorators.js'; -import { UmbModalHandler } from '..'; +import { UmbChangePasswordModalData } from '.'; +import { UmbModalHandler } from '@umbraco-cms/modal'; import { UmbLitElement } from '@umbraco-cms/element'; -export interface UmbModalChangePasswordData { - requireOldPassword: boolean; -} - -@customElement('umb-modal-layout-change-password') -export class UmbModalLayoutChangePasswordElement extends UmbLitElement { +@customElement('umb-change-password-modal') +export class UmbChangePasswordModalElement extends UmbLitElement { static styles: CSSResultGroup = [ UUITextStyles, css` @@ -32,10 +29,10 @@ export class UmbModalLayoutChangePasswordElement extends UmbLitElement { modalHandler?: UmbModalHandler; @property() - data?: UmbModalChangePasswordData; + data?: UmbChangePasswordModalData; private _close() { - this.modalHandler?.close(); + this.modalHandler?.submit(); } private _handleSubmit(e: SubmitEvent) { @@ -105,8 +102,10 @@ export class UmbModalLayoutChangePasswordElement extends UmbLitElement { } } +export default UmbChangePasswordModalElement; + declare global { interface HTMLElementTagNameMap { - 'umb-modal-layout-change-password': UmbModalLayoutChangePasswordElement; + 'umb-change-password-modal': UmbChangePasswordModalElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/modals/change-password/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/modals/change-password/index.ts new file mode 100644 index 0000000000..6a3504c607 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/modals/change-password/index.ts @@ -0,0 +1,12 @@ +import { UmbModalToken } from '@umbraco-cms/modal'; + +export interface UmbChangePasswordModalData { + requireOldPassword: boolean; +} + +export const UMB_CHANGE_PASSWORD_MODAL_TOKEN = new UmbModalToken<UmbChangePasswordModalData>( + 'Umb.Modal.ChangePassword', + { + type: 'dialog', + } +); diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout-current-user.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/modals/current-user/current-user-modal.element.ts similarity index 89% rename from src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout-current-user.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/modals/current-user/current-user-modal.element.ts index e8bafe3cb1..e64252a10d 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout-current-user.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/modals/current-user/current-user-modal.element.ts @@ -1,21 +1,19 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, CSSResultGroup, html, nothing } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; -import { UmbModalHandler, UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '..'; import { UmbCurrentUserHistoryStore, UmbCurrentUserHistoryItem, UMB_CURRENT_USER_HISTORY_STORE_CONTEXT_TOKEN, -} from '../../../backoffice/users/current-user/current-user-history.store'; -import { - UmbCurrentUserStore, - UMB_CURRENT_USER_STORE_CONTEXT_TOKEN, -} from '../../../backoffice/users/current-user/current-user.store'; +} from '../../current-user-history.store'; +import { UmbCurrentUserStore, UMB_CURRENT_USER_STORE_CONTEXT_TOKEN } from '../../current-user.store'; +import { UMB_CHANGE_PASSWORD_MODAL_TOKEN } from '../change-password'; +import { UmbModalHandler, UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; import type { UserDetails } from '@umbraco-cms/models'; import { UmbLitElement } from '@umbraco-cms/element'; -@customElement('umb-modal-layout-current-user') -export class UmbModalLayoutCurrentUserElement extends UmbLitElement { +@customElement('umb-current-user-modal') +export class UmbCurrentUserModalElement extends UmbLitElement { static styles: CSSResultGroup = [ UUITextStyles, css` @@ -128,7 +126,7 @@ export class UmbModalLayoutCurrentUserElement extends UmbLitElement { } private _close() { - this.modalHandler?.close(); + this.modalHandler?.submit(); } private _edit() { @@ -141,7 +139,9 @@ export class UmbModalLayoutCurrentUserElement extends UmbLitElement { private _changePassword() { if (!this._modalContext) return; - this._modalContext.changePassword({ requireOldPassword: this._currentUserStore?.isAdmin || false }); + this._modalContext.open(UMB_CHANGE_PASSWORD_MODAL_TOKEN, { + requireOldPassword: this._currentUserStore?.isAdmin || false, + }); } private _renderHistoryItem(item: UmbCurrentUserHistoryItem) { @@ -202,8 +202,10 @@ export class UmbModalLayoutCurrentUserElement extends UmbLitElement { } } +export default UmbCurrentUserModalElement; + declare global { interface HTMLElementTagNameMap { - 'umb-modal-layout-current-user': UmbModalLayoutCurrentUserElement; + 'umb-current-user-modal': UmbCurrentUserModalElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/modals/current-user/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/modals/current-user/index.ts new file mode 100644 index 0000000000..978eeb535c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/modals/current-user/index.ts @@ -0,0 +1,6 @@ +import { UmbModalToken } from '@umbraco-cms/modal'; + +export const UMB_CURRENT_USER_MODAL_TOKEN = new UmbModalToken('Umb.Modal.CurrentUser', { + type: 'sidebar', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/modals/manifests.ts new file mode 100644 index 0000000000..68cc073e78 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/modals/manifests.ts @@ -0,0 +1,12 @@ +import type { ManifestModal } from '@umbraco-cms/extensions-registry'; + +const modals: Array<ManifestModal> = [ + { + type: 'modal', + alias: 'Umb.Modal.CurrentUser', + name: 'Current User Modal', + loader: () => import('./current-user/current-user-modal.element'), + }, +]; + +export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/index.ts index e799a8eff7..03d81407b4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/index.ts @@ -6,11 +6,10 @@ import { manifests as currentUserManifests } from './current-user/manifests'; import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; import { ManifestTypes } from '@umbraco-cms/extensions-registry'; +export const manifests = [...userGroupManifests, ...userManifests, ...userSectionManifests, ...currentUserManifests]; + const registerExtensions = (manifests: Array<ManifestTypes>) => { - manifests.forEach((manifest) => { - if (umbExtensionsRegistry.isRegistered(manifest.alias)) return; - umbExtensionsRegistry.register(manifest); - }); + manifests.forEach((manifest) => umbExtensionsRegistry.register(manifest)); }; -registerExtensions([...userGroupManifests, ...userManifests, ...userSectionManifests, ...currentUserManifests]); +registerExtensions(manifests); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/manifests.ts index ea4ed7ce8a..dd709645d0 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/manifests.ts @@ -1,3 +1,5 @@ +import { manifests as repositoryManifests } from './repository/manifests'; import { manifests as workspaceManifests } from './workspace/manifests'; +import { manifests as modalManifests } from './modals/manifests'; -export const manifests = [...workspaceManifests]; +export const manifests = [...repositoryManifests, ...workspaceManifests, ...modalManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/modals/manifests.ts new file mode 100644 index 0000000000..afd2fe55b9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/modals/manifests.ts @@ -0,0 +1,12 @@ +import type { ManifestModal } from '@umbraco-cms/extensions-registry'; + +const modals: Array<ManifestModal> = [ + { + type: 'modal', + alias: 'Umb.Modal.UserGroupPicker', + name: 'User Group Picker Modal', + loader: () => import('./user-group-picker/user-group-picker-modal.element'), + }, +]; + +export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/modals/user-group-picker/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/modals/user-group-picker/index.ts new file mode 100644 index 0000000000..b56c013f03 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/modals/user-group-picker/index.ts @@ -0,0 +1,10 @@ +import type { UserDetails } from '@umbraco-cms/models'; +import { UmbModalToken, UmbPickerModalData } from '@umbraco-cms/modal'; + +export const UMB_USER_GROUP_PICKER_MODAL_TOKEN = new UmbModalToken<UmbPickerModalData<UserDetails>>( + 'Umb.Modal.UserGroupPicker', + { + type: 'sidebar', + size: 'small', + } +); diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user-group/picker-layout-user-group.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/modals/user-group-picker/user-group-picker-modal.element.ts similarity index 84% rename from src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user-group/picker-layout-user-group.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/modals/user-group-picker/user-group-picker-modal.element.ts index 9d103d4d19..05332be801 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user-group/picker-layout-user-group.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/modals/user-group-picker/user-group-picker-modal.element.ts @@ -1,13 +1,12 @@ import { UUITextStyles } from '@umbraco-ui/uui-css'; import { css, html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; -import { UmbModalLayoutPickerBase } from '../modal-layout-picker-base'; -import { UMB_USER_GROUP_STORE_CONTEXT_TOKEN } from '../../../../backoffice/users/user-groups/user-group.store'; -import type { UmbUserGroupStore } from '../../../../backoffice/users/user-groups/user-group.store'; +import { UmbUserGroupStore, UMB_USER_GROUP_STORE_CONTEXT_TOKEN } from '../../repository/user-group.store'; +import { UmbModalElementPickerBase } from '@umbraco-cms/modal'; import type { UserGroupDetails } from '@umbraco-cms/models'; -@customElement('umb-picker-layout-user-group') -export class UmbPickerLayoutUserGroupElement extends UmbModalLayoutPickerBase<UserGroupDetails> { +@customElement('umb-user-group-picker-modal') +export class UmbUserGroupPickerModalElement extends UmbModalElementPickerBase<UserGroupDetails> { static styles = [ UUITextStyles, css` @@ -101,8 +100,10 @@ export class UmbPickerLayoutUserGroupElement extends UmbModalLayoutPickerBase<Us } } +export default UmbUserGroupPickerModalElement; + declare global { interface HTMLElementTagNameMap { - 'umb-picker-layout-user-group': UmbPickerLayoutUserGroupElement; + 'umb-user-group-picker-modal': UmbUserGroupPickerModalElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user-group/picker-layout-user-group.test.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/modals/user-group-picker/user-group-picker-modal.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user-group/picker-layout-user-group.test.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/modals/user-group-picker/user-group-picker-modal.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/repository/manifests.ts new file mode 100644 index 0000000000..bf2a3e442c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/repository/manifests.ts @@ -0,0 +1,23 @@ +import { UmbUserGroupRepository } from '../repository/user-group.repository'; +import { UmbUserGroupStore } from './user-group.store'; +import type { ManifestStore, ManifestRepository } from '@umbraco-cms/extensions-registry'; + +export const USER_GROUP_REPOSITORY_ALIAS = 'Umb.Repository.UserGroup'; + +const repository: ManifestRepository = { + type: 'repository', + alias: USER_GROUP_REPOSITORY_ALIAS, + name: 'User Group Repository', + class: UmbUserGroupRepository, +}; + +export const USER_GROUP_STORE_ALIAS = 'Umb.Store.UserGroup'; + +const store: ManifestStore = { + type: 'store', + alias: USER_GROUP_STORE_ALIAS, + name: 'User Group Store', + class: UmbUserGroupStore, +}; + +export const manifests = [repository, store]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/user-group.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/repository/user-group.store.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/user-group.store.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/repository/user-group.store.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/workspace/user-group-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/workspace/user-group-workspace.context.ts index 56ca9c313e..bdf1d7f7ac 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/workspace/user-group-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/workspace/user-group-workspace.context.ts @@ -1,7 +1,7 @@ import { UmbEntityWorkspaceManager } from '../../../shared/components/workspace/workspace-context/entity-manager-controller'; import { UmbWorkspaceContext } from '../../../shared/components/workspace/workspace-context/workspace-context'; import { UmbWorkspaceEntityContextInterface } from '../../../shared/components/workspace/workspace-context/workspace-entity-context.interface'; -import { UMB_USER_GROUP_STORE_CONTEXT_TOKEN } from '../user-group.store'; +import { UMB_USER_GROUP_STORE_CONTEXT_TOKEN } from '../repository/user-group.store'; import { UmbUserGroupRepository } from '../repository/user-group.repository'; import type { UserGroupDetails } from '@umbraco-cms/models'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/workspace/user-group-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/workspace/user-group-workspace.element.ts index 43d00f80d4..733034d8a4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/workspace/user-group-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/workspace/user-group-workspace.element.ts @@ -3,7 +3,7 @@ import { UUITextStyles } from '@umbraco-ui/uui-css'; import { css, html, nothing } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; -import { UmbUserStore, UMB_USER_STORE_CONTEXT_TOKEN } from '../../../users/users/user.store'; +import { UmbUserStore, UMB_USER_STORE_CONTEXT_TOKEN } from '../../users/repository/user.store'; import { UmbWorkspaceEntityElement } from '../../../shared/components/workspace/workspace-entity-element.interface'; import { UmbWorkspaceUserGroupContext } from './user-group-workspace.context'; import { UmbSaveWorkspaceAction } from '@umbraco-cms/workspace'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/user-groups/workspace-view-user-groups.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/user-groups/workspace-view-user-groups.element.ts index 9d3f8ac467..9c2e101940 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/user-groups/workspace-view-user-groups.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/user-groups/workspace-view-user-groups.element.ts @@ -17,7 +17,7 @@ import './user-group-table-sections-column-layout.element'; import { UmbUserGroupStore, UMB_USER_GROUP_STORE_CONTEXT_TOKEN, -} from 'src/backoffice/users/user-groups/user-group.store'; +} from 'src/backoffice/users/user-groups/repository/user-group.store'; import { UmbLitElement } from '@umbraco-cms/element'; @customElement('umb-workspace-view-user-groups') diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/list-view-layouts/grid/workspace-view-users-grid.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/list-view-layouts/grid/workspace-view-users-grid.element.ts index c484e7ec8a..a8c88c9605 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/list-view-layouts/grid/workspace-view-users-grid.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/list-view-layouts/grid/workspace-view-users-grid.element.ts @@ -4,7 +4,10 @@ import { customElement, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; import { ifDefined } from 'lit-html/directives/if-defined.js'; import type { UmbSectionViewUsersElement } from '../../section-view-users.element'; -import { UmbUserGroupStore, UMB_USER_GROUP_STORE_CONTEXT_TOKEN } from '../../../../../user-groups/user-group.store'; +import { + UmbUserGroupStore, + UMB_USER_GROUP_STORE_CONTEXT_TOKEN, +} from '../../../../../user-groups/repository/user-group.store'; import { getLookAndColorFromUserStatus } from '@umbraco-cms/utils'; import type { UserDetails, UserEntity, UserGroupEntity } from '@umbraco-cms/models'; import { UmbLitElement } from '@umbraco-cms/element'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/list-view-layouts/table/workspace-view-users-table.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/list-view-layouts/table/workspace-view-users-table.element.ts index bcd3ae99fd..eb997c2d60 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/list-view-layouts/table/workspace-view-users-table.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/list-view-layouts/table/workspace-view-users-table.element.ts @@ -18,7 +18,7 @@ import './column-layouts/status/user-table-status-column-layout.element'; import { UmbUserGroupStore, UMB_USER_GROUP_STORE_CONTEXT_TOKEN, -} from 'src/backoffice/users/user-groups/user-group.store'; +} from 'src/backoffice/users/user-groups/repository/user-group.store'; import { UmbLitElement } from '@umbraco-cms/element'; @customElement('umb-workspace-view-users-table') diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/section-view-users.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/section-view-users.element.ts index f5dda922fa..5985ea2851 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/section-view-users.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/section-view-users.element.ts @@ -1,14 +1,14 @@ import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, state } from 'lit/decorators.js'; +import { UmbUserStore, UMB_USER_STORE_CONTEXT_TOKEN } from '../../../users/repository/user.store'; import type { IRoute, IRoutingInfo } from '@umbraco-cms/router'; -import { UmbUserStore, UMB_USER_STORE_CONTEXT_TOKEN } from '../../../users/user.store'; import { umbExtensionsRegistry, createExtensionElement } from '@umbraco-cms/extensions-api'; import './list-view-layouts/table/workspace-view-users-table.element'; import './list-view-layouts/grid/workspace-view-users-grid.element'; import './workspace-view-users-selection.element'; -import './workspace-view-users-invite.element'; + import type { ManifestWorkspace, UserDetails } from '@umbraco-cms/models'; import { UmbLitElement } from '@umbraco-cms/element'; import { DeepState } from '@umbraco-cms/observable-api'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/workspace-view-users-overview.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/workspace-view-users-overview.element.ts index 2835d003d2..ce73c18cc5 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/workspace-view-users-overview.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/workspace-view-users-overview.element.ts @@ -1,18 +1,18 @@ import { css, html, nothing } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, state } from 'lit/decorators.js'; -import type { IRoute } from '@umbraco-cms/router'; import { UUIPopoverElement } from '@umbraco-ui/uui'; - -import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '../../../../../core/modal'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; +import { UMB_INVITE_USER_MODAL_TOKEN } from '../../../../users/users/modals/invite-user'; +import { UMB_CREATE_USER_MODAL_TOKEN } from '../../../../users/users/modals/create-user'; import type { UmbSectionViewUsersElement } from './section-view-users.element'; +import type { IRoute } from '@umbraco-cms/router'; + import { UmbLitElement } from '@umbraco-cms/element'; import './list-view-layouts/table/workspace-view-users-table.element'; import './list-view-layouts/grid/workspace-view-users-grid.element'; import './workspace-view-users-selection.element'; -import './workspace-view-users-invite.element'; -import './workspace-view-users-create.element'; export type UsersViewType = 'list' | 'grid'; @customElement('umb-workspace-view-users-overview') @@ -160,13 +160,15 @@ export class UmbWorkspaceViewUsersOverviewElement extends UmbLitElement { } private _showInviteOrCreate() { - let modal = undefined; + let token = undefined; + // TODO: we need to find a better way to determine if we should create or invite if (this.isCloud) { - modal = document.createElement('umb-workspace-view-users-invite'); + token = UMB_INVITE_USER_MODAL_TOKEN; } else { - modal = document.createElement('umb-workspace-view-users-create'); + token = UMB_CREATE_USER_MODAL_TOKEN; } - this._modalContext?.open(modal, { type: 'dialog' }); + + this._modalContext?.open(token); } render() { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/workspace-view-users-selection.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/workspace-view-users-selection.element.ts index 9de0d77569..8a293d92da 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/workspace-view-users-selection.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/workspace-view-users-selection.element.ts @@ -2,7 +2,7 @@ import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, state } from 'lit/decorators.js'; import { UmbSectionViewUsersElement } from './section-view-users.element'; -import { UmbUserStore, UMB_USER_STORE_CONTEXT_TOKEN } from 'src/backoffice/users/users/user.store'; +import { UmbUserStore, UMB_USER_STORE_CONTEXT_TOKEN } from 'src/backoffice/users/users/repository/user.store'; import { UmbLitElement } from '@umbraco-cms/element'; @customElement('umb-workspace-view-users-selection') diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/manifests.ts index ea4ed7ce8a..dd709645d0 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/manifests.ts @@ -1,3 +1,5 @@ +import { manifests as repositoryManifests } from './repository/manifests'; import { manifests as workspaceManifests } from './workspace/manifests'; +import { manifests as modalManifests } from './modals/manifests'; -export const manifests = [...workspaceManifests]; +export const manifests = [...repositoryManifests, ...workspaceManifests, ...modalManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/workspace-view-users-create.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/create-user/create-user-modal.element.ts similarity index 94% rename from src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/workspace-view-users-create.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/create-user/create-user-modal.element.ts index d5aa8702e4..a17a870036 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/workspace-view-users-create.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/create-user/create-user-modal.element.ts @@ -3,8 +3,8 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, query, state } from 'lit/decorators.js'; import { UUIInputPasswordElement } from '@umbraco-ui/uui'; import { UmbInputPickerUserGroupElement } from '../../../../shared/components/input-user-group/input-user-group.element'; -import { UmbModalLayoutElement } from '../../../../../core/modal'; -import { UmbUserStore, UMB_USER_STORE_CONTEXT_TOKEN } from '../../../users/user.store'; +import { UmbUserStore, UMB_USER_STORE_CONTEXT_TOKEN } from '../../repository/user.store'; +import { UmbModalBaseElement } from '@umbraco-cms/modal'; import type { UserDetails } from '@umbraco-cms/models'; import { UmbNotificationDefaultData, @@ -13,8 +13,8 @@ import { } from '@umbraco-cms/notification'; export type UsersViewType = 'list' | 'grid'; -@customElement('umb-workspace-view-users-create') -export class UmbWorkspaceViewUsersCreateElement extends UmbModalLayoutElement { +@customElement('umb-create-user-modal') +export class UmbCreateUserModalElement extends UmbModalBaseElement { static styles = [ UUITextStyles, css` @@ -115,7 +115,7 @@ export class UmbWorkspaceViewUsersCreateElement extends UmbModalLayoutElement { } private _closeModal() { - this.modalHandler?.close(); + this.modalHandler?.reject(); } private _resetForm() { @@ -210,10 +210,10 @@ export class UmbWorkspaceViewUsersCreateElement extends UmbModalLayoutElement { } } -export default UmbWorkspaceViewUsersCreateElement; +export default UmbCreateUserModalElement; declare global { interface HTMLElementTagNameMap { - 'umb-workspace-view-users-create': UmbWorkspaceViewUsersCreateElement; + 'umb-create-user-modal': UmbCreateUserModalElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/workspace-view-users-create.test.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/create-user/create-user-modal.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/workspace-view-users-create.test.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/create-user/create-user-modal.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/create-user/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/create-user/index.ts new file mode 100644 index 0000000000..7b8ff0d726 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/create-user/index.ts @@ -0,0 +1,6 @@ +import { UmbModalToken } from '@umbraco-cms/modal'; + +export const UMB_CREATE_USER_MODAL_TOKEN = new UmbModalToken('Umb.Modal.CreateUser', { + type: 'dialog', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/invite-user/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/invite-user/index.ts new file mode 100644 index 0000000000..5c9a70ee64 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/invite-user/index.ts @@ -0,0 +1,6 @@ +import { UmbModalToken } from '@umbraco-cms/modal'; + +export const UMB_INVITE_USER_MODAL_TOKEN = new UmbModalToken('Umb.Modal.InviteUser', { + type: 'dialog', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/workspace-view-users-invite.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/invite-user/invite-user-modal.element.ts similarity index 93% rename from src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/workspace-view-users-invite.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/invite-user/invite-user-modal.element.ts index 6a5e16aca7..a01e344bde 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/workspace-view-users-invite.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/invite-user/invite-user-modal.element.ts @@ -2,13 +2,13 @@ import { css, html, nothing } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, query, state } from 'lit/decorators.js'; import { UmbInputPickerUserGroupElement } from '../../../../shared/components/input-user-group/input-user-group.element'; -import { UmbModalLayoutElement } from '../../../../../core/modal'; -import { UmbUserStore, UMB_USER_STORE_CONTEXT_TOKEN } from '../../../users/user.store'; +import { UmbUserStore, UMB_USER_STORE_CONTEXT_TOKEN } from '../../repository/user.store'; +import { UmbModalBaseElement } from '@umbraco-cms/modal'; import type { UserDetails } from '@umbraco-cms/models'; export type UsersViewType = 'list' | 'grid'; -@customElement('umb-workspace-view-users-invite') -export class UmbWorkspaceViewUsersInviteElement extends UmbModalLayoutElement { +@customElement('umb-invite-user-modal') +export class UmbInviteUserModalElement extends UmbModalBaseElement { static styles = [ UUITextStyles, css` @@ -94,7 +94,7 @@ export class UmbWorkspaceViewUsersInviteElement extends UmbModalLayoutElement { } private _closeModal() { - this.modalHandler?.close(); + this.modalHandler?.reject(); } private _resetForm() { @@ -182,10 +182,10 @@ export class UmbWorkspaceViewUsersInviteElement extends UmbModalLayoutElement { } } -export default UmbWorkspaceViewUsersInviteElement; +export default UmbInviteUserModalElement; declare global { interface HTMLElementTagNameMap { - 'umb-workspace-view-users-invite': UmbWorkspaceViewUsersInviteElement; + 'umb-invite-user-modal': UmbInviteUserModalElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/workspace-view-users-invite.test.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/invite-user/invite-user-modal.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/workspace-view-users-invite.test.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/invite-user/invite-user-modal.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/manifests.ts new file mode 100644 index 0000000000..623b47dc39 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/manifests.ts @@ -0,0 +1,24 @@ +import type { ManifestModal } from '@umbraco-cms/extensions-registry'; + +const modals: Array<ManifestModal> = [ + { + type: 'modal', + alias: 'Umb.Modal.CreateUser', + name: 'Create User Modal', + loader: () => import('./create-user/create-user-modal.element'), + }, + { + type: 'modal', + alias: 'Umb.Modal.InviteUser', + name: 'Invite User Modal', + loader: () => import('./invite-user/invite-user-modal.element'), + }, + { + type: 'modal', + alias: 'Umb.Modal.UserPicker', + name: 'User Picker Modal', + loader: () => import('./user-picker/user-picker-modal.element'), + }, +]; + +export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/user-picker/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/user-picker/index.ts new file mode 100644 index 0000000000..501ca347b8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/user-picker/index.ts @@ -0,0 +1,7 @@ +import type { UserDetails } from '@umbraco-cms/models'; +import { UmbModalToken, UmbPickerModalData } from '@umbraco-cms/modal'; + +export const UMB_USER_PICKER_MODAL_TOKEN = new UmbModalToken<UmbPickerModalData<UserDetails>>('Umb.Modal.UserPicker', { + type: 'sidebar', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user/picker-layout-user.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/user-picker/user-picker-modal.element.ts similarity index 90% rename from src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user/picker-layout-user.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/user-picker/user-picker-modal.element.ts index b2c0ddbb56..d66a0d2ac8 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user/picker-layout-user.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/user-picker/user-picker-modal.element.ts @@ -1,12 +1,12 @@ import { UUITextStyles } from '@umbraco-ui/uui-css'; import { css, html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; -import { UmbModalLayoutPickerBase } from '../modal-layout-picker-base'; -import { UmbUserStore, UMB_USER_STORE_CONTEXT_TOKEN } from '../../../../backoffice/users/users/user.store'; +import { UmbUserStore, UMB_USER_STORE_CONTEXT_TOKEN } from '../../repository/user.store'; +import { UmbModalElementPickerBase } from '@umbraco-cms/modal'; import type { UserDetails } from '@umbraco-cms/models'; -@customElement('umb-picker-layout-user') -export class UmbPickerLayoutUserElement extends UmbModalLayoutPickerBase<UserDetails> { +@customElement('umb-user-picker-modal') +export class UmbUserPickerModalElement extends UmbModalElementPickerBase<UserDetails> { static styles = [ UUITextStyles, css` @@ -105,8 +105,10 @@ export class UmbPickerLayoutUserElement extends UmbModalLayoutPickerBase<UserDet } } +export default UmbUserPickerModalElement; + declare global { interface HTMLElementTagNameMap { - 'umb-picker-layout-user': UmbPickerLayoutUserElement; + 'umb-user-picker-modal': UmbUserPickerModalElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user/picker-layout-user.test.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/user-picker/user-picker-modal.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user/picker-layout-user.test.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/users/users/modals/user-picker/user-picker-modal.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/repository/manifests.ts new file mode 100644 index 0000000000..96ae970229 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/repository/manifests.ts @@ -0,0 +1,23 @@ +import { UmbUserRepository } from '../repository/user.repository'; +import { UmbUserStore } from './user.store'; +import type { ManifestStore, ManifestRepository } from '@umbraco-cms/extensions-registry'; + +export const USER_REPOSITORY_ALIAS = 'Umb.Repository.User'; + +const repository: ManifestRepository = { + type: 'repository', + alias: USER_REPOSITORY_ALIAS, + name: 'User Repository', + class: UmbUserRepository, +}; + +export const USER_STORE_ALIAS = 'Umb.Store.User'; + +const store: ManifestStore = { + type: 'store', + alias: USER_STORE_ALIAS, + name: 'User Store', + class: UmbUserStore, +}; + +export const manifests = [repository, store]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/user.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/repository/user.store.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/users/users/user.store.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/users/users/repository/user.store.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/workspace/user-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/workspace/user-workspace.context.ts index 86d2df2b64..71e99083fc 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/workspace/user-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/workspace/user-workspace.context.ts @@ -1,4 +1,4 @@ -import { UMB_USER_STORE_CONTEXT_TOKEN } from '../../users/user.store'; +import { UMB_USER_STORE_CONTEXT_TOKEN } from '../repository/user.store'; import { UmbWorkspaceContext } from '../../../shared/components/workspace/workspace-context/workspace-context'; import { UmbWorkspaceEntityContextInterface } from '../../../shared/components/workspace/workspace-context/workspace-entity-context.interface'; import { UmbEntityWorkspaceManager } from '../../../shared/components/workspace/workspace-context/entity-manager-controller'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/workspace/user-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/workspace/user-workspace.element.ts index 928c6516a4..3ef980f51e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/workspace/user-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/workspace/user-workspace.element.ts @@ -1,16 +1,15 @@ import { UUIInputElement, UUIInputEvent } from '@umbraco-ui/uui'; import { css, html, nothing, TemplateResult } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { customElement, property, state } from 'lit/decorators.js'; +import { customElement, state } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { repeat } from 'lit/directives/repeat.js'; -import { distinctUntilChanged } from 'rxjs'; - import { UmbCurrentUserStore, UMB_CURRENT_USER_STORE_CONTEXT_TOKEN } from '../../current-user/current-user.store'; -import type { UmbModalContext } from '../../../../core/modal'; import type { UmbWorkspaceEntityElement } from '../../../shared/components/workspace/workspace-entity-element.interface'; +import { UMB_CHANGE_PASSWORD_MODAL_TOKEN } from '../../current-user/modals/change-password'; import { UmbWorkspaceUserContext } from './user-workspace.context'; +import type { UmbModalContext } from '@umbraco-cms/modal'; import { getLookAndColorFromUserStatus } from '@umbraco-cms/utils'; import type { UserDetails } from '@umbraco-cms/models'; @@ -191,7 +190,9 @@ export class UmbUserWorkspaceElement extends UmbLitElement implements UmbWorkspa } private _changePassword() { - this._modalContext?.changePassword({ requireOldPassword: this._currentUserStore?.isAdmin === false }); + this._modalContext?.open(UMB_CHANGE_PASSWORD_MODAL_TOKEN, { + requireOldPassword: this._currentUserStore?.isAdmin === false, + }); } private _renderActionButtons() { diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts index 014a4e8c61..29f1f1d104 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts @@ -24,6 +24,7 @@ import { handlers as templateHandlers } from './domains/template.handlers'; import { handlers as languageHandlers } from './domains/language.handlers'; import { handlers as cultureHandlers } from './domains/culture.handlers'; import { handlers as redirectManagementHandlers } from './domains/redirect-management.handlers'; +import { handlers as logViewerHandlers } from './domains/log-viewer.handlers'; import { handlers as packageHandlers } from './domains/package.handlers'; const handlers = [ @@ -52,6 +53,7 @@ const handlers = [ ...languageHandlers, ...cultureHandlers, ...redirectManagementHandlers, + ...logViewerHandlers, ...packageHandlers, ]; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts index f1403b64d6..fd0b0b01c0 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts @@ -206,7 +206,54 @@ export const data: Array<DataTypeModel & { type: 'data-type' }> = [ parentKey: null, propertyEditorAlias: 'Umbraco.DateTime', propertyEditorUiAlias: 'Umb.PropertyEditorUI.DatePicker', - data: [], + data: [ + { + alias: 'format', + value: 'YYYY-MM-DD', + }, + { + alias: 'offsetTime', + value: true, + }, + ], + }, + { + $type: 'data-type', + name: 'Date Picker With Time', + type: 'data-type', + key: 'dt-datePicker-time', + parentKey: null, + propertyEditorAlias: 'Umbraco.DateTime', + propertyEditorUiAlias: 'Umb.PropertyEditorUI.DatePicker', + data: [ + { + alias: 'format', + value: 'YYYY-MM-DD HH:mm:ss', + }, + { + alias: 'offsetTime', + value: true, + }, + ], + }, + { + $type: 'data-type', + name: 'Time', + type: 'data-type', + key: 'dt-time', + parentKey: null, + propertyEditorAlias: 'Umbraco.DateTime', + propertyEditorUiAlias: 'Umb.PropertyEditorUI.DatePicker', + data: [ + { + alias: 'format', + value: 'HH:mm:ss', + }, + { + alias: 'offsetTime', + value: false, + }, + ], }, { $type: 'data-type', diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/data.ts index 7b405ceed8..8f08949ada 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/data.ts @@ -5,4 +5,8 @@ export class UmbData<T> { constructor(data: Array<T>) { this.data = data; } + + get total() { + return this.data.length; + } } diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/document-type.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/document-type.data.ts index f49db67bbb..2b25677dfc 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/document-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/document-type.data.ts @@ -717,6 +717,25 @@ export const data: Array<DocumentTypeModel> = [ labelOnTop: true, }, }, + { + key: '22', + containerKey: '2c943997-b685-432d-a6c5-601d8e7a298a', + alias: 'blockGrid', + name: 'Block Grid', + description: '', + dataTypeKey: 'dt-blockGrid', + variesByCulture: false, + variesBySegment: false, + validation: { + mandatory: true, + mandatoryMessage: null, + regEx: null, + regExMessage: null, + }, + appearance: { + labelOnTop: false, + }, + }, ], containers: [ { diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/document.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/document.data.ts index d52e61ab8a..8f5b4bfd10 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/document.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/document.data.ts @@ -59,7 +59,19 @@ export const data: Array<DocumentModel> = [ alias: 'datePicker', culture: null, segment: null, - value: null, + value: '2023-12-24', + }, + { + alias: 'datePickerTime', + culture: null, + segment: null, + value: '2023-12-24 14:52', + }, + { + alias: 'time', + culture: null, + segment: null, + value: '14:52:00', }, { alias: 'email', diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/log-viewer.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/log-viewer.data.ts new file mode 100644 index 0000000000..b74e755fa9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/log-viewer.data.ts @@ -0,0 +1,413 @@ +import { logs } from './logs.data'; +import { UmbData } from './data'; +import { LogMessageModel, LogTemplateModel, SavedLogSearchModel } from '@umbraco-cms/backend-api'; + +// Temp mocked database +class UmbLogviewerSearchesData extends UmbData<SavedLogSearchModel> { + constructor(data: SavedLogSearchModel[]) { + super(data); + } + + // skip can be number or null + getSavedSearches(skip = 0, take = this.data.length): Array<SavedLogSearchModel> { + return this.data.slice(skip, take); + } + + getByName(name: string) { + return this.data.find((search) => search.name === name); + } +} + +class UmbLogviewerTemplatesData extends UmbData<LogTemplateModel> { + constructor(data: LogTemplateModel[]) { + super(data); + } + + // skip can be number or null + getTemplates(skip = 0, take = this.data.length): Array<LogTemplateModel> { + return this.data.slice(skip, take); + } +} + +class UmbLogviewerMessagesData extends UmbData<LogMessageModel> { + constructor(data: LogTemplateModel[]) { + super(data); + } + + // skip can be number or null + getLogs(skip = 0, take = this.data.length): Array<LogMessageModel> { + return this.data.slice(skip, take); + } + + getLevelCount() { + const levels = this.data.map((log) => log.level ?? 'unknown'); + const counts = {}; + levels.forEach((level: string) => { + //eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + counts[level ?? 'unknown'] = (counts[level] || 0) + 1; + }); + return counts; + } +} + +export const savedSearches: Array<SavedLogSearchModel> = [ + { + name: 'Find all logs where the Level is NOT Verbose and NOT Debug', + query: "Not(@Level='Verbose') and Not(@Level='Debug')", + }, + { + name: 'Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)', + query: 'Has(@Exception)', + }, + { + name: "Find all logs that have the property 'Duration'", + query: 'Has(Duration)', + }, + { + name: "Find all logs that have the property 'Duration' and the duration is greater than 1000ms", + query: 'Has(Duration) and Duration > 1000', + }, + { + name: "Find all logs that are from the namespace 'Umbraco.Core'", + query: "StartsWith(SourceContext, 'Umbraco.Core')", + }, + { + name: 'Find all logs that use a specific log message template', + query: "@messageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'", + }, + { + name: 'Find logs where one of the items in the SortedComponentTypes property array is equal to', + query: "SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'", + }, + { + name: 'Find logs where one of the items in the SortedComponentTypes property array contains', + query: "Contains(SortedComponentTypes[?], 'DatabaseServer')", + }, + { + name: 'Find all logs that the message has localhost in it with SQL like', + query: "@Message like '%localhost%'", + }, + { + name: "Find all logs that the message that starts with 'end' in it with SQL like", + query: "@Message like 'end%'", + }, + { + name: 'bla', + query: 'bla bla', + }, +]; + +export const messageTemplates: LogTemplateModel[] = [ + { + messageTemplate: 'Create Foreign Key:\n {Sql}', + count: 90, + }, + { + messageTemplate: 'Create Index:\n {Sql}', + count: 86, + }, + { + messageTemplate: 'Create table:\n {Sql}', + count: 82, + }, + { + messageTemplate: 'Create Primary Key:\n {Sql}', + count: 78, + }, + { + messageTemplate: 'Creating data in {TableName}', + count: 58, + }, + { + messageTemplate: 'Completed creating data in {TableName}', + count: 58, + }, + { + messageTemplate: 'New table {TableName} was created', + count: 58, + }, + { + messageTemplate: 'At {OrigState}', + count: 18, + }, + { + messageTemplate: 'SQL [{ContextIndex}]: {Sql}', + count: 15, + }, + { + messageTemplate: '{StartMessage} [Timing {TimingId}]', + count: 14, + }, + { + messageTemplate: '{EndMessage} ({Duration}ms) [Timing {TimingId}]', + count: 14, + }, + { + messageTemplate: 'Execute {MigrationType}', + count: 13, + }, + { + messageTemplate: "Assigned Deploy permission letter '{Permission}' to user group '{UserGroupAlias}'", + count: 6, + }, + { + messageTemplate: "Starting '{MigrationName}'...", + count: 5, + }, + { + messageTemplate: 'Done (pending scope completion).', + count: 5, + }, + { + messageTemplate: + "Umbraco Forms scheduled record deletion task will not run as it has been not enabled via configuration. To enable, set the configuration value at 'Umbraco:Forms:Options:ScheduledRecordDeletion:Enabled' to true.", + count: 5, + }, + { + messageTemplate: + 'Mapped Umbraco.Cloud.Deployment.SiteExtension.Messages.External.Git.ApplyChangesFromWwwRootToRepositoryCommand -> "siteext-input-queue"', + count: 3, + }, + { + messageTemplate: 'Bus "Rebus 1" started', + count: 3, + }, + { + messageTemplate: 'Acquiring MainDom.', + count: 3, + }, + { + messageTemplate: 'Acquired MainDom.', + count: 3, + }, + { + messageTemplate: 'Profiler is VoidProfiler, not profiling (must run debug mode to profile).', + count: 3, + }, + { + messageTemplate: + "Found single permission letter for '{LegacyPermission}' on user group '{UserGroupAlias}', assuming this is the 'Notifications' permission instead of the Deploy 'Queue for transfer' permission", + count: 3, + }, + { + messageTemplate: 'Started :: Running {edition} edition', + count: 3, + }, + { + messageTemplate: + "File system watcher for deploy events started with filter 'deploy*' and notify filter 'FileName'.", + count: 3, + }, + { + messageTemplate: 'Application started. Press Ctrl+C to shut down.', + count: 3, + }, + { + messageTemplate: 'Hosting environment: {envName}', + count: 3, + }, + { + messageTemplate: 'Content root path: {contentRoot}', + count: 3, + }, + { + messageTemplate: + 'Database migration step not completed: could not create primary key constraint on UFRecordDataLongString as a primary key already exists.', + count: 2, + }, + { + messageTemplate: + 'No last synced Id found, this generally means this is a new server/install. A cold boot will be triggered.', + count: 2, + }, + { + messageTemplate: 'Deploy permissions updated and saved', + count: 2, + }, + { + messageTemplate: 'Starting :: Running on Umbraco Cloud', + count: 2, + }, + { + messageTemplate: + 'Registered with MainDom, localContentDbExists? {LocalContentDbExists}, localMediaDbExists? {LocalMediaDbExists}', + count: 2, + }, + { + messageTemplate: 'Creating the content store, localContentDbExists? {LocalContentDbExists}', + count: 2, + }, + { + messageTemplate: 'Creating the media store, localMediaDbExists? {LocalMediaDbExists}', + count: 2, + }, + { + messageTemplate: 'Stopping ({SignalSource})', + count: 2, + }, + { + messageTemplate: 'Released ({SignalSource})', + count: 2, + }, + { + messageTemplate: 'Application is shutting down...', + count: 2, + }, + { + messageTemplate: 'Bus "Rebus 1" stopped', + count: 2, + }, + { + messageTemplate: 'Queued Hosted Service is stopping.', + count: 2, + }, + { + messageTemplate: 'User logged will be logged out due to timeout: {Username}, IP Address: {IPAddress}', + count: 2, + }, + { + messageTemplate: 'Starting unattended install.', + count: 1, + }, + { + messageTemplate: 'Unattended install completed.', + count: 1, + }, + { + messageTemplate: 'Configured with Azure database.', + count: 1, + }, + { + messageTemplate: 'Initialized the SqlServer database schema.', + count: 1, + }, + { + messageTemplate: + 'Database migration step not completed: could not create primary key constraint on UFRecordDataBit as a primary key already exists.', + count: 1, + }, + { + messageTemplate: + 'Database migration step not completed: could not create primary key constraint on UFRecordDataDateTime as a primary key already exists.', + count: 1, + }, + { + messageTemplate: + 'Database migration step not completed: could not create primary key constraint on UFRecordDataInteger as a primary key already exists.', + count: 1, + }, + { + messageTemplate: + 'Database migration step not completed: could not create primary key constraint on UFUserFormSecurity as a primary key already exists.', + count: 1, + }, + { + messageTemplate: + 'Database migration step not completed: could not create primary key constraint on UFUserGroupSecurity as a primary key already exists.', + count: 1, + }, + { + messageTemplate: + 'Database migration step not completed: could not create primary key constraint on UFUserGroupFormSecurity as a primary key already exists.', + count: 1, + }, + { + messageTemplate: + 'Database NuCache was serialized using {CurrentSerializer}. Currently configured NuCache serializer {Serializer}. Rebuilding Nucache', + count: 1, + }, + { + messageTemplate: 'Starting :: Running locally', + count: 1, + }, + { + messageTemplate: 'No XML encryptor configured. Key {KeyId:B} may be persisted to storage in unencrypted form.', + count: 1, + }, + { + messageTemplate: 'Started :: Transitioned from azure init marker to deploy marker', + count: 1, + }, + { + messageTemplate: 'Found {diskReadTrigger} or {diskReadOnStartTrigger} trigger file when starting, processing...', + count: 1, + }, + { + messageTemplate: 'Beginning deployment {id}.', + count: 1, + }, + { + messageTemplate: 'Suspend scheduled publishing.', + count: 1, + }, + { + messageTemplate: 'Preparing', + count: 1, + }, + { + messageTemplate: 'Reading state', + count: 1, + }, + { + messageTemplate: 'No artifacts', + count: 1, + }, + { + messageTemplate: 'Restore caches and indexes', + count: 1, + }, + { + messageTemplate: 'Resume scheduled publishing.', + count: 1, + }, + { + messageTemplate: 'Complete', + count: 1, + }, + { + messageTemplate: 'Deployment {id} completed.', + count: 1, + }, + { + messageTemplate: 'Work Status {WorkStatus}.', + count: 1, + }, + { + messageTemplate: 'Released from MainDom', + count: 1, + }, + { + messageTemplate: "Keep alive failed (at '{keepAlivePingUrl}').", + count: 1, + }, + { + messageTemplate: 'Adding examine event handlers for {RegisteredIndexers} index providers.', + count: 1, + }, + { + messageTemplate: 'Document {ContentName} (id={ContentId}) has been published.', + count: 1, + }, +]; + +export const logLevels = { + total: 2, + items: [ + { + name: 'Global', + level: 'Information', + }, + { + name: 'UmbracoFile', + level: 'Verbose', + }, + ], +}; + +export const umbLogviewerData = { + searches: new UmbLogviewerSearchesData(savedSearches), + templates: new UmbLogviewerTemplatesData(messageTemplates), + logs: new UmbLogviewerMessagesData(logs), + logLevels: logLevels, +}; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/logs.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/logs.data.ts new file mode 100644 index 0000000000..974ec00f50 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/logs.data.ts @@ -0,0 +1,7350 @@ +import { LogLevelModel, LogMessageModel } from '@umbraco-cms/backend-api'; + +const allLogs = [ + { + timestamp: '2023-02-14T12:02:15.8094382+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'c80ec31d-3d58-401a-9361-1ae7451c215f', + }, + { + name: 'HttpRequestNumber', + value: '982', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:02:15.7464278+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '77052e32-93ab-426f-b96c-4c71fab7d48a', + }, + { + name: 'HttpRequestNumber', + value: '981', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:02:10.7972442+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '1cec3ec5-4980-4bfc-bdf4-085070da6f4d', + }, + { + name: 'HttpRequestNumber', + value: '980', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:02:10.735678+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '6941710a-4f09-41b2-8f11-fce2c813ad10', + }, + { + name: 'HttpRequestNumber', + value: '979', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:02:05.7944643+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '5e129d0e-595d-43f4-9055-7ea218347659', + }, + { + name: 'HttpRequestNumber', + value: '978', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:02:05.7340093+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'a760748a-43cd-4132-9f51-c518df8ae368', + }, + { + name: 'HttpRequestNumber', + value: '977', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:02:00.7904684+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '3acbe275-1340-4d92-a2f9-8b15eea7c5da', + }, + { + name: 'HttpRequestNumber', + value: '976', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:02:00.7298561+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '09ab7c8b-ae78-4b86-adff-e82bf9bed66e', + }, + { + name: 'HttpRequestNumber', + value: '975', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:55.7815654+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '3252af15-c998-4463-b9c0-18d663d65c53', + }, + { + name: 'HttpRequestNumber', + value: '974', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:55.7215251+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'aa6552dc-226d-4302-ab2f-6a1ae7b82205', + }, + { + name: 'HttpRequestNumber', + value: '973', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:50.7812088+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '85f8e869-9b32-49f7-a0b6-e1be33258230', + }, + { + name: 'HttpRequestNumber', + value: '972', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:50.7176788+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '70effdd7-f20a-4c54-bdf1-573f12c5c4df', + }, + { + name: 'HttpRequestNumber', + value: '971', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:45.7721389+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '62c6c02d-91b2-414d-82d6-ba6485cd901d', + }, + { + name: 'HttpRequestNumber', + value: '970', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:45.7101655+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '88ee8366-0cbe-465f-ab06-39e082f62cc3', + }, + { + name: 'HttpRequestNumber', + value: '969', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:40.7656432+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '72c6403d-097d-4274-b77e-9c33afe05705', + }, + { + name: 'HttpRequestNumber', + value: '968', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:40.7034373+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'da7ddb6d-ec9b-44aa-9376-b38f9dbf16ea', + }, + { + name: 'HttpRequestNumber', + value: '967', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:35.7586458+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'ada57301-d4fe-4399-acab-5506b96f0724', + }, + { + name: 'HttpRequestNumber', + value: '966', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:35.6957643+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '4603522a-12fa-43fa-b171-170c0a7dee91', + }, + { + name: 'HttpRequestNumber', + value: '965', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:30.7559798+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'bb012edc-36c7-4426-86e3-525e8c2ff44e', + }, + { + name: 'HttpRequestNumber', + value: '964', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:30.6937419+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '1180c2e4-ede3-458a-97ca-c733b4224718', + }, + { + name: 'HttpRequestNumber', + value: '963', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:25.744449+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '7c5d8901-3453-402d-881c-12d3c4d9477a', + }, + { + name: 'HttpRequestNumber', + value: '962', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:25.6824877+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'a2ae4602-2a58-4521-99dd-6fa41562b004', + }, + { + name: 'HttpRequestNumber', + value: '961', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:20.7317217+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '0b55b0c8-57f3-4ed9-ae8e-1cb85cb11077', + }, + { + name: 'HttpRequestNumber', + value: '960', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:20.6696118+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'c502a5a8-c8cb-4cec-ac6c-f9b66b3bad33', + }, + { + name: 'HttpRequestNumber', + value: '959', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:15.724729+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'bcada94f-9851-4c81-8da0-1bf27be66573', + }, + { + name: 'HttpRequestNumber', + value: '958', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:15.662463+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'f761bc23-9e7f-45e7-8847-7885624dd261', + }, + { + name: 'HttpRequestNumber', + value: '957', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:10.7235889+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '88785b30-85c1-40da-bba9-0663c5b38817', + }, + { + name: 'HttpRequestNumber', + value: '956', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:10.6605318+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'a26b64b4-5b83-405d-9860-e01bcbc0edaa', + }, + { + name: 'HttpRequestNumber', + value: '955', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:05.7157154+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'b192ac30-88f0-476d-a9ab-6462a4ca5195', + }, + { + name: 'HttpRequestNumber', + value: '954', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:05.6547824+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '97ee6cef-af1e-472a-acf0-d296890a1916', + }, + { + name: 'HttpRequestNumber', + value: '953', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:00.7039952+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'b537e0e1-e4ed-4f6b-afce-11c19a798632', + }, + { + name: 'HttpRequestNumber', + value: '952', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:01:00.6417516+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '3011bb2a-e3df-4dc6-a46d-9619287ad9bb', + }, + { + name: 'HttpRequestNumber', + value: '951', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:55.6948223+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'f43990a1-fdeb-4169-85c1-60b6833b4c82', + }, + { + name: 'HttpRequestNumber', + value: '950', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:55.6326808+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '43def4b0-c9e6-41db-9031-f7c3fc112900', + }, + { + name: 'HttpRequestNumber', + value: '949', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:50.6948589+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'e048057f-5dc0-408f-a560-2044b99e53a3', + }, + { + name: 'HttpRequestNumber', + value: '948', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:50.632697+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'ff426368-f6e5-4e9d-a2d2-0d061bedb688', + }, + { + name: 'HttpRequestNumber', + value: '947', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:45.6943493+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '0f2f6a91-487b-440d-9350-9737bed00c8d', + }, + { + name: 'HttpRequestNumber', + value: '946', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:45.6319218+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'fc244d40-46ae-49f9-8eee-ef5e41738283', + }, + { + name: 'HttpRequestNumber', + value: '945', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:40.6902419+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '5d3e350c-0b62-4122-9850-1b821fc14824', + }, + { + name: 'HttpRequestNumber', + value: '944', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:40.6274587+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'f4101feb-d20b-4007-b0d8-5fb8448b0ba5', + }, + { + name: 'HttpRequestNumber', + value: '943', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:35.6787856+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '136c6fa2-59b3-4a35-bf62-41b4b7e47aae', + }, + { + name: 'HttpRequestNumber', + value: '942', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:35.6168559+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'fd9d8b63-8cbe-448d-acfe-fb0225630c8f', + }, + { + name: 'HttpRequestNumber', + value: '941', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:30.6723677+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '5f550460-063c-4ffe-b4b7-977916a0bf3c', + }, + { + name: 'HttpRequestNumber', + value: '940', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:30.6121168+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '7205a37f-3c57-4388-86d0-aa82334ac8b9', + }, + { + name: 'HttpRequestNumber', + value: '939', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:25.6705459+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '11c619df-ec09-400d-bac3-0796ec8313b3', + }, + { + name: 'HttpRequestNumber', + value: '938', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:25.6072437+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '4acb6550-415f-4acd-9d5d-d16e3f24358d', + }, + { + name: 'HttpRequestNumber', + value: '937', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:20.655909+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '538ddeb5-5f61-4f0f-b997-d730ca43c308', + }, + { + name: 'HttpRequestNumber', + value: '936', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:20.593934+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '0bbed3f1-3732-4961-987b-dee855bf14e5', + }, + { + name: 'HttpRequestNumber', + value: '935', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:15.652173+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '2fc31793-1567-4166-ba1d-fedc5aa86c76', + }, + { + name: 'HttpRequestNumber', + value: '934', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:15.5893967+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'a1229318-a1f3-45ae-81ea-553c6380dc63', + }, + { + name: 'HttpRequestNumber', + value: '933', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:10.638341+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '3c73f5d8-fb91-4cb1-99ea-710dc5a077d0', + }, + { + name: 'HttpRequestNumber', + value: '932', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:10.5772254+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'c90cfc2c-4257-4963-b983-66e8154d411b', + }, + { + name: 'HttpRequestNumber', + value: '931', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:05.6357729+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '1545fb5a-eb3f-42ad-b996-83a25cc2884f', + }, + { + name: 'HttpRequestNumber', + value: '930', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:05.5733282+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'c9d266d1-4934-46c7-9b53-ed09fbd886a0', + }, + { + name: 'HttpRequestNumber', + value: '929', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:00.6298079+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'de2e3225-9d89-4a80-9ee5-e71690355f9d', + }, + { + name: 'HttpRequestNumber', + value: '928', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T12:00:00.5660574+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '82db2316-2b67-451c-86c0-dd45244c1d96', + }, + { + name: 'HttpRequestNumber', + value: '927', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:55.6191475+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '8dc85495-24f2-4842-8ff5-d0374cad3741', + }, + { + name: 'HttpRequestNumber', + value: '926', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:55.5568848+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'cc428ec4-db17-4cee-981d-901fde3af15c', + }, + { + name: 'HttpRequestNumber', + value: '925', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:50.6177966+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '245be706-df80-4501-a915-d2af6e44af26', + }, + { + name: 'HttpRequestNumber', + value: '924', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:50.5567918+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '8d5082a3-4ca1-4fb2-976b-8a761a245661', + }, + { + name: 'HttpRequestNumber', + value: '923', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:45.6058749+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '8fd1dee6-eb15-4081-b623-f4139cc465fd', + }, + { + name: 'HttpRequestNumber', + value: '922', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:45.5419858+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '87434476-9f86-49f5-bacd-37978f97ec3a', + }, + { + name: 'HttpRequestNumber', + value: '921', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:40.6030991+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '3a79d410-232f-46f0-8521-f71c58cefcd3', + }, + { + name: 'HttpRequestNumber', + value: '920', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:40.5414694+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '0fda8a3d-b7df-46e4-bf3d-0cf15829f6a7', + }, + { + name: 'HttpRequestNumber', + value: '919', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:35.5924386+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '54f6057e-d0f6-46d0-b7b4-082f315c4533', + }, + { + name: 'HttpRequestNumber', + value: '918', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:35.5304962+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '39ae83f8-0778-474a-b41f-eea93b82b201', + }, + { + name: 'HttpRequestNumber', + value: '917', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:30.5881163+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'c521cadd-3e47-4eab-b640-ea9dfc50abb8', + }, + { + name: 'HttpRequestNumber', + value: '916', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:30.5260781+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '5c3f597f-abcd-41d5-9360-8bd725c85b60', + }, + { + name: 'HttpRequestNumber', + value: '915', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:25.5753438+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '886f9acd-60bd-4ed4-8cf2-1c35b007c58e', + }, + { + name: 'HttpRequestNumber', + value: '914', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:25.5127958+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'c215f085-f3b8-4150-b77c-0a9882404530', + }, + { + name: 'HttpRequestNumber', + value: '913', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:20.571759+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'b3d613ba-6a53-4d6e-9b1a-c755752fe566', + }, + { + name: 'HttpRequestNumber', + value: '912', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:20.5091665+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'f97c2d30-f658-447e-b728-136bcd824bc9', + }, + { + name: 'HttpRequestNumber', + value: '911', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:15.569587+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'f7d59956-1034-4698-aad1-c2da66d067e8', + }, + { + name: 'HttpRequestNumber', + value: '910', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:15.5076538+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'a98bb970-a9f1-4a31-b22c-6874e7bcc811', + }, + { + name: 'HttpRequestNumber', + value: '909', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:10.5556531+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '188b901c-9022-4e8e-80c7-45bb8d5c7c0a', + }, + { + name: 'HttpRequestNumber', + value: '908', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:10.5074665+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'dba6dbdd-2142-4803-8223-cce07f231a51', + }, + { + name: 'HttpRequestNumber', + value: '907', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:05.5524744+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '07d65eca-a9ca-4109-8ad8-4db2a737cc68', + }, + { + name: 'HttpRequestNumber', + value: '906', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:05.5045339+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'c75e74d2-dec9-4525-8966-34e455047231', + }, + { + name: 'HttpRequestNumber', + value: '905', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:00.5441587+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '4787c7ab-a877-47e5-96fa-a8cdbc8eb427', + }, + { + name: 'HttpRequestNumber', + value: '904', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:59:00.4976461+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '6fd66545-4305-4f30-9791-54be1d151483', + }, + { + name: 'HttpRequestNumber', + value: '903', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:55.5306481+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '06ac6e6f-d866-48e7-ae49-b908cd472555', + }, + { + name: 'HttpRequestNumber', + value: '902', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:55.4849332+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'a8a3191e-1324-49b7-9fc5-c65b99f29c37', + }, + { + name: 'HttpRequestNumber', + value: '901', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:50.5183018+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '5c3c404b-03bb-47ee-8e60-2678249015a8', + }, + { + name: 'HttpRequestNumber', + value: '900', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:50.4715437+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '40090ff4-ac41-4e2d-a69b-99414ce77df5', + }, + { + name: 'HttpRequestNumber', + value: '899', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:45.5057522+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'b4cf7b6b-ebaf-40d0-97d6-69acd93bec86', + }, + { + name: 'HttpRequestNumber', + value: '898', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:45.4595094+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '17ee778c-62bb-4025-a232-bfaf76eda60b', + }, + { + name: 'HttpRequestNumber', + value: '897', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:40.4955381+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'f2f9a23f-efd1-4bc5-a316-bc603544c43a', + }, + { + name: 'HttpRequestNumber', + value: '896', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:40.4490867+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '1894e914-d45b-436f-b125-56160011d80f', + }, + { + name: 'HttpRequestNumber', + value: '895', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:35.4899092+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'f092d759-7c3a-4e7e-a560-9ac7e6c77f7c', + }, + { + name: 'HttpRequestNumber', + value: '894', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:35.4438695+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'f02cb18d-8976-4f2c-a16a-a0c9006724a1', + }, + { + name: 'HttpRequestNumber', + value: '893', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:30.4782757+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '743f4f0b-7e83-4eb8-bfdc-e3e97be0ecc6', + }, + { + name: 'HttpRequestNumber', + value: '892', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:30.4324179+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '422d43b5-45a4-4b59-901b-399cb3fb448a', + }, + { + name: 'HttpRequestNumber', + value: '891', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:25.4653028+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '7a3fe678-d6c9-475a-ab57-f7b4d06b8005', + }, + { + name: 'HttpRequestNumber', + value: '890', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:25.4179895+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '956a5a79-b0dd-446b-b4d0-f9594d6f538d', + }, + { + name: 'HttpRequestNumber', + value: '889', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:20.4554989+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '7cdab7b5-0d9b-4ae4-b023-83b5415e2909', + }, + { + name: 'HttpRequestNumber', + value: '888', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:20.4074817+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'f9dbdf17-fc1a-45b4-bd88-030b2c26ec16', + }, + { + name: 'HttpRequestNumber', + value: '887', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:15.4508724+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '9fef4e4a-e373-4a9a-b162-857af5c160f9', + }, + { + name: 'HttpRequestNumber', + value: '886', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:15.4043773+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: '1b1c9faa-c0be-4722-b3f3-441e48d42c0a', + }, + { + name: 'HttpRequestNumber', + value: '885', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:10.4501237+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"InternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'InternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: '1d9a0529-a75a-40cc-be5d-c871548ab233', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.EmptyRecycleBin (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000002e-0005-ef00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/EmptyRecycleBin', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '50', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'd8bedb8f-1151-4e2b-858f-286df530ffa2', + }, + { + name: 'HttpRequestNumber', + value: '884', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, + { + timestamp: '2023-02-14T11:58:10.4036882+00:00', + level: 'Debug', + messageTemplate: '{IndexName} searcher refreshed? {DidRefresh}', + renderedMessage: '"ExternalIndex" searcher refreshed? False', + properties: [ + { + name: 'IndexName', + value: 'ExternalIndex', + }, + { + name: 'DidRefresh', + value: 'False', + }, + { + name: 'SourceContext', + value: 'Examine.Lucene.Providers.LuceneIndex', + }, + { + name: 'ActionId', + value: 'c4a4ecf3-34e7-4ff9-99ad-4aab0a23c3c2', + }, + { + name: 'ActionName', + value: 'Umbraco.Cms.Web.BackOffice.Controllers.ContentController.DeleteById (Umbraco.Web.BackOffice)', + }, + { + name: 'RequestId', + value: '4000005e-0004-fe00-b63f-84710c7967bb', + }, + { + name: 'RequestPath', + value: '/umbraco/backoffice/umbracoapi/content/DeleteById', + }, + { + name: 'ProcessId', + value: '17632', + }, + { + name: 'ProcessName', + value: 'iisexpress', + }, + { + name: 'ThreadId', + value: '57', + }, + { + name: 'ApplicationId', + value: '0cf9334a65daa2e8a69943c6db2fb730bb14f2de', + }, + { + name: 'MachineName', + value: 'DESKTOP-M35N63H', + }, + { + name: 'Log4NetLevel', + value: 'DEBUG', + }, + { + name: 'HttpRequestId', + value: 'cdfde20a-6e4e-4062-be6f-5daff839ed04', + }, + { + name: 'HttpRequestNumber', + value: '883', + }, + { + name: 'HttpSessionId', + value: '0', + }, + ], + exception: null, + }, +]; + +const randomEnumValue = (enumeration: any): LogLevelModel => { + const values = Object.keys(enumeration); + const enumKey = values[Math.floor(Math.random() * values.length)]; + return enumeration[enumKey]; +}; + +export const logs: LogMessageModel[] = allLogs.map((log) => { + + const randomLevel = randomEnumValue(LogLevelModel); + + return { + ...log, + level: randomLevel, + eventId: { + TypeTag: null, + Properties: [ + { + Name: 'Id', + Value: { + Value: 17, + }, + }, + { + Name: 'Name', + Value: { + Value: 'ExceptionProcessingMessage', + }, + }, + ], + }, + exception: + randomLevel === LogLevelModel.ERROR + ? `System.InvalidOperationException: The identity did not contain requried claim name + at Umbraco.Cloud.Identity.Cms.PrincipalExtensions.GetRequiredFirstValue(ClaimsIdentity identity, String claimType) + at Umbraco.Cloud.Identity.Cms.ClaimsIdentityExtensions.ApplyNameClaim(ClaimsIdentity identity) + at Umbraco.Cloud.Identity.Cms.ClaimsIdentityExtensions.ValidateAndTransformClaims(ClaimsIdentity identity, String currentPolicy, String[] userRoles, String passwordChangePolicy, String profilePolicy, String passwordResetPolicy) + at Umbraco.Cloud.Identity.Cms.ClaimsIdentityExtensions.ValidateAndTransformClaims(ClaimsPrincipal principal, String currentPolicy, String[] userRoles, String passwordChangePolicy, String profilePolicy, String passwordResetPolicy) + at Umbraco.Cloud.Identity.Cms.V10.OpenIdConnectEventHandler.OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context) + at Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilder.<>c__DisplayClass11_1.<<WebAppCallsWebApiImplementation>b__1>d.MoveNext() + --- End of stack trace from previous location --- + at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.RunAuthorizationCodeReceivedEventAsync(OpenIdConnectMessage authorizationResponse, ClaimsPrincipal user, AuthenticationProperties properties, JwtSecurityToken jwt) + at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.HandleRemoteAuthenticateAsync()` + : undefined, + }; +}); diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/document-type.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/document-type.handlers.ts index 2787f1cae1..bc06e3b5b8 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/document-type.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/document-type.handlers.ts @@ -13,6 +13,24 @@ export const handlers = [ return res(ctx.status(200), ctx.json(saved)); }), + rest.get('/umbraco/management/api/v1/document-type/details/:key', (req, res, ctx) => { + const key = req.params.key as string; + if (!key) return; + + const document = umbDocumentTypeData.getByKey(key); + + return res(ctx.status(200), ctx.json([document])); + }), + + rest.post<DocumentTypeModel[]>('/umbraco/management/api/v1/document-type/details/save', (req, res, ctx) => { + const data = req.body; + if (!data) return; + + const saved = umbDocumentTypeData.save(data); + + return res(ctx.status(200), ctx.json(saved)); + }), + rest.get('/umbraco/management/api/v1/tree/document-type/root', (req, res, ctx) => { const rootItems = umbDocumentTypeData.getTreeRoot(); const response = { diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/log-viewer.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/log-viewer.handlers.ts new file mode 100644 index 0000000000..70eabc1d7c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/log-viewer.handlers.ts @@ -0,0 +1,86 @@ +import { rest } from 'msw'; +import { umbLogviewerData } from '../data/log-viewer.data'; +import { umbracoPath } from '@umbraco-cms/utils'; +import { SavedLogSearchModel } from '@umbraco-cms/backend-api'; + +export const handlers = [ + //#region Searches + rest.get(umbracoPath('/log-viewer/saved-search'), (req, res, ctx) => { + const skip = req.url.searchParams.get('skip'); + const skipNumber = skip ? Number.parseInt(skip) : undefined; + const take = req.url.searchParams.get('take'); + const takeNumber = take ? Number.parseInt(take) : undefined; + + const items = umbLogviewerData.searches.getSavedSearches(skipNumber, takeNumber); + + const response = { + total: items.length, + items, + }; + + return res(ctx.delay(), ctx.status(200), ctx.json(response)); + }), + + rest.get(umbracoPath('/log-viewer/saved-search/:name'), (req, res, ctx) => { + const name = req.params.key as string; + + if (!name) return; + + const item = umbLogviewerData.searches.getByName(name); + return res(ctx.delay(), ctx.status(200), ctx.json(item)); + }), + + rest.post<SavedLogSearchModel>(umbracoPath('/log-viewer/saved-search'), async (req, res, ctx) => { + return res(ctx.delay(), ctx.status(200)); + }), + + rest.delete(umbracoPath('/log-viewer/saved-search/:name'), async (req, res, ctx) => { + return res(ctx.status(200)); + }), + //#endregion + + //#region Temaplates + rest.get(umbracoPath('/log-viewer/message-template'), (req, res, ctx) => { + const skip = req.url.searchParams.get('skip'); + const skipNumber = skip ? Number.parseInt(skip) : undefined; + const take = req.url.searchParams.get('take'); + const takeNumber = take ? Number.parseInt(take) : undefined; + + const items = umbLogviewerData.templates.getTemplates(skipNumber, takeNumber); + + const response = { + total: umbLogviewerData.templates.total, + items, + }; + + return res(ctx.delay(), ctx.status(200), ctx.json(response)); + }), + //#endregion + //#region Logs + rest.get(umbracoPath('/log-viewer/level'), (req, res, ctx) => { + return res(ctx.delay(), ctx.status(200), ctx.json(umbLogviewerData.logLevels)); + }), + + rest.get(umbracoPath('/log-viewer/level-count'), (req, res, ctx) => { + return res(ctx.delay(), ctx.status(200), ctx.json(umbLogviewerData.logs.getLevelCount())); + }), + + rest.get(umbracoPath('/log-viewer/validate-logs-size'), (req, res, ctx) => { + return res(ctx.delay(), ctx.status(200)); + }), + + rest.get(umbracoPath('/log-viewer/log'), (req, res, ctx) => { + const skip = req.url.searchParams.get('skip'); + const skipNumber = skip ? Number.parseInt(skip) : undefined; + const take = req.url.searchParams.get('take'); + const takeNumber = take ? Number.parseInt(take) : undefined; + + const items = umbLogviewerData.logs.getLogs(skipNumber, takeNumber); + const response = { + total: umbLogviewerData.logs.total, + items, + }; + + return res(ctx.delay(), ctx.status(200), ctx.json(response)); + }), +]; diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/index.ts b/src/Umbraco.Web.UI.Client/src/core/modal/index.ts deleted file mode 100644 index 0065759e9e..0000000000 --- a/src/Umbraco.Web.UI.Client/src/core/modal/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './modal.context'; -export * from './modal-handler'; -export * from './layouts/modal-layout.element'; diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/basic/modal-layout-basic.element.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/basic/modal-layout-basic.element.ts deleted file mode 100644 index 593f2415ab..0000000000 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/basic/modal-layout-basic.element.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { css, html, TemplateResult } from 'lit'; -import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { customElement } from 'lit/decorators.js'; -import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar'; -import { UmbModalLayoutElement } from '../modal-layout.element'; - -export interface UmbBasicModalData { - header: TemplateResult | string; - content: TemplateResult | string; - overlaySize?: UUIModalSidebarSize; -} - -@customElement('umb-modal-layout-basic') -export class UmbModalLayoutBasicElement extends UmbModalLayoutElement<UmbBasicModalData> { - static styles = [ - UUITextStyles, - css` - uui-scroll-container { - background-color: var(--uui-color-surface); - } - `, - ]; - - private _close() { - // As this is a basic modal designed for viewing readonly info - // Then we don't need to pass any data back to the parent when - // we close/save the modal etc... - this.modalHandler?.close(); - } - - connectedCallback(): void { - super.connectedCallback(); - } - - render() { - return html` - <umb-workspace-layout .headline=${this.data?.header}> - <uui-scroll-container>${this.data?.content}</uui-scroll-container> - <uui-button slot="actions" look="secondary" label="Close sidebar" @click="${this._close}">Close</uui-button> - </umb-workspace-layout> - `; - } -} - -export default UmbModalLayoutBasicElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-modal-layout-basic': UmbModalLayoutBasicElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/basic/modal-layout-basic.stories.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/basic/modal-layout-basic.stories.ts deleted file mode 100644 index 7f04471515..0000000000 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/basic/modal-layout-basic.stories.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Meta, Story } from '@storybook/web-components'; -import { html } from 'lit'; -import { UmbModalLayoutBasicElement, UmbBasicModalData } from './modal-layout-basic.element'; - -export default { - title: 'API/Modals/Layouts/Basic', - component: 'umb-modal-layout-basic', - id: 'modal-layout-basic', -} as Meta; - - -const htmlContent = html` - <uui-table aria-label="Example table" aria-describedby="#some-element-id"> - <!-- Apply styles to the uui-table-column to style the columns. You must have the same number of this elements as you have columns --> - <uui-table-column style="width: 20%;"></uui-table-column> - <uui-table-column style="width: 80%;"></uui-table-column> - - <uui-table-head> - <uui-table-head-cell>Title 1</uui-table-head-cell> - <uui-table-head-cell>Title 2</uui-table-head-cell> - </uui-table-head> - - <uui-table-row> - <uui-table-cell>Cell 1</uui-table-cell> - <uui-table-cell>Cell 2</uui-table-cell> - </uui-table-row> - - <uui-table-row> - <uui-table-cell>Cell 3</uui-table-cell> - <uui-table-cell>Cell 4</uui-table-cell> - </uui-table-row> - </uui-table>`; - - -const data: UmbBasicModalData = { - header: html`<uui-icon name="umb:bug"></uui-icon> Debug: Contexts`, - content: htmlContent, - overlaySize: 'small' -}; - - -export const Overview: Story<UmbModalLayoutBasicElement> = () => html` - <!-- TODO: figure out if generics are allowed for properties: - https://github.com/runem/lit-analyzer/issues/149 - https://github.com/runem/lit-analyzer/issues/163 --> - <umb-modal-layout-basic .data=${data as any}></umb-modal-layout-basic> -`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/content-picker/modal-layout-content-picker.stories.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/content-picker/modal-layout-content-picker.stories.ts deleted file mode 100644 index 632d63f007..0000000000 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/content-picker/modal-layout-content-picker.stories.ts +++ /dev/null @@ -1,28 +0,0 @@ -import '../../../../backoffice/shared/components/body-layout/body-layout.element'; -import './modal-layout-content-picker.element'; - -import { Meta, Story } from '@storybook/web-components'; -import { html } from 'lit'; - -import type { - UmbModalLayoutContentPickerElement, - UmbModalContentPickerData, -} from './modal-layout-content-picker.element'; - -export default { - title: 'API/Modals/Layouts/Content Picker', - component: 'umb-modal-layout-content-picker', - id: 'modal-layout-content-picker', -} as Meta; - -const data: UmbModalContentPickerData = { - multiple: true, - selection: [], -}; - -export const Overview: Story<UmbModalLayoutContentPickerElement> = () => html` - <!-- TODO: figure out if generics are allowed for properties: - https://github.com/runem/lit-analyzer/issues/149 - https://github.com/runem/lit-analyzer/issues/163 --> - <umb-modal-layout-content-picker .data=${data as any}></umb-modal-layout-content-picker> -`; diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/icon-picker/modal-layout-icon-picker.stories.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/icon-picker/modal-layout-icon-picker.stories.ts deleted file mode 100644 index b36e9f04aa..0000000000 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/icon-picker/modal-layout-icon-picker.stories.ts +++ /dev/null @@ -1,25 +0,0 @@ -import '../../../../backoffice/shared/components/body-layout/body-layout.element'; -import './modal-layout-icon-picker.element'; - -import { Meta, Story } from '@storybook/web-components'; -import { html } from 'lit'; - -import type { UmbModalLayoutIconPickerElement, UmbModalIconPickerData } from './modal-layout-icon-picker.element'; - -export default { - title: 'API/Modals/Layouts/Icon Picker', - component: 'umb-modal-layout-icon-picker', - id: 'modal-layout-icon-picker', -} as Meta; - -const data: UmbModalIconPickerData = { - multiple: true, - selection: [], -}; - -export const Overview: Story<UmbModalLayoutIconPickerElement> = () => html` - <!-- TODO: figure out if generics are allowed for properties: - https://github.com/runem/lit-analyzer/issues/149 - https://github.com/runem/lit-analyzer/issues/163 --> - <umb-modal-layout-icon-picker .data=${data as any}></umb-modal-layout-icon-picker> -`; diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/link-picker/modal-layout-multi-url-picker.stories.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/link-picker/modal-layout-multi-url-picker.stories.ts deleted file mode 100644 index 6f9b80f05f..0000000000 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/link-picker/modal-layout-multi-url-picker.stories.ts +++ /dev/null @@ -1,20 +0,0 @@ -import '../../../../backoffice/shared/components/body-layout/body-layout.element'; -import './modal-layout-link-picker.element'; - -import { Meta, Story } from '@storybook/web-components'; -import { html } from 'lit'; - -import type { UmbModalLayoutLinkPickerElement } from './modal-layout-link-picker.element'; - -export default { - title: 'API/Modals/Layouts/Link Picker', - component: 'umb-modal-layout-link-picker', - id: 'modal-layout-link-picker', -} as Meta; - -export const Overview: Story<UmbModalLayoutLinkPickerElement> = () => html` - <!-- TODO: figure out if generics are allowed for properties: - https://github.com/runem/lit-analyzer/issues/149 - https://github.com/runem/lit-analyzer/issues/163 --> - <umb-modal-layout-link-picker></umb-modal-layout-link-picker> -`; diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout.element.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout.element.ts deleted file mode 100644 index bf28bcae3a..0000000000 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout.element.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { customElement, property } from 'lit/decorators.js'; -import { UmbModalHandler } from '..'; -import { UmbLitElement } from '@umbraco-cms/element'; - -@customElement('umb-modal-layout') -export class UmbModalLayoutElement<UmbModalData = void> extends UmbLitElement { - @property({ attribute: false }) - modalHandler?: UmbModalHandler; - - @property({ type: Object }) - data?: UmbModalData; -} - -declare global { - interface HTMLElementTagNameMap { - 'umb-modal-layout': UmbModalLayoutElement<unknown>; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/property-editor-ui-picker/modal-layout-property-editor-ui-picker.stories.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/property-editor-ui-picker/modal-layout-property-editor-ui-picker.stories.ts deleted file mode 100644 index 917bce04dc..0000000000 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/property-editor-ui-picker/modal-layout-property-editor-ui-picker.stories.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Meta, Story } from '@storybook/web-components'; -import { html } from 'lit'; -import type { - UmbModalLayoutPropertyEditorUIPickerElement, - UmbModalPropertyEditorUIPickerData, -} from './modal-layout-property-editor-ui-picker.element'; -import './modal-layout-property-editor-ui-picker.element'; - -import '../../../../backoffice/shared/components/body-layout/body-layout.element'; - -export default { - title: 'API/Modals/Layouts/Property Editor UI Picker', - component: 'umb-modal-layout-property-editor-ui-picker', - id: 'modal-layout-property-editor-ui-picker', -} as Meta; - -const data: UmbModalPropertyEditorUIPickerData = { selection: [] }; - -export const Overview: Story<UmbModalLayoutPropertyEditorUIPickerElement> = () => html` - <umb-modal-layout-property-editor-ui-picker .data=${data as any}></umb-modal-layout-property-editor-ui-picker> -`; diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/modal-handler.ts b/src/Umbraco.Web.UI.Client/src/core/modal/modal-handler.ts deleted file mode 100644 index 2c07d85451..0000000000 --- a/src/Umbraco.Web.UI.Client/src/core/modal/modal-handler.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { UUIDialogElement } from '@umbraco-ui/uui'; -import type { UUIModalDialogElement } from '@umbraco-ui/uui-modal-dialog'; -import type { UUIModalSidebarElement, UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar'; -import { v4 as uuidv4 } from 'uuid'; - -import { UmbModalOptions } from './modal.context'; - -//TODO consider splitting this into two separate handlers -export class UmbModalHandler { - private _closeResolver: any; - private _closePromise: any; - - public element: UUIModalDialogElement | UUIModalSidebarElement; - public key: string; - public type: string; - public size: UUIModalSidebarSize; - - constructor(element: string | HTMLElement, options?: UmbModalOptions<unknown>) { - this.key = uuidv4(); - - this.type = options?.type || 'dialog'; - this.size = options?.size || 'small'; - this.element = this._createElement(element, options); - - // 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._closePromise = new Promise((resolve) => { - this._closeResolver = resolve; - }); - } - - private _createElement(element: string | HTMLElement, options?: UmbModalOptions<unknown>) { - const layoutElement = this._createLayoutElement(element, options?.data); - return this.type === 'sidebar' - ? this._createSidebarElement(layoutElement) - : this._createDialogElement(layoutElement); - } - - private _createSidebarElement(layoutElement: HTMLElement) { - const sidebarElement = document.createElement('uui-modal-sidebar'); - sidebarElement.appendChild(layoutElement); - sidebarElement.size = this.size; - return sidebarElement; - } - - private _createDialogElement(layoutElement: HTMLElement) { - const modalDialogElement = document.createElement('uui-modal-dialog'); - const dialogElement: UUIDialogElement = document.createElement('uui-dialog'); - modalDialogElement.appendChild(dialogElement); - dialogElement.appendChild(layoutElement); - return modalDialogElement; - } - - private _createLayoutElement(element: string | HTMLElement, data: unknown) { - const layoutElement: any = element instanceof HTMLElement ? element : document.createElement(element); - layoutElement.data = data; - layoutElement.modalHandler = this; - return layoutElement; - } - - public close(...args: any) { - this._closeResolver(...args); - this.element.close(); - } - - public onClose(): Promise<any> { - return this._closePromise; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/modal.context.ts b/src/Umbraco.Web.UI.Client/src/core/modal/modal.context.ts deleted file mode 100644 index 1bf8696341..0000000000 --- a/src/Umbraco.Web.UI.Client/src/core/modal/modal.context.ts +++ /dev/null @@ -1,232 +0,0 @@ -// TODO: lazy load -import './layouts/confirm/modal-layout-confirm.element'; -import './layouts/content-picker/modal-layout-content-picker.element'; -import './layouts/media-picker/modal-layout-media-picker.element'; -import './layouts/property-editor-ui-picker/modal-layout-property-editor-ui-picker.element'; -import './layouts/modal-layout-current-user.element'; -import './layouts/icon-picker/modal-layout-icon-picker.element'; -import '../../backoffice/settings/languages/language-picker/language-picker-modal-layout.element'; -import './layouts/link-picker/modal-layout-link-picker.element'; -import './layouts/basic/modal-layout-basic.element'; -import './layouts/search/modal-layout-search.element.ts'; - -import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar'; -import { BehaviorSubject } from 'rxjs'; -import type { UUIModalDialogElement } from '@umbraco-ui/uui-modal-dialog'; -import { UmbModalChangePasswordData } from './layouts/modal-layout-change-password.element'; -import type { UmbModalIconPickerData } from './layouts/icon-picker/modal-layout-icon-picker.element'; -import type { UmbModalConfirmData } from './layouts/confirm/modal-layout-confirm.element'; -import type { UmbModalContentPickerData } from './layouts/content-picker/modal-layout-content-picker.element'; -import type { UmbModalPropertyEditorUIPickerData } from './layouts/property-editor-ui-picker/modal-layout-property-editor-ui-picker.element'; -import type { UmbModalMediaPickerData } from './layouts/media-picker/modal-layout-media-picker.element'; -import type { UmbModalLinkPickerData } from './layouts/link-picker/modal-layout-link-picker.element'; -import { UmbModalHandler } from './modal-handler'; -import type { UmbBasicModalData } from './layouts/basic/modal-layout-basic.element'; -import { UmbPickerModalData } from './layouts/modal-layout-picker-base'; -import { UmbContextToken } from '@umbraco-cms/context-api'; -import { LanguageModel } from '@umbraco-cms/backend-api'; - -export type UmbModalType = 'dialog' | 'sidebar'; - -export interface UmbModalOptions<UmbModalData> { - type?: UmbModalType; - size?: UUIModalSidebarSize; - data?: UmbModalData; -} - -// 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 { - // 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>>[]); - public readonly modals = this.#modals.asObservable(); - - /** - * Opens a Confirm modal - * @public - * @param {UmbModalConfirmData} data - * @return {*} {UmbModalHandler} - * @memberof UmbModalContext - */ - public confirm(data: UmbModalConfirmData): UmbModalHandler { - return this.open('umb-modal-layout-confirm', { data, type: 'dialog' }); - } - - /** - * Opens a Content Picker sidebar modal - * @public - * @param {UmbModalContentPickerData} [data] - * @return {*} {UmbModalHandler} - * @memberof UmbModalContext - */ - public contentPicker(data?: UmbModalContentPickerData): UmbModalHandler { - return this.open('umb-modal-layout-content-picker', { data, type: 'sidebar', size: 'small' }); - } - - /** - * Opens a Media Picker sidebar modal - * @public - * @param {UmbModalMediaPickerData} [data] - * @return {*} {UmbModalHandler} - * @memberof UmbModalContext - */ - public mediaPicker(data?: UmbModalMediaPickerData): UmbModalHandler { - return this.open('umb-modal-layout-media-picker', { data, type: 'sidebar', size: 'small' }); - } - - /** - * Opens a Property Editor UI sidebar modal - * @public - * @param {UmbModalPropertyEditorUIPickerData} [data] - * @return {*} {UmbModalHandler} - * @memberof UmbModalContext - */ - public propertyEditorUIPicker(data?: UmbModalPropertyEditorUIPickerData): UmbModalHandler { - return this.open('umb-modal-layout-property-editor-ui-picker', { data, type: 'sidebar', size: 'small' }); - } - - /** - * Opens an Icon Picker sidebar modal - * @public - * @param {UmbModalIconPickerData} [data] - * @return {*} {UmbModalHandler} - * @memberof UmbModalContext - */ - public iconPicker(data?: UmbModalIconPickerData): UmbModalHandler { - return this.open('umb-modal-layout-icon-picker', { data, type: 'sidebar', size: 'small' }); - } - - /** - * Opens an Link Picker sidebar modal - * @public - * @param {(LinkPickerData & LinkPickerConfig)} [data] - * @return {*} {UmbModalHandler} - * @memberof UmbModalContext - */ - public linkPicker(data?: UmbModalLinkPickerData): UmbModalHandler { - return this.open('umb-modal-layout-link-picker', { - data, - type: 'sidebar', - size: data?.config?.overlaySize || 'small', - }); - } - - /** - * Opens the user settings sidebar modal - * @public - * @return {*} {UmbModalHandler} - * @memberof UmbModalContext - */ - public userSettings(): UmbModalHandler { - return this.open('umb-modal-layout-current-user', { type: 'sidebar', size: 'small' }); - } - - /** - * Opens the change password sidebar modal - * @public - * @return {*} {UmbModalHandler} - * @memberof UmbModalContext - */ - public changePassword(data: UmbModalChangePasswordData): UmbModalHandler { - return this.open('umb-modal-layout-change-password', { data, type: 'dialog' }); - } - - /** - * Opens a language picker sidebar modal - * @public - * @return {*} {UmbModalHandler} - * @memberof UmbModalContext - */ - public languagePicker(data: UmbPickerModalData<LanguageModel>): UmbModalHandler { - return this.open('umb-language-picker-modal-layout', { data, type: 'sidebar' }); - } - - /** - * Opens a basic sidebar modal to display readonly information - * @public - * @return {*} {UmbModalHandler} - * @memberof UmbModalContext - */ - public openBasic(data: UmbBasicModalData): UmbModalHandler { - return this.open('umb-modal-layout-basic', { - data, - type: 'sidebar', - size: data?.overlaySize || 'small', - }); - } - - public search(): UmbModalHandler { - const modalHandler = new UmbModalHandler('umb-modal-layout-search'); - - //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-modal-layout-search'); - dialog.appendChild(search); - requestAnimationFrame(() => { - dialog.showModal(); - }); - modalHandler.element = dialog as unknown as UUIModalDialogElement; - //TODO END - - modalHandler.element.addEventListener('close-end', () => this._handleCloseEnd(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(element: string | HTMLElement, options?: UmbModalOptions<unknown>): UmbModalHandler { - const modalHandler = new UmbModalHandler(element, options); - - modalHandler.element.addEventListener('close-end', () => this._handleCloseEnd(modalHandler)); - - this.#modals.next([...this.#modals.getValue(), modalHandler]); - return modalHandler; - } - - /** - * Closes a modal or sidebar modal - * @private - * @param {string} key - * @memberof UmbModalContext - */ - private _close(key: string) { - this.#modals.next(this.#modals.getValue().filter((modal) => modal.key !== key)); - } - - /** - * Handles the close-end event - * @private - * @param {UmbModalHandler} modalHandler - * @memberof UmbModalContext - */ - private _handleCloseEnd(modalHandler: UmbModalHandler) { - modalHandler.element.removeEventListener('close-end', () => this._handleCloseEnd(modalHandler)); - this._close(modalHandler.key); - } -} - -export const UMB_MODAL_CONTEXT_TOKEN = new UmbContextToken<UmbModalContext>('UmbModalContext'); diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/stories/modal.mdx b/src/Umbraco.Web.UI.Client/src/core/modal/stories/modal.mdx deleted file mode 100644 index 262d7bb65d..0000000000 --- a/src/Umbraco.Web.UI.Client/src/core/modal/stories/modal.mdx +++ /dev/null @@ -1,101 +0,0 @@ -import { Meta } from '@storybook/addon-docs'; - -<Meta title="API/Modals/Intro" parameters={{ previewTabs: { canvas: { hidden: true } } }} /> - -# 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. - -```typescript -import { html, LitElement } from 'lit'; -import { UmbLitElement } from '@umbraco-cms/element'; -import { UmbModalContext, UMB_MODAL_CONTEXT_ALIAS } from '@umbraco-cms/modal'; -class MyElement extends UmbLitElement { - #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 one of the helper methods on the UmbModalContext. The methods will accept an element template and modal options and return an instance of UmbModalHandler. - -```typescript -import { html, LitElement } from 'lit'; -import { UmbLitElement } from '@umbraco-cms/element'; -import { UmbModalContext, UMB_MODAL_CONTEXT_ALIAS } from './core/services/modal'; -class MyElement extends UmbLitElement { - #modalContext?: UmbModalContext; - - constructor() { - super(); - this.consumeContext(UMB_MODAL_CONTEXT_ALIAS, (instance) => { - this.#modalContext = instance; - // modalContext is now ready to be used - }); - } - - #onClick() { - const options = {'options goes here'} - const modalHandler = this.#modalContext?.openDialog('my-dialog'), options); - modalHandler.onClose().then((data) => { - // if any data is supplied on close, it will be available here. - }); - } - - render() { - return html`<button @click=${this.#onClick}>Open modal</button>`; - } -} -``` - -The dialog template to open: - -```typescript -import { html, LitElement } from 'lit'; -import type { UmbModalHandler } from './core/services/modal'; - -class MyDialog extends LitElement { - // the modal handler will be injected into the element when the modal is opened. - @property({ attribute: false }) - modalHandler?: UmbModalHandler; - - private _handleClose() { - /* Optional data of any type can be applied to the close method to pass it - to the modal parent through the onClose promise. */ - this._modalHandler?.close('optional data'); - } - - render() { - return html` - <div> - <h1>My Dialog</h1> - <button @click=${this._handleClose}>Close</button> - </div> - `; - } -} -``` diff --git a/src/Umbraco.Web.UI.Client/src/core/router/router-slot-change.event.ts b/src/Umbraco.Web.UI.Client/src/core/router/router-slot-change.event.ts index d5004c1d27..5b9c9bd806 100644 --- a/src/Umbraco.Web.UI.Client/src/core/router/router-slot-change.event.ts +++ b/src/Umbraco.Web.UI.Client/src/core/router/router-slot-change.event.ts @@ -1,7 +1,8 @@ import { UUIEvent } from '@umbraco-ui/uui-base/lib/events'; import type { UmbRouterSlotElement } from './router-slot.element'; export class UmbRouterSlotChangeEvent extends UUIEvent<never, UmbRouterSlotElement> { + static readonly CHANGE = 'change'; constructor() { - super('change'); + super(UmbRouterSlotChangeEvent.CHANGE); } } diff --git a/src/Umbraco.Web.UI.Client/src/core/router/router-slot-init.event.ts b/src/Umbraco.Web.UI.Client/src/core/router/router-slot-init.event.ts index 896abe1825..0a4895e968 100644 --- a/src/Umbraco.Web.UI.Client/src/core/router/router-slot-init.event.ts +++ b/src/Umbraco.Web.UI.Client/src/core/router/router-slot-init.event.ts @@ -1,7 +1,8 @@ import { UUIEvent } from '@umbraco-ui/uui-base/lib/events'; import type { UmbRouterSlotElement } from './router-slot.element'; export class UmbRouterSlotInitEvent extends UUIEvent<never, UmbRouterSlotElement> { + static readonly INIT = 'init'; constructor() { - super('init'); + super(UmbRouterSlotInitEvent.INIT); } } diff --git a/src/Umbraco.Web.UI.Client/src/core/router/router-slot.element.ts b/src/Umbraco.Web.UI.Client/src/core/router/router-slot.element.ts index a343c9acf4..474e5d64a3 100644 --- a/src/Umbraco.Web.UI.Client/src/core/router/router-slot.element.ts +++ b/src/Umbraco.Web.UI.Client/src/core/router/router-slot.element.ts @@ -9,8 +9,8 @@ import { UmbRouterSlotChangeEvent } from './router-slot-change.event'; * @element umb-router-slot-element * @description - Component for wrapping Router Slot element, providing some local events for implementation. * @extends UmbRouterSlotElement - * @fires {UmbRouterSlotInitEvent} init - fires when the media card is selected - * @fires {UmbRouterSlotChangeEvent} change - fires when the media card is unselected + * @fires {UmbRouterSlotInitEvent} init - fires when the router is connected + * @fires {UmbRouterSlotChangeEvent} change - fires when a path of this router is changed */ @customElement('umb-router-slot') export class UmbRouterSlotElement extends LitElement { @@ -22,6 +22,30 @@ export class UmbRouterSlotElement extends LitElement { return (this.#router as any).routes; } public set routes(value: IRoute[] | undefined) { + /* + Concept for extending routes with modal routes. + const routesWithModals = value?.map((route, i, array) => { + { + path: 'bla/:key/' + component: () => { + return import('.....'); + } + setup: () => { + ... + } + } + + if (route.path === '') { + { + ...route, + path: route.path + '/modal/:modal-alias', + setup: () => { + route.setup?.(); + // Call modal service to open modal. + } + } + }); + */ (this.#router as any).routes = value; } @@ -39,6 +63,12 @@ export class UmbRouterSlotElement extends LitElement { return this._routerPath + '/' + this._activeLocalPath; } + constructor() { + super(); + this.#router.addEventListener('changestate', this._onChangeState); + this.#router.appendChild(document.createElement('slot')); + } + connectedCallback() { super.connectedCallback(); if (this.#listening === false) { @@ -59,6 +89,20 @@ export class UmbRouterSlotElement extends LitElement { this.dispatchEvent(new UmbRouterSlotInitEvent()); } + private _onChangeState = () => { + const newAbsolutePath = this.#router.constructAbsolutePath('') || ''; + if (this._routerPath !== newAbsolutePath) { + this._routerPath = newAbsolutePath; + this.dispatchEvent(new UmbRouterSlotInitEvent()); + + const newActiveLocalPath = this.#router.match?.route.path; + if (this._activeLocalPath !== newActiveLocalPath) { + this._activeLocalPath = newActiveLocalPath; + this.dispatchEvent(new UmbRouterSlotChangeEvent()); + } + } + }; + private _onNavigationChanged = (event?: any) => { if (event.detail.slot === this.#router) { this._activeLocalPath = event.detail.match.route.path; diff --git a/src/Umbraco.Web.UI.Client/src/stories/contextapi.mdx b/src/Umbraco.Web.UI.Client/src/stories/context-api.mdx similarity index 86% rename from src/Umbraco.Web.UI.Client/src/stories/contextapi.mdx rename to src/Umbraco.Web.UI.Client/src/stories/context-api.mdx index 51829d1e4d..1765929ced 100644 --- a/src/Umbraco.Web.UI.Client/src/stories/contextapi.mdx +++ b/src/Umbraco.Web.UI.Client/src/stories/context-api.mdx @@ -2,20 +2,19 @@ import { Meta } from '@storybook/blocks'; <Meta title="Guides/Context API" /> - # Context API - This element can be used as the base of any Element. Do this if you need to Observe Data, Consume or Provide a Context API or use a Resource. The Element implements the Controller Host and provides a few shortcut methods for initializing some Controllers. -The methods are (*note this can be out of date, we need to look into how we can ensure this Doc originates from code.*) +The methods are (_note this can be out of date, we need to look into how we can ensure this Doc originates from code._) ### Consume a Context API. From a Umbraco Element: -```typescript + +```ts this.consumeContext('requestThisContextAlias', (context) => { // Notice this is a subscription, as context might change or a new one appears. console.log("I've got the context", context); @@ -24,7 +23,7 @@ this.consumeContext('requestThisContextAlias', (context) => { Or with a Controller using a Controller Host(Umbraco Element): -```typescript +```ts new UmbContextConsumerController(hostElement, 'requestThisContextAlias', (context) => { // Notice this is a subscription, as context might change or a new one appears. console.log("I've got the context", context); @@ -34,12 +33,13 @@ new UmbContextConsumerController(hostElement, 'requestThisContextAlias', (contex ### Provide a Context API. From a Umbraco Element: -```typescript + +```ts this.provideContext('myContextAlias', new MyContextApi()); ``` Or with a Controller using a Controller Host(Umbraco Element): -```typescript +```ts new UmbContextProviderController(hostElement, 'myContextAlias', new MyContextApi()); ``` diff --git a/src/Umbraco.Web.UI.Client/src/stories/extending/entity-actions.mdx b/src/Umbraco.Web.UI.Client/src/stories/extending/entity-actions.mdx new file mode 100644 index 0000000000..e7847203d0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/stories/extending/entity-actions.mdx @@ -0,0 +1,187 @@ +import { Meta } from '@storybook/addon-docs'; + +<Meta + title="Guides/Extending the Backoffice/Entity Actions" + parameters={{ previewTabs: { canvas: { hidden: true } } }} +/> + +# Entity Actions + +TODO: introduction to actions + +- **Entity Action:** + Relates to an entity type: (document, media, etc.). Performs the action on a specific item. + +- **Entity Bulk Action:** + Relates to an entity type: document, media, etc. Performs the action on a selection of items. + +## Entity Actions in the UI + +<table> + <tbody> + <tr> + <td> + <div> + <strong>Sidebar Context Menu</strong> + </div> + <img src="docs/entity-action-sidebar-context.svg" width="400" /> + </td> + <td> + <div> + <strong>Workspace Action Menu</strong> + </div> + <img src="docs/entity-action-workspace-menu.svg" width="400" /> + </td> + </tr> + <tr> + <td> + <div> + <strong>Collection</strong> + </div> + <img src="docs/entity-action-collection-menu.svg" width="400" /> + </td> + <td> + <div> + <strong>Pickers</strong> + </div> + <img src="docs/entity-action-picker-context-menu.svg" width="400" /> + </td> + </tr> + </tbody> +</table> + +### Registering an Entity Action + +TODO: can we show the typescript interface for the manifest? + +```javascript +import { extensionRegistry } from '@umbraco-cms/extension-registry'; +import { MyEntityAction } from './entity-action'; + +const manifest = { + type: 'entityAction', + alias: 'My.EntityAction', + name: 'My Entity Action', + weight: 10, + meta: { + entityType: 'my-entity', + icon: 'umb:add', + label: 'My Entity Action', + repositoryAlias: 'My.Repository', + api: MyEntityAction, + }, +}; + +extensionRegistry.register(manifest); +``` + +**Default Element** + +```ts +// TODO: get interface +interface UmbEntityActionElement {} +``` + +### The Entity Action Class + +As part of the Extension Manifest you can attach a class that will be instanciated as part of the action. It will have access to the host element, a repository with the given alias and the unique (key etc) of the entity. When the action is clicked the `execute` method on the api class will be run. When the action is completed, an event on the host element will be dispatched to notify any surrounding elements. + +```ts +import { UmbEntityActionBase } from '@umbraco-cms/entity-action'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import type { MyRepository } from './my-repository'; + +export class MyEntityAction extends UmbEntityActionBase<MyRepository> { + constructor(host: UmbControllerHostInterface, repositoryAlias: string, unique: string) { + super(host, repositoryAlias, unique); + } + + async execute() { + await this.repository.myAction(this.unique); + } +} +``` + +If any additional contexts are needed, these can be consumed from the host element: + +```ts +import { UmbEntityActionBase } from '@umbraco-cms/entity-action'; +import { UmbContextConsumerController } from '@umbraco-cms/controller'; +import { UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/modal'; +import { MyRepository } from './my-repository'; + +export class MyEntityAction extends UmbEntityActionBase<MyRepository> { + constructor(host: UmbControllerHostInterface, repositoryAlias: string, unique: string) { + super(host, repositoryAlias, unique); + + new UmbContextConsumerController(this.host, UMB_MODAL_SERVICE_CONTEXT_TOKEN, (instance) => { + this.#modalService = instance; + }); + } + ... +} +``` + +We currently have a couple of generic actions that can be used across silos, so we don't have to write the same logic again: copy, move, trash, delete, etc. We can add more as we discover the needs. + +TODO: List generic actions + List what alias' they are registered under. + +## Entity Bulk Actions in the UI + +<table> + <tbody> + <tr> + <td> + <div> + <strong>Collection</strong> + </div> + <img src="docs/entity-bulk-action-collection-menu.svg" width="400" /> + </td> + </tr> + </tbody> +</table> + +### Registering an Entity Bulk Action + +TODO: can we show the typescript interface for the manifest? + +```javascript +import { extensionRegistry } from '@umbraco-cms/extension-registry'; +import { MyEntityBulkAction } from './entity-bulk-action'; + +const manifest = { + type: 'entityBulkAction', + alias: 'My.EntityBulkAction', + name: 'My Entity Bulk Action', + weight: 10, + meta: { + entityType: 'my-entity', + icon: 'umb:add', + label: 'My Entity Bulk Action', + repositoryAlias: 'My.Repository', + api: MyEntityBulkAction, + }, +}; + +extensionRegistry.register(manifest); +``` + +### The Entity Bulk Action Class + +As part of the Extension Manifest you can attach a class that will be instanciated as part of the action. It will have access to the host element, a repository with the given alias and the unique (key etc) of the entity. When the action is clicked the `execute` method on the api class will be run. When the action is completed, an event on the host element will be dispatched to notify any surrounding elements. + +```ts +import { UmbEntityBulkActionBase } from '@umbraco-cms/entity-action'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { MyRepository } from './my-repository'; + +export class MyEntityBulkAction extends UmbEntityBulkActionBase<MyRepository> { + constructor(host: UmbControllerHostInterface, repositoryAlias: string, selection: Array<string>) { + super(host, repositoryAlias, selection); + } + + async execute() { + await this.repository?.myBulkAction(this.selection); + } +} +``` diff --git a/src/Umbraco.Web.UI.Client/src/stories/extending/header-apps.mdx b/src/Umbraco.Web.UI.Client/src/stories/extending/header-apps.mdx new file mode 100644 index 0000000000..3aff51bf16 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/stories/extending/header-apps.mdx @@ -0,0 +1,60 @@ +import { Meta } from '@storybook/addon-docs'; + +<Meta title="Guides/Extending the Backoffice/Header Apps" parameters={{ previewTabs: { canvas: { hidden: true } } }} /> + +# Header Apps + +TODO: Describe header apps + +<table> + <tbody> + <tr> + <td> + <img src="docs/header-apps.svg" width="400" /> + </td> + </tr> + </tbody> +</table> + +**Manifest** + +```json +// TODO: get interface +{ + "type": "headerApp", + "alias": "My.HeaderApp", + "name": "My Header App", + "elementName": "my-header-app", + "js": "./my-header-app.js", + "weight": 10, + "meta": { + "label": "My Header App", + "icon": "umb:icon-name", + "pathname": "search", + }, + }, + +``` + +**Default Element** + +```ts +// TODO: get interface +interface UmbHeaderAppElement {} +``` + +#### Button Header App + +**Manifest** + +```json +// TODO: get interface +{} +``` + +**Default Element** + +```ts +// TODO: get interface +interface UmbButtonHeaderAppElement {} +``` diff --git a/src/Umbraco.Web.UI.Client/src/stories/extending/intro.mdx b/src/Umbraco.Web.UI.Client/src/stories/extending/intro.mdx new file mode 100644 index 0000000000..c0ede6a70b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/stories/extending/intro.mdx @@ -0,0 +1,18 @@ +import { Meta } from '@storybook/addon-docs'; + +<Meta title="Guides/Extending the Backoffice/Intro" parameters={{ previewTabs: { canvas: { hidden: true } } }} /> + +# UI Extensions + +TODO: introduction to the extendable UI + +The Umbraco Backoffice currently support the following extension types: + +- [Registration](?path=/docs/guides-extending-the-backoffice-registration--docs) +- [Header App](?path=/docs/guides-extending-the-backoffice-header-apps--docs) +- [Section](?path=/docs/guides-extending-the-backoffice-sections--docs) +- [Entity Action](?path=/docs/guides-extending-the-backoffice-entity-actions--docs) +- [Workspace](?path=/docs/guides-extending-the-backoffice-workspaces-intro--docs) +- [Property Editor](?path=/docs/guides-extending-the-backoffice-property-editors--docs) +- [Repository](?path=/docs/guides-extending-the-backoffice-property-editors--docs) +- [Menu](?path=/docs/guides-extending-the-backoffice-menu--docs) diff --git a/src/Umbraco.Web.UI.Client/src/stories/extending/menu.mdx b/src/Umbraco.Web.UI.Client/src/stories/extending/menu.mdx new file mode 100644 index 0000000000..7082738d66 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/stories/extending/menu.mdx @@ -0,0 +1,83 @@ +import { Meta } from '@storybook/addon-docs'; + +<Meta title="Guides/Extending the Backoffice/Menu" parameters={{ previewTabs: { canvas: { hidden: true } } }} /> + +# Menu + +TODO: introduction to the menu +The menu is still work in progress + +<img src="docs/menu.svg" width="150" /> + +```json +{ + "type": "menu", + "alias": "My.Menu", + "name": "My Menu" +} +``` + +### Menu Item + +TODO: introduction to the menu item + +<img src="docs/menu-item.svg" width="150" /> + +```json +{ + "type": "menuItem", + "alias": "My.MenuItem", + "name": "My Menu Item", + "meta": { + "label": "My Menu Item", + "menus": ["My.Menu"] + } +} +``` + +#### Tree Menu Item + +// TODO adds docs when we have extension kinds + +**Manifest** + +```json +// it will be something like this +{ + "type": "menuItem", + "kind": "tree", + "alias": "My.TreeMenuItem", + "name": "My Tree Menu Item", + "meta": { + "label": "My Tree Menu Item", + "menus": ["My.Menu"] + } +} +``` + +**Default Element** + +```ts +// get interface +interface UmbTreeMenuItemElement {} +``` + +#### Adding menu items to an existing menu + +The backoffice comes with a couple of menus. + +- Content, Media, Settings, Templating, Dictionary, etc. + +To add a menu item to an existing menu, you can use the `meta.menus` property. + +```json +{ + "type": "menuItem", + "alias": "My.MenuItem", + "name": "My Menu Item", + "meta": { + "label": "My Menu Item", + "menus": ["Umb.Menu.Content"] + } +} +``` diff --git a/src/Umbraco.Web.UI.Client/src/stories/extending/property-editors.mdx b/src/Umbraco.Web.UI.Client/src/stories/extending/property-editors.mdx new file mode 100644 index 0000000000..849adeac35 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/stories/extending/property-editors.mdx @@ -0,0 +1,134 @@ +import { Meta } from '@storybook/addon-docs'; + +<Meta + title="Guides/Extending the Backoffice/Property Editors" + parameters={{ previewTabs: { canvas: { hidden: true } } }} +/> + +# Property Editors + +// TODO: add description +(rough notes. We have a lot on docs.umbraco.com we can use) + +- This section describes how to work with and create Property Editors. A property editor is the editor used to insert content into Umbraco. +- Property editor are defined in two parts. +- The Property Editor and the Property Editor UI. +- The Property Editor UI is the UI that is used to edit the data in the backoffice. +- Each Property Editor can have multiple Property Editor UIs. + +- Both a Property Editor and Property Editor UI can have a configuration. +- The Property Editor configuration is used for configuration that the server needs to know about. +- The Property Editor UI configuration is used for configuration that is related to rendering the UI in the backoffice. + +## Property Editor + +**Manifest** + +```json +{ + "type": "propertyEditorModel", + "name": "Text Box", + "alias": "Umbraco.TextBox", +}; +``` + +## Property Editor UI + +```json +{ + "type": "propertyEditorUI", + "alias": "Umb.PropertyEditorUI.TextBox", + "name": "Text Box Property Editor UI", + "elementName": "my-text-box", + "js": "./my-text-box.element.js", + "meta": { + "label": "My Text Box", + "propertyEditorModel": "Umbraco.TextBox", + "icon": "umb:autofill", + "group": "common" + } +} +``` + +If no Property Editor is specified in the manifest, the Propety Editor will use a generic JSON Property Editor Model + +## Configuration + +// TODO: add description + +- Configuration for a Property Editor or Property Editor UI is defined in the manifest. +- The both use the same configuration format. +- It is possible to use another Property Editor UI as the editor for a configuration property. + +**Manifest** + +```json +{ + "type": "propertyEditorUI", + "alias": "My.PropertyEditorUI.TextArea", + //... more + "meta": { + //... more + "config": { + "properties": [ + { + "alias": "rows", + "label": "Number of rows", + "description": "If empty - 10 rows would be set as the default value", + "propertyEditorUI": "Umb.PropertyEditorUI.Number", + }, + ], + "defaultData": [ + { + "alias": "rows", + "value": 10, + }, + ], + }, + }, +}; +``` + +## The Property Editor UI Element + +```ts +// TODO: get interface +interface UmbPropertyEditorUIElement {} +``` + +**Example with LitElement** + +```ts +import { LitElement, html, css } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { UUITextStyles } from '@umbraco-ui/uui-base/lib/styles'; +import { UmbElement } from '@umbraco-cms/element'; + +// TODO: should we make examples with LitElement or just vanilla JS? or should we have for more libraries? +@customElement('my-text-box') +export class UmbPropertyEditorUITextBoxElement extends UmbElement(LitElement) { + static styles = [ + UUITextStyles, + css` + uui-input { + width: 100%; + } + `, + ]; + + @property() + value = ''; + + @property({ type: Array, attribute: false }) + public config = []; + + private onInput(e: InputEvent) { + this.value = (e.target as HTMLInputElement).value; + this.dispatchEvent(new CustomEvent('property-value-change')); + } + + render() { + return html`<uui-input .value=${this.value} type="text" @input=${this.onInput}></uui-input>`; + } +} +``` diff --git a/src/Umbraco.Web.UI.Client/src/stories/extending/registration.mdx b/src/Umbraco.Web.UI.Client/src/stories/extending/registration.mdx new file mode 100644 index 0000000000..2366eecb43 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/stories/extending/registration.mdx @@ -0,0 +1,79 @@ +import { Meta } from '@storybook/addon-docs'; + +<Meta title="Guides/Extending the Backoffice/Registration" parameters={{ previewTabs: { canvas: { hidden: true } } }} /> + +# Registration + +TODO: describe the registration process and the extension registry. +TODO: add typescript interface + +```ts +import { extensionRegistry } from '@umbraco-cms/extension-registry'; +``` + +## Package Manifest + +TODO: Describe the Package Manifest + +```json +// show example of package-manifest.json +``` + +## UI Extension Registration + +Registering UI extensions happens through the global extension registry. + +There are two ways to register UI extensions: + +1. Reference an entry point resource in the manifest where full control over registration can take place. +2. Directly through a manifest file + +### Entry Point Manifest + +TODO: describe where to position the entry point manifest file. Describe the UI extension manifest. + +**_Register an entry point in a JSON manifest_** + +```json +{ + "type": "entryPoint", + "alias": "My.EntryPoint", + "js": "./index.js" +} +``` + +**_Register additional UI extensions in the entry point file_** + +```ts +import { extensionRegistry } from "@umbraco-cms/extension-registry" + +const manifest = { + { + type: '', // type of extension + alias: '', // unique alias for the extension + elementName: '', // unique name of the custom element + js: '', // path to the javascript resource + meta: { + // additional props for the extension type + } + } +}; + +extensionRegistry.register(extension); +``` + +### JSON Manifest files + +TODO: describe how and where to position the JSON manifest files + +```json +{ + "type": "", // type of extension + "alias": "", // unique alias for the extension + "elementName": "", // unique name of the custom element + "js": "", // path to the javascript resource + "meta": { + // additional props for the extension type + } +} +``` diff --git a/src/Umbraco.Web.UI.Client/src/stories/extending/repositories.mdx b/src/Umbraco.Web.UI.Client/src/stories/extending/repositories.mdx new file mode 100644 index 0000000000..7d36cf6978 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/stories/extending/repositories.mdx @@ -0,0 +1,38 @@ +import { Meta } from '@storybook/addon-docs'; + +<Meta title="Guides/Extending the Backoffice/Repositories" parameters={{ previewTabs: { canvas: { hidden: true } } }} /> + +# Repositories + +TODO: make this understandable for others + +A repository is the Backoffices entry point to request data and get notified about updates. Each domain should register their own repository in the Backoffice. + +## Register a repository: + +```js +import { umbExtensionRegistry } from '@umbraco-cms/extensions-registry'; +import { MyRepository } from './MyRepository'; + +const repositoryManifest = { + type: 'repository', + alias: 'My.Repository', + name: 'My Repository', + class: MyRepository, +}; +``` + +With a repository we can have different data sources depending on the state of the app. It can be from a server, an offline database, a store, a Signal-R connection, etc. That means that the consumer will not have to be concerned how to access the data, add or remove items from a collection of items, etc. This means we get a loose connection between the consumer and the data storing procedures hiding all complex implementation. + +## Data flow with a repository + +<img src="docs/data-flow.svg" width="400" /> + +A repository has to be instanced in the context where it is used. It should take a host element as part of the constructor, so any contexts consumed in the repository (notifications, modals, etc.) get rendered in the correct DOM context. + +A repository can be called directly from an element, but will often be instantiated in a context, like the Workspace Context. + +## The Repository Class + +TODO: get typescript interface +TODO: show repository example: diff --git a/src/Umbraco.Web.UI.Client/src/stories/extending/sections/intro.mdx b/src/Umbraco.Web.UI.Client/src/stories/extending/sections/intro.mdx new file mode 100644 index 0000000000..61a68757a8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/stories/extending/sections/intro.mdx @@ -0,0 +1,47 @@ +import { Meta } from '@storybook/addon-docs'; + +<Meta + title="Guides/Extending the Backoffice/Sections/Intro" + parameters={{ previewTabs: { canvas: { hidden: true } } }} +/> + +# Sections + +TODO: Introduction to sections + +<img src="docs/section.svg" width="400" /> + +**Manifest** + +```json +// TODO: get interface +{ + "type": "section", + "alias": "My.Section", + "name": "My Section", + "meta": { + "label": "My Section", + "pathname": "my-section" + } +} +``` + +**Default Element** + +```ts +// TODO: get interface +interface UmbSectionElement {} +``` + +## The Section Context + +**Interface** + +```ts +// TODO: get interface +interface UmbSectionContext {} +``` + +## Examples of sections: + +TODO: link to all sections in storybook. Can we somehow auto-generate this list? diff --git a/src/Umbraco.Web.UI.Client/src/stories/extending/sections/sidebar.mdx b/src/Umbraco.Web.UI.Client/src/stories/extending/sections/sidebar.mdx new file mode 100644 index 0000000000..5c076b5321 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/stories/extending/sections/sidebar.mdx @@ -0,0 +1,83 @@ +import { Meta } from '@storybook/addon-docs'; + +<Meta + title="Guides/Extending the Backoffice/Sections/Sidebar" + parameters={{ previewTabs: { canvas: { hidden: true } } }} +/> + +# Section Sidebar + +TODO: Introduction to section sidebar + +<img src="docs/section-sidebar.svg" width="400" /> + +### Section Sidebar Apps + +TODO: Introduction to Section Sidebar Apps + +<img src="docs/section-sidebar-apps.svg" width="400" /> + +**Manifest** + +```json +// TODO: add interface +{ + "type": "sectionSidebarApp", + "alias": "My.SectionSidebarApp", + "name": "My Section Sidebar App", + "meta": { + "sections": ["My.Section"] + } +} +``` + +**Default Element** + +```ts +// TODO: get interface +interface UmbSectionSidebarAppElement {} +``` + +#### Menu Sidebar App + +TODO: Introduction to the sidebar menu + +(rough notes) + +- The Backoffice comes with a menu sidebar app that can be used to create a menu in the sidebar. +- To register a new menu sidebar app, add the following to your manifest +- The menu sidebar app will reference a menu that you have registered in the menu with a menu manifest + +<img src="docs/section-menu-sidebar-app.svg" width="400" /> + +**Manifest** + +```json +{ + "type": "menuSectionSidebarApp", + "alias": "My.SectionSidebarApp.MyMenu", + "name": "My Menu Section Sidebar App", + "meta": { + "label": "My Sidebar Menu", + "sections": ["My.Section"], + "menu": "My.Menu" + } +} +``` + +**Default Element** + +```ts +// TODO: get interface +interface UmbMenuSectionSidebarAppElement {} +``` + +[See Menu Docs](?path=/docs/guides-extending-the-backoffice-menu--docs) + +This will make it possible to compose a sidebar menu from multiple Apps: + +<img src="docs/section-sidebar-composed-apps.svg" width="400" /> + +#### Adding Items to an existing menu + +[See Menu Docs](?path=/docs/guides-extending-the-backoffice-menu--docs) diff --git a/src/Umbraco.Web.UI.Client/src/stories/extending/sections/views.mdx b/src/Umbraco.Web.UI.Client/src/stories/extending/sections/views.mdx new file mode 100644 index 0000000000..6ca7288f27 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/stories/extending/sections/views.mdx @@ -0,0 +1,27 @@ +import { Meta } from '@storybook/addon-docs'; + +<Meta + title="Guides/Extending the Backoffice/Sections/Views" + parameters={{ previewTabs: { canvas: { hidden: true } } }} +/> + +# Section Views + +TODO: add description of section views + +<img src="docs/section-views.svg" width="400" /> + +**Manifest** + +```json +{ + "type": "sectionView", + "alias": "My.SectionView", + "name": "My Section View", + "meta": { + "sections": ["My.Section"], + "label": "My View", + "pathname": "/my-view" + } +} +``` diff --git a/src/Umbraco.Web.UI.Client/src/stories/extending/workspaces/actions.mdx b/src/Umbraco.Web.UI.Client/src/stories/extending/workspaces/actions.mdx new file mode 100644 index 0000000000..994c52edd7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/stories/extending/workspaces/actions.mdx @@ -0,0 +1,63 @@ +import { Meta } from '@storybook/addon-docs'; + +<Meta + title="Guides/Extending the Backoffice/Workspaces/Actions" + parameters={{ previewTabs: { canvas: { hidden: true } } }} +/> + +# Workspace Actions + +// TODO: intro to workspace actions + +- Relates to a workspace alias (Umb.Workspace.Document). +- Performs the action on the workspace draft state. +- Has Access to the workspace context. + +<img src="docs/workspace-actions.svg" width="400" /> + +**Manifest** + +```javascript +import { extensionRegistry } from '@umbraco-cms/extension-registry'; +import { MyWorkspaceAction } from './my-workspace-action'; + +const manifest = { + type: 'workspaceAction', + alias: 'My.WorkspaceAction', + name: 'My Workspace Action', + meta: { + workspaces: ['My.Workspace'], + label: 'My Action', + api: MyWorkspaceAction, + }, +}; + +extensionRegistry.register(manifest); +``` + +### The Workspace Action Class + +As part of the Extension Manifest you can attach a class that will be instanciated as part of the action. It will have access to the host element and the Workspace Context. When the action is clicked the `execute` method on the api class will be run. When the action is completed, an event on the host element will be dispatched to notify any surrounding elements. + +```ts +import { UmbEntityBulkActionBase } from '@umbraco-cms/entity-action'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { MyRepository } from './my-repository'; + +export class MyWorkspaceAction extends UmbWorkspaceActionBase { + constructor(host: UmbControllerHostInterface) { + super(host); + } + + async execute() { + await this.workspaceContext.repository?.myAction(this.selection); + } +} +``` + +**Default Element** + +```ts +// TODO: get interface +interface UmbWorkspaceActionElement {} +``` diff --git a/src/Umbraco.Web.UI.Client/src/stories/extending/workspaces/context.mdx b/src/Umbraco.Web.UI.Client/src/stories/extending/workspaces/context.mdx new file mode 100644 index 0000000000..1ea5c76b9b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/stories/extending/workspaces/context.mdx @@ -0,0 +1,27 @@ +import { Meta } from '@storybook/addon-docs'; + +<Meta + title="Guides/Extending the Backoffice/Workspaces/Context" + parameters={{ previewTabs: { canvas: { hidden: true } } }} +/> + +# Workspace Context + +A Workspace context is a container for the data of a workspace. It is a wrapper around the data of the entity that the workspace is working on. It is also responsible for loading and saving the data to the server. +TODO: extend the description of a workspace + +(rough notes) + +- A workspace context knows about its entity type (e.g. content, media, member, etc.) and holds its unique string (ex: key). +- Most workspaces contexts holds a draft state of its entities data. It is a copy of the entity data that can be modified at runtime and send to the server to be saved. + +TODO: More points and examples: + +```ts +// TODO: get typescript interface +interface UmbWorkspaceContext {} +``` + +## Examples of workspaces: + +TODO: link to all workspaces in storybook. Can we somehow auto-generate this list? diff --git a/src/Umbraco.Web.UI.Client/src/stories/extending/workspaces/intro.mdx b/src/Umbraco.Web.UI.Client/src/stories/extending/workspaces/intro.mdx new file mode 100644 index 0000000000..a42638a304 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/stories/extending/workspaces/intro.mdx @@ -0,0 +1,33 @@ +import { Meta } from '@storybook/addon-docs'; + +<Meta + title="Guides/Extending the Backoffice/Workspaces/Intro" + parameters={{ previewTabs: { canvas: { hidden: true } } }} +/> + +# Workspaces + +A Workspace is the editor for a specific entity type. It can either be a simple view of data or a complex editor with multiple views. +TODO: extend the description of a workspace + +(rough notes) + +- A workspace is based on a entity type (e.g. content, media, member, etc.) and a unique string (ex: key). +- Most workspaces holds a draft state of an entity. It is a copy of the entity data that can be modified at runtime and send to the server to be saved. +- A workspace can be a single view or consist of multiple views. +- A workspace should host a workspace context, of which anything within can communicate with. + +<img src="docs/workspace.svg" width="400" /> + +```ts +// TODO: get typescript interface +interface UmbWorkspaceElement {} +``` + +## The Workspace Context + +[Read more about Workspace Context](?path=/docs/guides-extending-the-backoffice-workspaces-context--docs) + +## Examples of workspaces: + +TODO: link to all workspaces in storybook. Can we somehow auto-generate this list? diff --git a/src/Umbraco.Web.UI.Client/src/stories/extending/workspaces/views.mdx b/src/Umbraco.Web.UI.Client/src/stories/extending/workspaces/views.mdx new file mode 100644 index 0000000000..66d892267f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/stories/extending/workspaces/views.mdx @@ -0,0 +1,27 @@ +import { Meta } from '@storybook/addon-docs'; + +<Meta + title="Guides/Extending the Backoffice/Workspaces/Views" + parameters={{ previewTabs: { canvas: { hidden: true } } }} +/> + +# Workspace Views + +TODO: add description of a workspace views + +<img src="docs/workspace-views.svg" width="400" /> + +**Manifest** + +```json +{ + "type": "workspaceView", + "alias": "My.WorkspaceView", + "name": "My Workspace View", + "meta": { + "workspaces": ["My.Workspace"], + "label": "My View", + "pathname": "/my-view" + } +} +``` diff --git a/src/Umbraco.Web.UI.Client/src/stories/gettingstarted.mdx b/src/Umbraco.Web.UI.Client/src/stories/getting-started.mdx similarity index 94% rename from src/Umbraco.Web.UI.Client/src/stories/gettingstarted.mdx rename to src/Umbraco.Web.UI.Client/src/stories/getting-started.mdx index a2dd678c88..9769fd34c2 100644 --- a/src/Umbraco.Web.UI.Client/src/stories/gettingstarted.mdx +++ b/src/Umbraco.Web.UI.Client/src/stories/getting-started.mdx @@ -2,16 +2,16 @@ import { Meta } from '@storybook/blocks'; <Meta title="Guides/Getting Started" /> - # Getting started This section contains a set of guide which will ease the learning of the Umbraco CMS (Backoffice). In this document you will get a overview of the articles — Enabling you to get started with the parts that makes sense for you. ## Terminology + There is a few words that covers certain concepts, which is good to learn to easilier decode the purpose of code. -- **Resource** A API enabling communication with a server. +- **Resource** A API enabling communication with a server. [Go to Resource Guide](/?path=/story/guides-resource--page) - **Store** A API representing data, generally coming from the server. Most stores would talk with one or more resources. [Go to Store Guide](?path=/docs/guides-store--docs) - **State** A reactive container holding data, when data is changed all its Observables will be notified. @@ -28,4 +28,4 @@ There is a few words that covers certain concepts, which is good to learn to eas - **Observer Controller** A Controller for handling the observe subscription for a Observable. - **Controller Host** The element that can host one or more controllers. -- **Umbraco Element** The UmbLitElement or UmbElemenMixin is a implementation of the Controller Host as an element. Using this as your base element provides a few methods that makes life easier. [Go to Umbraco Element Guide](?path=/docs/guides-umbraco-element--docs) \ No newline at end of file +- **Umbraco Element** The UmbLitElement or UmbElemenMixin is a implementation of the Controller Host as an element. Using this as your base element provides a few methods that makes life easier. [Go to Umbraco Element Guide](?path=/docs/guides-umbraco-element--docs) diff --git a/src/Umbraco.Web.UI.Client/src/stories/umbelement.mdx b/src/Umbraco.Web.UI.Client/src/stories/umb-element.mdx similarity index 97% rename from src/Umbraco.Web.UI.Client/src/stories/umbelement.mdx rename to src/Umbraco.Web.UI.Client/src/stories/umb-element.mdx index 025a2235d8..0b33b86c97 100644 --- a/src/Umbraco.Web.UI.Client/src/stories/umbelement.mdx +++ b/src/Umbraco.Web.UI.Client/src/stories/umb-element.mdx @@ -10,7 +10,7 @@ The Element implements the Controller Host and provides a few shortcut methods f The methods are (_note this can be out of date, we need to look into how we can ensure this Doc originates from code._) -```typescript +```ts observe<T>(source: Observable<T>, callback: (_value: T) => void, unique?: string): UmbObserverController<T> provideContext<R = unknown>(alias: string | UmbContextToken<R>, instance: R): UmbContextProviderController<R> @@ -20,7 +20,7 @@ consumeContext<R = unknown>(alias: string | UmbContextToken<R>, callback: UmbCon Use these for an smooth consumption, like this request for a Context API using a simple string context, where the callback value is unknown -```typescript +```ts this.consumeContext('requestThisContextAlias', (context) => { // Notice this is a subscription, as context might change or a new one appears. console.log("I've got the context", context); @@ -29,7 +29,7 @@ this.consumeContext('requestThisContextAlias', (context) => { Or use the UmbContextToken type to define the type of the context, like this -```typescript +```ts const contextAlias = new UmbContextToken<SomeType>('SomeTypeAlias', 'description of context for debugging purposes'); this.consumeContext(contextAlias, (context) => { diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index 73ddb9a903..500796ce4c 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -21,7 +21,7 @@ "baseUrl": ".", "paths": { "@umbraco-cms/css": ["libs/css/custom-properties.css"], - "@umbraco-cms/modal": ["src/core/modal"], + "@umbraco-cms/modal": ["libs/modal"], "@umbraco-cms/models": ["libs/models"], "@umbraco-cms/backend-api": ["libs/backend-api"], "@umbraco-cms/context-api": ["libs/context-api"], diff --git a/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs b/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs index 70be4bf8f8..d1edd086aa 100644 --- a/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs +++ b/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs @@ -57,6 +57,7 @@ export default { '@umbraco-cms/resources': './libs/resources/index.ts', '@umbraco-cms/repository': './libs/repository/index.ts', '@umbraco-cms/router': './src/core/router/index.ts', + '@umbraco-cms/modal': './libs/modal/index.ts', }, }, },