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..a8fa7c2779 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/extension/extension.registry.ts @@ -0,0 +1,36 @@ +import { BehaviorSubject, Observable } from 'rxjs'; + +// TODO: how do we want to type extensions? +export type UmbExtensionType = 'startUp' | 'section' | 'propertyEditor'; + +export interface UmbExtensionManifest { + type: UmbExtensionType; + alias: string; + name: string; + js?: string; + elementName?: string; + meta: Meta; +} + +export interface UmbManifestSectionMeta { + 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/index.ts b/src/Umbraco.Web.UI.Client/src/index.ts new file mode 100644 index 0000000000..b17bcc2127 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/index.ts @@ -0,0 +1,70 @@ +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', + 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', + meta: {} + } + ]; + 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..9504408bb7 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers.ts @@ -3,6 +3,23 @@ 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' + } + ] + }) + ); + }), + rest.get('/umbraco/backoffice/init', (_req, res, ctx) => { return res( // Respond with a 200 status code @@ -67,7 +84,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..3bfa7d046b 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,9 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) { } render() { - return html`
`; + return html` +
+ `; } }