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`
+
+ `;
}
}