init extensions api

This commit is contained in:
Mads Rasmussen
2022-05-20 17:45:09 +02:00
parent d290fdb629
commit 8d05b1bdd2
10 changed files with 258 additions and 51 deletions

View File

@@ -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">

View File

@@ -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()}

View 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-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;
}
}

View File

@@ -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
}

View File

@@ -0,0 +1 @@
export * from './extension.registry';

View File

@@ -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 {

View 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();

View 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;
}
}

View File

@@ -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'])
);
}),

View File

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