init extensions api
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Umbraco</title>
|
||||
<script type="module" src="/src/umb-app.ts"></script>
|
||||
<script type="module" src="/src/index.ts"></script>
|
||||
</head>
|
||||
|
||||
<body class="uui-font uui-text" style="margin: 0; padding: 0">
|
||||
|
||||
@@ -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<any> = [
|
||||
{
|
||||
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<UmbExtensionManifest<UmbManifestSectionMeta>> = [];
|
||||
|
||||
@state()
|
||||
private _activeSection: string = this._sections[0];
|
||||
private _allowedSection: Array<string> = [];
|
||||
|
||||
@state()
|
||||
private _availableSections: string[] = [];
|
||||
private _sections: Array<UmbExtensionManifest<UmbManifestSectionMeta>> = [];
|
||||
|
||||
@state()
|
||||
private _visibleSections: Array<string> = [];
|
||||
private _visibleSections: Array<UmbExtensionManifest<UmbManifestSectionMeta>> = [];
|
||||
|
||||
@state()
|
||||
private _extraSections: Array<string> = [];
|
||||
private _extraSections: Array<UmbExtensionManifest<UmbManifestSectionMeta>> = [];
|
||||
|
||||
@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<UmbManifestSectionMeta>) {
|
||||
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<string, any>): 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<UmbExtensionManifest<unknown>>) =>
|
||||
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`
|
||||
<uui-menu-item
|
||||
?active="${this._activeSection === section}"
|
||||
label="${section}"
|
||||
?active="${this._activeSection === section.alias}"
|
||||
label="${section.name}"
|
||||
@click-label="${this._handleLabelClick}"></uui-menu-item>
|
||||
`
|
||||
)}
|
||||
@@ -192,9 +205,9 @@ export class UmbBackofficeHeader extends LitElement {
|
||||
${this._visibleSections.map(
|
||||
(section) => html`
|
||||
<uui-tab
|
||||
?active="${this._activeSection === section}"
|
||||
label="${section}"
|
||||
@click="${this._handleTabClick}"></uui-tab>
|
||||
?active="${this._activeSection === section.alias}"
|
||||
label="${section.name}"
|
||||
@click="${(e: PointerEvent) => this._handleTabClick(e, section)}"></uui-tab>
|
||||
`
|
||||
)}
|
||||
${this._renderExtraSections()}
|
||||
|
||||
@@ -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`<div>Content Section</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-content-section': UmbContentSection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { map } from 'rxjs/operators';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
export type UmbExtensionType = 'startUp' | 'section' | 'tree' | 'propertyEditor';
|
||||
|
||||
export interface UmbExtensionManifest<Meta> {
|
||||
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<Array<UmbExtensionManifest<unknown>>> = new BehaviorSubject(<Array<UmbExtensionManifest<unknown>>>[]);
|
||||
public readonly extensions: Observable<Array<UmbExtensionManifest<unknown>>> = this._extensions.asObservable();
|
||||
|
||||
register (manifest: UmbExtensionManifest<unknown>) {
|
||||
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
|
||||
}
|
||||
1
src/Umbraco.Web.UI.Client/src/core/extension/index.ts
Normal file
1
src/Umbraco.Web.UI.Client/src/core/extension/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './extension.registry';
|
||||
@@ -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 {
|
||||
|
||||
72
src/Umbraco.Web.UI.Client/src/index.ts
Normal file
72
src/Umbraco.Web.UI.Client/src/index.ts
Normal file
@@ -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<unknown>) => extensionRegistry.register(manifest));
|
||||
}
|
||||
|
||||
const registerInternalManifests = async () => {
|
||||
// TODO: where do we get these from?
|
||||
const manifests: Array<UmbExtensionManifest<unknown>> = [
|
||||
{
|
||||
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<unknown>) => extensionRegistry.register(manifest));
|
||||
}
|
||||
|
||||
const setup = async () => {
|
||||
await registerExtensionManifestsFromServer();
|
||||
await registerInternalManifests();
|
||||
// TODO: implement loading of "startUp" extensions
|
||||
await import('./umb-app');
|
||||
}
|
||||
|
||||
worker.start();
|
||||
setup();
|
||||
21
src/Umbraco.Web.UI.Client/src/media/media-section.element.ts
Normal file
21
src/Umbraco.Web.UI.Client/src/media/media-section.element.ts
Normal file
@@ -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`<div>Media Section</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-media-section': UmbMediaSection;
|
||||
}
|
||||
}
|
||||
@@ -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'])
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -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<UmbRoute> = [
|
||||
@@ -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<void> {
|
||||
const outlet = this.shadowRoot?.getElementById('outlet');
|
||||
if (!outlet) return;
|
||||
@@ -80,7 +83,10 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) {
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<div id="outlet"></div>`;
|
||||
return html`
|
||||
<umb-backoffice></umb-backoffice>
|
||||
<div id="outlet"></div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user