From 8d05b1bdd2e49dbb7bd8ce7753814b5bf29d89fc Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 20 May 2022 17:45:09 +0200 Subject: [PATCH 1/4] init extensions api --- src/Umbraco.Web.UI.Client/index.html | 2 +- .../umb-backoffice-header.element.ts | 103 ++++++++++-------- .../src/content/content-section.element.ts | 21 ++++ .../src/core/extension/extension.registry.ts | 41 +++++++ .../src/core/extension/index.ts | 1 + .../src/core/router/index.ts | 2 +- src/Umbraco.Web.UI.Client/src/index.ts | 72 ++++++++++++ .../src/media/media-section.element.ts | 21 ++++ .../src/mocks/handlers.ts | 34 +++++- src/Umbraco.Web.UI.Client/src/umb-app.ts | 12 +- 10 files changed, 258 insertions(+), 51 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/content/content-section.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/core/extension/extension.registry.ts create mode 100644 src/Umbraco.Web.UI.Client/src/core/extension/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/media/media-section.element.ts diff --git a/src/Umbraco.Web.UI.Client/index.html b/src/Umbraco.Web.UI.Client/index.html index 6882cfa6ed..9e88ebf14d 100644 --- a/src/Umbraco.Web.UI.Client/index.html +++ b/src/Umbraco.Web.UI.Client/index.html @@ -6,7 +6,7 @@ Umbraco - + diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/umb-backoffice-header.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/umb-backoffice-header.element.ts index b53e4b5eef..2cbc24371f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/umb-backoffice-header.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/umb-backoffice-header.element.ts @@ -1,11 +1,16 @@ +import { Subscription } from 'rxjs'; +import { map } from 'rxjs/operators'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, CSSResultGroup, html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { when } from 'lit/directives/when.js'; import { getUserSections } from '../api/fetcher'; +import { UmbContextInjectMixin } from '../core/context'; +import { UmbExtensionManifest, UmbExtensionRegistry, UmbManifestSectionMeta } from '../core/extension'; +import { UmbRouter } from '../core/router'; @customElement('umb-backoffice-header') -export class UmbBackofficeHeader extends LitElement { +export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) { static styles: CSSResultGroup = [ UUITextStyles, css` @@ -74,54 +79,34 @@ export class UmbBackofficeHeader extends LitElement { @state() private _open = false; - // TODO: these should come from all registered sections @state() - private _sections: Array = [ - { - type: 'section', - alias: 'Umb.Section.Content', - name: 'Content', - }, - { - type: 'section', - alias: 'Umb.Section.Media', - name: 'Media', - }, - { - type: 'section', - alias: 'Umb.Section.Members', - name: 'Members', - }, - { - type: 'section', - alias: 'Umb.Section.Settings', - name: 'Settings', - }, - { - type: 'section', - alias: 'Umb.Section.Packages', - name: 'Packages', - }, - ]; + private _availableSections: Array> = []; @state() - private _activeSection: string = this._sections[0]; + private _allowedSection: Array = []; @state() - private _availableSections: string[] = []; + private _sections: Array> = []; @state() - private _visibleSections: Array = []; + private _visibleSections: Array> = []; @state() - private _extraSections: Array = []; + private _extraSections: Array> = []; + + @state() + private _activeSection = ''; + + private _router?: UmbRouter; + private _extensionRegistry?: UmbExtensionRegistry; + private _subscription?: Subscription; private _handleMore(e: MouseEvent) { e.stopPropagation(); this._open = !this._open; } - private _handleTabClick(e: MouseEvent) { + private _handleTabClick(e: PointerEvent, section: UmbExtensionManifest) { const tab = e.currentTarget as any; // TODO: we need to be able to prevent the tab from setting the active state @@ -129,7 +114,9 @@ export class UmbBackofficeHeader extends LitElement { return; } - this._activeSection = tab.label; + // TODO: this could maybe be handled by an anchor tag + this._router?.push(`/section/${section.name}`); + this._activeSection = section.alias; } private _handleLabelClick(e: MouseEvent) { @@ -146,12 +133,38 @@ export class UmbBackofficeHeader extends LitElement { super.connectedCallback(); const { data } = await getUserSections({}); + this._allowedSection = data.sections; - this._availableSections = data.sections; - this._visibleSections = this._sections - .filter((section) => this._availableSections.includes(section.alias)) - .map((section) => section.name); - this._activeSection = this._visibleSections?.[0]; + this.requestContext('umbRouter'); + this.requestContext('umbExtensionRegistry'); + } + + contextInjected(contexts: Map): void { + if (contexts.has('umbExtensionRegistry')) { + this._extensionRegistry = contexts.get('umbExtensionRegistry'); + this._useSections(); + } + + if (contexts.has('umbRouter')) { + this._router = contexts.get('umbRouter'); + } + } + + private _useSections() { + this._subscription?.unsubscribe(); + + this._subscription = this._extensionRegistry?.extensions + .pipe( + map((extensions: Array>) => + extensions.filter(extension => extension.type === 'section') + )) + .subscribe((sectionExtensions: any) => { + this._availableSections = [...sectionExtensions]; + this._sections = this._availableSections.filter((section) => this._allowedSection.includes(section.alias)); + // TODO: implement resize observer + this._visibleSections = this._sections; + this._activeSection = this._visibleSections?.[0].alias; + }); } private _renderExtraSections() { @@ -168,8 +181,8 @@ export class UmbBackofficeHeader extends LitElement { ${this._extraSections.map( (section) => html` ` )} @@ -192,9 +205,9 @@ export class UmbBackofficeHeader extends LitElement { ${this._visibleSections.map( (section) => html` + ?active="${this._activeSection === section.alias}" + label="${section.name}" + @click="${(e: PointerEvent) => this._handleTabClick(e, section)}"> ` )} ${this._renderExtraSections()} diff --git a/src/Umbraco.Web.UI.Client/src/content/content-section.element.ts b/src/Umbraco.Web.UI.Client/src/content/content-section.element.ts new file mode 100644 index 0000000000..6fc2ed6b30 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/content/content-section.element.ts @@ -0,0 +1,21 @@ +import { defineElement } from '@umbraco-ui/uui-base/lib/registration'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { css, html, LitElement } from 'lit'; + +@defineElement('umb-content-section') +export class UmbContentSection extends LitElement { + static styles = [ + UUITextStyles, + css``, + ]; + + render() { + return html`
Content Section
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-content-section': UmbContentSection; + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/extension/extension.registry.ts b/src/Umbraco.Web.UI.Client/src/core/extension/extension.registry.ts new file mode 100644 index 0000000000..1c67ca1af7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/extension/extension.registry.ts @@ -0,0 +1,41 @@ +import { map } from 'rxjs/operators'; +import { BehaviorSubject, Observable } from 'rxjs'; + +export type UmbExtensionType = 'startUp' | 'section' | 'tree' | 'propertyEditor'; + +export interface UmbExtensionManifest { + type: UmbExtensionType; + alias: string; + name: string; + js?: string; + elementName?: string; + meta: Meta; +} + +export interface UmbManifestSectionMeta { + weight: number; +} + +export interface UmbManifestTreeMeta { + section: string; + weight: number; +} + +export class UmbExtensionRegistry { + private _extensions: BehaviorSubject>> = new BehaviorSubject(>>[]); + public readonly extensions: Observable>> = this._extensions.asObservable(); + + register (manifest: UmbExtensionManifest) { + const extensions = this._extensions.getValue(); + const extension = extensions.find(extension => extension.alias === manifest.alias); + + if (extension) { + console.error(`Extension with alias ${manifest.alias} is already registered`); + return; + } + + this._extensions.next([...extensions, manifest]); + } + + // TODO: implement unregister of extension +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/extension/index.ts b/src/Umbraco.Web.UI.Client/src/core/extension/index.ts new file mode 100644 index 0000000000..3dcab3f82c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/extension/index.ts @@ -0,0 +1 @@ +export * from './extension.registry'; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/router/index.ts b/src/Umbraco.Web.UI.Client/src/core/router/index.ts index e0ef976391..de08a293a0 100644 --- a/src/Umbraco.Web.UI.Client/src/core/router/index.ts +++ b/src/Umbraco.Web.UI.Client/src/core/router/index.ts @@ -115,7 +115,7 @@ export class UmbRouter { if (!canEnter) return; window.history.pushState(null, '', pathname); - this._render(); + //this._render(); } private _resolve(pathname: string): UmbRouteLocation | null { diff --git a/src/Umbraco.Web.UI.Client/src/index.ts b/src/Umbraco.Web.UI.Client/src/index.ts new file mode 100644 index 0000000000..43dacbdc0b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/index.ts @@ -0,0 +1,72 @@ +import { worker } from './mocks/browser'; +import { UmbExtensionRegistry, UmbExtensionManifest } from './core/extension'; + +const extensionRegistry = new UmbExtensionRegistry(); + +export interface Umbraco { + extensionRegistry: UmbExtensionRegistry; +} + +declare global { + interface Window { + Umbraco: Umbraco; + } +} + +window.Umbraco = { + extensionRegistry +}; + +const registerExtensionManifestsFromServer = async () => { + // TODO: add schema and use fetcher + const res = await fetch('/umbraco/backoffice/manifests'); + const { manifests } = await res.json(); + manifests.forEach((manifest: UmbExtensionManifest) => extensionRegistry.register(manifest)); +} + +const registerInternalManifests = async () => { + // TODO: where do we get these from? + const manifests: Array> = [ + { + type: 'section', + alias: 'Umb.Section.Content', + name: 'Content', + elementName: 'umb-content-section', + }, + { + type: 'section', + alias: 'Umb.Section.Media', + name: 'Media', + elementName: 'umb-media-section', + }, + { + type: 'section', + alias: 'Umb.Section.Members', + name: 'Members', + elementName: 'umb-members-section', + }, + { + type: 'section', + alias: 'Umb.Section.Settings', + name: 'Settings', + elementName: 'umb-settings-section', + }, + { + type: 'section', + alias: 'Umb.Section.Packages', + name: 'Packages', + elementName: 'umb-packages-section', + }, + ]; + manifests.forEach((manifest: UmbExtensionManifest) => extensionRegistry.register(manifest)); +} + +const setup = async () => { + await registerExtensionManifestsFromServer(); + await registerInternalManifests(); + // TODO: implement loading of "startUp" extensions + await import('./umb-app'); +} + +worker.start(); +setup(); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/media/media-section.element.ts b/src/Umbraco.Web.UI.Client/src/media/media-section.element.ts new file mode 100644 index 0000000000..42529c08f9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/media/media-section.element.ts @@ -0,0 +1,21 @@ +import { defineElement } from '@umbraco-ui/uui-base/lib/registration'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { css, html, LitElement } from 'lit'; + +@defineElement('umb-media-section') +export class UmbMediaSection extends LitElement { + static styles = [ + UUITextStyles, + css``, + ]; + + render() { + return html`
Media Section
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-media-section': UmbMediaSection; + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers.ts index 8010c62749..da4c8b30a3 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers.ts @@ -3,6 +3,38 @@ import { rest } from 'msw'; import { components } from '../../schemas/generated-schema'; export const handlers = [ + rest.get('/umbraco/backoffice/manifests', (_req, res, ctx) => { + return res( + // Respond with a 200 status code + ctx.status(200), + ctx.json({ + manifests: [ + { + type: 'section', + alias: 'My.Section.Custom', + name: 'Custom', + elementName: 'umb-custom-section' + }, + { + type: 'propertyEditor', + alias: 'My.PropertyEditor.Custom', + name: 'Custom', + elementName: 'umb-custom-property-editor' + }, + { + type: 'tree', + alias: 'My.Tree.Custom', + name: 'Custom', + elementName: 'umb-custom-tree', + meta: { + section: 'My.Section.Custom', + } + } + ] + }) + ); + }), + rest.get('/umbraco/backoffice/init', (_req, res, ctx) => { return res( // Respond with a 200 status code @@ -67,7 +99,7 @@ export const handlers = [ return res( ctx.status(200), ctx.json({ - sections: ['Umb.Section.Content', 'Umb.Section.Media', 'Umb.Section.Settings'], + sections: ['Umb.Section.Content', 'Umb.Section.Media', 'Umb.Section.Settings', 'My.Section.Custom'], } as components['schemas']['AllowedSectionsResponse']) ); }), diff --git a/src/Umbraco.Web.UI.Client/src/umb-app.ts b/src/Umbraco.Web.UI.Client/src/umb-app.ts index 7fc4053d11..ecf779e4d1 100644 --- a/src/Umbraco.Web.UI.Client/src/umb-app.ts +++ b/src/Umbraco.Web.UI.Client/src/umb-app.ts @@ -10,7 +10,6 @@ import { customElement, state } from 'lit/decorators.js'; import { getInitStatus } from './api/fetcher'; import { UmbRoute, UmbRouter } from './core/router'; -import { worker } from './mocks/browser'; import { UmbContextProvideMixin } from './core/context'; const routes: Array = [ @@ -47,10 +46,14 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) { constructor() { super(); - worker.start(); this._authorized = sessionStorage.getItem('is-authenticated') === 'true'; } + connectedCallback(): void { + super.connectedCallback(); + this.provide('umbExtensionRegistry', window.Umbraco.extensionRegistry); + } + protected async firstUpdated(): Promise { const outlet = this.shadowRoot?.getElementById('outlet'); if (!outlet) return; @@ -80,7 +83,10 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) { } render() { - return html`
`; + return html` + +
+ `; } } From dda9ba113e5c10a3ec782955488a84f2fc7e1cf6 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 23 May 2022 11:09:54 +0200 Subject: [PATCH 2/4] remove unused --- .../src/core/extension/extension.registry.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/core/extension/extension.registry.ts b/src/Umbraco.Web.UI.Client/src/core/extension/extension.registry.ts index 1c67ca1af7..a8fa7c2779 100644 --- a/src/Umbraco.Web.UI.Client/src/core/extension/extension.registry.ts +++ b/src/Umbraco.Web.UI.Client/src/core/extension/extension.registry.ts @@ -1,7 +1,7 @@ -import { map } from 'rxjs/operators'; import { BehaviorSubject, Observable } from 'rxjs'; -export type UmbExtensionType = 'startUp' | 'section' | 'tree' | 'propertyEditor'; +// TODO: how do we want to type extensions? +export type UmbExtensionType = 'startUp' | 'section' | 'propertyEditor'; export interface UmbExtensionManifest { type: UmbExtensionType; @@ -16,11 +16,6 @@ export interface UmbManifestSectionMeta { weight: number; } -export interface UmbManifestTreeMeta { - section: string; - weight: number; -} - export class UmbExtensionRegistry { private _extensions: BehaviorSubject>> = new BehaviorSubject(>>[]); public readonly extensions: Observable>> = this._extensions.asObservable(); From 14f5c289dacb25cad56366f214a0a8e3dd3f0eee Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 23 May 2022 11:16:21 +0200 Subject: [PATCH 3/4] rollback router tests --- src/Umbraco.Web.UI.Client/src/core/router/index.ts | 2 +- src/Umbraco.Web.UI.Client/src/index.ts | 12 +++++------- src/Umbraco.Web.UI.Client/src/umb-app.ts | 1 - 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/core/router/index.ts b/src/Umbraco.Web.UI.Client/src/core/router/index.ts index de08a293a0..e0ef976391 100644 --- a/src/Umbraco.Web.UI.Client/src/core/router/index.ts +++ b/src/Umbraco.Web.UI.Client/src/core/router/index.ts @@ -115,7 +115,7 @@ export class UmbRouter { if (!canEnter) return; window.history.pushState(null, '', pathname); - //this._render(); + this._render(); } private _resolve(pathname: string): UmbRouteLocation | null { diff --git a/src/Umbraco.Web.UI.Client/src/index.ts b/src/Umbraco.Web.UI.Client/src/index.ts index 43dacbdc0b..b17bcc2127 100644 --- a/src/Umbraco.Web.UI.Client/src/index.ts +++ b/src/Umbraco.Web.UI.Client/src/index.ts @@ -32,31 +32,29 @@ const registerInternalManifests = async () => { alias: 'Umb.Section.Content', name: 'Content', elementName: 'umb-content-section', + meta: {} }, { type: 'section', alias: 'Umb.Section.Media', name: 'Media', elementName: 'umb-media-section', + meta: {} }, { type: 'section', alias: 'Umb.Section.Members', name: 'Members', elementName: 'umb-members-section', + meta: {} }, { type: 'section', alias: 'Umb.Section.Settings', name: 'Settings', elementName: 'umb-settings-section', - }, - { - type: 'section', - alias: 'Umb.Section.Packages', - name: 'Packages', - elementName: 'umb-packages-section', - }, + meta: {} + } ]; manifests.forEach((manifest: UmbExtensionManifest) => extensionRegistry.register(manifest)); } diff --git a/src/Umbraco.Web.UI.Client/src/umb-app.ts b/src/Umbraco.Web.UI.Client/src/umb-app.ts index ecf779e4d1..3bfa7d046b 100644 --- a/src/Umbraco.Web.UI.Client/src/umb-app.ts +++ b/src/Umbraco.Web.UI.Client/src/umb-app.ts @@ -84,7 +84,6 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) { render() { return html` -
`; } From 8f5ecdf9e396c59f0eac95604f6bd05189dfb40c Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 23 May 2022 11:18:46 +0200 Subject: [PATCH 4/4] remove unused tests --- src/Umbraco.Web.UI.Client/src/mocks/handlers.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers.ts index da4c8b30a3..9504408bb7 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers.ts @@ -14,21 +14,6 @@ export const handlers = [ alias: 'My.Section.Custom', name: 'Custom', elementName: 'umb-custom-section' - }, - { - type: 'propertyEditor', - alias: 'My.PropertyEditor.Custom', - name: 'Custom', - elementName: 'umb-custom-property-editor' - }, - { - type: 'tree', - alias: 'My.Tree.Custom', - name: 'Custom', - elementName: 'umb-custom-tree', - meta: { - section: 'My.Section.Custom', - } } ] })