diff --git a/src/Umbraco.Web.UI.Client/.vscode/extensions.json b/src/Umbraco.Web.UI.Client/.vscode/extensions.json index 5c7db24a59..3e18e13dc4 100644 --- a/src/Umbraco.Web.UI.Client/.vscode/extensions.json +++ b/src/Umbraco.Web.UI.Client/.vscode/extensions.json @@ -7,6 +7,7 @@ "runem.lit-plugin", "esbenp.prettier-vscode", "hbenl.vscode-test-explorer", - "vunguyentuan.vscode-css-variables" + "vunguyentuan.vscode-css-variables", + "unifiedjs.vscode-mdx" ] } diff --git a/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-request.event.ts b/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-request.event.ts index 9d8fa86dec..ab96c5bd51 100644 --- a/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-request.event.ts +++ b/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-request.event.ts @@ -1,6 +1,7 @@ import { UmbContextToken } from '../context-token'; export const umbContextRequestEventType = 'umb:context-request'; +export const umbDebugContextEventType = 'umb:debug-contexts'; export type UmbContextCallback = (instance: T) => void; @@ -31,3 +32,10 @@ export class UmbContextRequestEventImplementation extends Event imp export const isUmbContextRequestEvent = (event: Event): event is UmbContextRequestEventImplementation => { return event.type === umbContextRequestEventType; }; + + +export class UmbContextDebugRequest extends Event { + public constructor(public readonly callback:any) { + super(umbDebugContextEventType, { bubbles: true, composed: true, cancelable: false }); + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.ts b/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.ts index 458ddcb8a0..c9ce3ab9c3 100644 --- a/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.ts +++ b/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.ts @@ -1,4 +1,4 @@ -import { umbContextRequestEventType, isUmbContextRequestEvent } from '../consume/context-request.event'; +import { umbContextRequestEventType, isUmbContextRequestEvent, umbDebugContextEventType } from '../consume/context-request.event'; import { UmbContextToken } from '../context-token'; import { UmbContextProvideEventImplementation } from './context-provide.event'; @@ -40,6 +40,9 @@ export class UmbContextProvider { public hostConnected() { this.host.addEventListener(umbContextRequestEventType, this._handleContextRequest); this.host.dispatchEvent(new UmbContextProvideEventImplementation(this._contextAlias)); + + // Listen to our debug event 'umb:debug-contexts' + this.host.addEventListener(umbDebugContextEventType, this._handleDebugContextRequest); } /** @@ -63,6 +66,20 @@ export class UmbContextProvider { event.callback(this.#instance); }; + private _handleDebugContextRequest = (event: any) => { + // If the event doesn't have an instances property, create it. + if(!event.instances){ + event.instances = new Map(); + } + + // If the event doesn't have an instance for this context, add it. + // Nearest to the DOM element of will be added first + // as contexts can change/override deeper in the DOM + if(!event.instances.has(this._contextAlias)){ + event.instances.set(this._contextAlias, this.#instance); + } + }; + destroy(): void { // I want to make sure to call this, but for now it was too overwhelming to require the destroy method on context instances. (this.#instance as any).destroy?.(); diff --git a/src/Umbraco.Web.UI.Client/src/app.ts b/src/Umbraco.Web.UI.Client/src/app.ts index 22ab81b634..18d11ccbf2 100644 --- a/src/Umbraco.Web.UI.Client/src/app.ts +++ b/src/Umbraco.Web.UI.Client/src/app.ts @@ -19,6 +19,7 @@ import { UmbLitElement } from '@umbraco-cms/element'; import { tryExecuteAndNotify } from '@umbraco-cms/resources'; import { OpenAPI, RuntimeLevelModel, ServerResource } from '@umbraco-cms/backend-api'; import { UmbIconStore } from '@umbraco-cms/store'; +import { UmbContextDebugRequest, umbDebugContextEventType } from '@umbraco-cms/context-api'; @customElement('umb-app') export class UmbApp extends UmbLitElement { @@ -83,6 +84,16 @@ export class UmbApp extends UmbLitElement { await this._setInitStatus(); await this._registerExtensionManifestsFromServer(); this._redirect(); + + // Listen for the debug event from the component + this.addEventListener(umbDebugContextEventType, (event: any) => { + // Once we got to the outter most component + // we can send the event containing all the contexts + // we have collected whilst coming up through the DOM + // and pass it back down to the callback in + // the component that originally fired the event + event.callback(event.instances); + }); } private async _setup() { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/modal-views/fields-viewer.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/modal-views/fields-viewer.element.ts index 6e81567c79..e55b6b4b22 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/modal-views/fields-viewer.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/modal-views/fields-viewer.element.ts @@ -16,11 +16,7 @@ export class UmbModalLayoutFieldsViewerElement extends UmbModalLayoutElement

${this._publishedStatusText}

div { + padding: 10px; + } + + h4 { + margin: 0; + } + `, + ]; + + @property({ reflect: true, type: Boolean }) + enabled = false; + + @property({ reflect: true, type: Boolean }) + dialog = false; + + @property() + contexts = new Map(); + + @state() + private _debugPaneOpen = false; + + private _modalService?: UmbModalService; + + constructor() { + super(); + this.consumeContext(UMB_MODAL_SERVICE_CONTEXT_TOKEN, (modalService) => { + this._modalService = modalService; + }); + } + + connectedCallback(): void { + super.connectedCallback(); + + // Dispatch it + this.dispatchEvent( + new UmbContextDebugRequest((contexts: Map) => { + // The Contexts are collected + // When travelling up through the DOM from this element + // to the root of which then uses the callback prop + // of the this event tha has been raised to assign the contexts + // back to this property of the WebComponent + this.contexts = contexts; + }) + ); + } + + render() { + if (this.enabled) { + return this.dialog ? this._renderDialog() : this._renderPanel(); + } else { + return nothing; + } + } + + private _toggleDebugPane() { + this._debugPaneOpen = !this._debugPaneOpen; + } + + private async _openDialog() { + // Open a modal that uses the HTML component called 'umb-debug-modal-layout' + await import('./debug.modal.element.js'); + this._modalService?.open('umb-debug-modal-layout', { + size: 'small', + type: 'sidebar', + data: { + content: this._renderContextAliases(), + }, + }); + } + + private _renderDialog() { + return html`
+ + Debug + +
`; + } + + private _renderPanel() { + return html`
+ + + Debug + + +
+
+
    + ${this._renderContextAliases()} +
+
+
+
`; + } + + private _renderContextAliases() { + const contextsTemplates: TemplateResult[] = []; + + for (const [alias, instance] of this.contexts) { + contextsTemplates.push( + html`
  • + Context: ${alias} + (${typeof instance}) +
      + ${this._renderInstance(instance)} +
    +
  • ` + ); + } + + return contextsTemplates; + } + + private _renderInstance(instance: any) { + const instanceTemplates: TemplateResult[] = []; + + // TODO: WB - Maybe make this a switch statement? + if (typeof instance === 'function') { + return instanceTemplates.push(html`
  • Callable Function
  • `); + } else if (typeof instance === 'object') { + const methodNames = this.getClassMethodNames(instance); + if (methodNames.length) { + instanceTemplates.push( + html` +
  • + Methods +
      + ${methodNames.map((methodName) => html`
    • ${methodName}
    • `)} +
    +
  • + ` + ); + } + + const props: TemplateResult[] = []; + + for (const key in instance) { + if (key.startsWith('_')) { + continue; + } + + const value = instance[key]; + if (typeof value === 'string') { + props.push(html`
  • ${key} = ${value}
  • `); + } else { + props.push(html`
  • ${key} (${typeof value})
  • `); + } + } + + instanceTemplates.push(html` +
  • + Properties +
      + ${props} +
    +
  • + `); + } else { + instanceTemplates.push(html`
  • Context is a primitive with value: ${instance}
  • `); + } + + return instanceTemplates; + } + + private getClassMethodNames(klass: any) { + const isGetter = (x: any, name: string): boolean => !!(Object.getOwnPropertyDescriptor(x, name) || {}).get; + const isFunction = (x: any, name: string): boolean => typeof x[name] === 'function'; + const deepFunctions = (x: any): any => + x !== Object.prototype && + Object.getOwnPropertyNames(x) + .filter((name) => isGetter(x, name) || isFunction(x, name)) + .concat(deepFunctions(Object.getPrototypeOf(x)) || []); + const distinctDeepFunctions = (klass: any) => Array.from(new Set(deepFunctions(klass))); + + const allMethods = + typeof klass.prototype === 'undefined' + ? distinctDeepFunctions(klass) + : Object.getOwnPropertyNames(klass.prototype); + return allMethods.filter((name: any) => name !== 'constructor' && !name.startsWith('_')); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-debug': UmbDebug; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/debug.modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/debug.modal.element.ts new file mode 100644 index 0000000000..d9d370aaf0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/debug.modal.element.ts @@ -0,0 +1,79 @@ +import { css, html, TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { UmbModalLayoutElement } from '@umbraco-cms/modal'; + +export interface UmbDebugModalData { + content: TemplateResult | string; +} + +@customElement('umb-debug-modal-layout') +export default class UmbDebugModalLayout extends UmbModalLayoutElement { + 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?.close(); + } + + render() { + return html` + + Debug: Contexts + ${this.data?.content} + Close + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-debug-modal-layout': UmbDebugModalLayout; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/stories/debug.stories.mdx b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/stories/debug.stories.mdx new file mode 100644 index 0000000000..3497502ad5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/stories/debug.stories.mdx @@ -0,0 +1,44 @@ +import { Canvas, Meta, Story } from '@storybook/addon-docs'; +import DebugDialogImage from './umb-debug-dialog.jpg'; +import DebugImage from './umb-debug.jpg'; + + + + + + +# Debugging + +## Debugging Contexts + +The component `` allows you to discover the available contexts from the current DOM element, that you are able to consume and use. + +For example it will help you as a package developer or implementor to know you are able to consume the `DigalogService` and quickly see what properties and methods are available to use. + +This can help with the developer experience to quickly see what is available to use and how to use it. + +### Usage +The `` component can be used in two different ways, either as a button or as a dialog. By default it is rendered as a button and the debug information about available contexts is dissplayed inline to where the element is placed. + + +```typescript +// This will add a Debug button to the UI and once clicked the information about avilable contextes will slide down + +``` + +#### Dialog +This example uses an additional property/attribute `dialog` which adds a smaller badge to the UI as opposed to a button and will open the information in a small dialog/modal from the right hand side, this may be more useful to use when space is limited in the UI to add a button and pane of information directly to where the element is placed. + + +```typescript +// This will open the debug information in a small dialog/modal from the right hand side + +``` + +#### Disable +You may wish to temporarily hide or disable the debug information but return to it later on in the development process. + +```typescript +// To hide or remove the button ensure you remove the enabled attribute or set the enabled property to false + +``` diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/stories/umb-debug-dialog.jpg b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/stories/umb-debug-dialog.jpg new file mode 100644 index 0000000000..449ae6dc14 Binary files /dev/null and b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/stories/umb-debug-dialog.jpg differ diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/stories/umb-debug.jpg b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/stories/umb-debug.jpg new file mode 100644 index 0000000000..a33e519014 Binary files /dev/null and b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/debug/stories/umb-debug.jpg differ 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 3369fe9172..fee735ecf4 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 @@ -33,3 +33,6 @@ import './input-checkbox-list/input-checkbox-list.element'; import './input-multi-url-picker/input-multi-url-picker.element'; import './empty-state/empty-state.element'; + +import './debug/debug.element'; +