diff --git a/src/Umbraco.Web.UI.Client/src/app.ts b/src/Umbraco.Web.UI.Client/src/app.ts index c751824654..d38597414c 100644 --- a/src/Umbraco.Web.UI.Client/src/app.ts +++ b/src/Umbraco.Web.UI.Client/src/app.ts @@ -1,31 +1,36 @@ import '@umbraco-ui/uui'; import '@umbraco-ui/uui-css/dist/uui-css.css'; + +// TODO: lazy load these +import './installer/installer.element'; import './auth/login/login.element'; import './auth/auth-layout.element'; import './backoffice/backoffice.element'; -import './installer/installer.element'; + +import { UmbSectionContext } from './section.context'; import { css, html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { getInitStatus } from './api/fetcher'; -import { isUmbRouterBeforeEnterEvent, UmbRoute, UmbRouter, UmbRouterBeforeEnterEvent, umbRouterBeforeEnterEventType } from './core/router'; +import { isUmbRouterBeforeEnterEvent, UmbRoute, UmbRouteLocation, UmbRouter, UmbRouterBeforeEnterEvent, umbRouterBeforeEnterEventType } from './core/router'; import { UmbContextProvideMixin } from './core/context'; +import { Subscription } from 'rxjs'; const routes: Array = [ { path: '/login', - elementName: 'umb-login', + alias: 'login', meta: { requiresAuth: false }, }, { path: '/install', - elementName: 'umb-installer', + alias: 'install', meta: { requiresAuth: false }, }, { path: '/section/:section', - elementName: 'umb-backoffice', + alias: 'app', meta: { requiresAuth: true }, }, ]; @@ -42,7 +47,11 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) { } `; - _router?: UmbRouter; + private _isInstalled = false; + + private _view?: HTMLElement; + private _router?: UmbRouter; + private _locationSubscription?: Subscription; constructor() { super(); @@ -51,7 +60,10 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) { connectedCallback(): void { super.connectedCallback(); + const { extensionRegistry } = window.Umbraco; + this.provide('umbExtensionRegistry', window.Umbraco.extensionRegistry); + this.provide('umbSectionContext', new UmbSectionContext(extensionRegistry)); } private _onBeforeEnter = (event: Event) => { @@ -71,10 +83,7 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) { } protected async firstUpdated(): Promise { - const outlet = this.shadowRoot?.getElementById('outlet'); - if (!outlet) return; - - this._router = new UmbRouter(this, outlet); + this._router = new UmbRouter(this); this._router.setRoutes(routes); // TODO: find a solution for magic strings @@ -83,27 +92,59 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) { try { const { data } = await getInitStatus({}); - if (!data.installed) { + this._isInstalled = data.installed; + + if (!this._isInstalled) { this._router.push('/install'); return; } - if (!this._isAuthorized()) { + if (!this._isAuthorized() || window.location.pathname === '/install') { this._router.push('/login'); } else { - const next = window.location.pathname === '/' ? '/section/content' : window.location.pathname; + const next = window.location.pathname === '/' ? '/section/Content' : window.location.pathname; this._router.push(next); } + this._useLocation(); + } catch (error) { console.log(error); } } + private _useLocation () { + this._locationSubscription?.unsubscribe(); + + this._locationSubscription = this._router?.location + .subscribe((location: UmbRouteLocation) => { + if (location.route.alias === 'login') { + this._renderView('umb-login'); + return; + } + + if (location.route.alias === 'install') { + this._renderView('umb-installer'); + return; + } + + this._renderView('umb-backoffice'); + }); + } + + _renderView (view: string) { + if (this._view?.tagName === view.toUpperCase()) return; + this._view = document.createElement(view); + this.requestUpdate(); + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + this._locationSubscription?.unsubscribe(); + } + render() { - return html` -
- `; + return html`${this._view}`; } } diff --git a/src/Umbraco.Web.UI.Client/src/auth/login/login.element.ts b/src/Umbraco.Web.UI.Client/src/auth/login/login.element.ts index 115e9c718c..8f9a9801d8 100644 --- a/src/Umbraco.Web.UI.Client/src/auth/login/login.element.ts +++ b/src/Umbraco.Web.UI.Client/src/auth/login/login.element.ts @@ -71,7 +71,7 @@ export class UmbLogin extends UmbContextInjectMixin(LitElement) { await postUserLogin({ username, password, persist }); this._loggingIn = false; // TODO: how do we know where to go? - this._router?.push('/section/content'); + this._router?.push('/section/Content'); } catch (error) { console.log(error); this._loggingIn = false; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-header.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-header.element.ts index 43329b3cae..8ac71d3e6b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-header.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-header.element.ts @@ -1,13 +1,14 @@ 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'; +import { UmbExtensionManifest, UmbManifestSectionMeta } from '../core/extension'; +import { UmbRouteLocation, UmbRouter } from '../core/router'; +import { UmbSectionContext } from '../section.context'; // TODO: umb or not umb in file name? @@ -81,9 +82,6 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) { @state() private _open = false; - @state() - private _availableSections: Array> = []; - @state() private _allowedSection: Array = []; @@ -97,11 +95,14 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) { private _extraSections: Array> = []; @state() - private _activeSection = ''; + private _currentSectionAlias = ''; private _router?: UmbRouter; - private _extensionRegistry?: UmbExtensionRegistry; - private _subscription?: Subscription; + private _sectionContext?: UmbSectionContext; + private _sectionSubscription?: Subscription; + private _currentSectionSubscription?: Subscription; + private _locationSubscription?: Subscription; + private _location? : UmbRouteLocation; private _handleMore(e: MouseEvent) { e.stopPropagation(); @@ -118,12 +119,14 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) { // TODO: this could maybe be handled by an anchor tag this._router?.push(`/section/${section.name}`); - this._activeSection = section.alias; + this._sectionContext?.setCurrent(section.alias); } private _handleLabelClick(e: PointerEvent) { const label = (e.target as any).label; - this._activeSection = label; + + // TODO: set current section + //this._sectionContext?.setCurrent(section.alias); const moreTab = this.shadowRoot?.getElementById('moreTab'); moreTab?.setAttribute('active', 'true'); @@ -135,40 +138,64 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) { super.connectedCallback(); this.requestContext('umbRouter'); - this.requestContext('umbExtensionRegistry'); + this.requestContext('umbSectionContext'); } contextInjected(contexts: Map): void { - if (contexts.has('umbExtensionRegistry')) { - this._extensionRegistry = contexts.get('umbExtensionRegistry'); - this._useSections(); - } - if (contexts.has('umbRouter')) { this._router = contexts.get('umbRouter'); + this._useLocation(); + } + + if (contexts.has('umbSectionContext')) { + this._sectionContext = contexts.get('umbSectionContext'); + this._useCurrentSection(); + this._useSections(); } } + private _useLocation () { + this._locationSubscription?.unsubscribe(); + + this._locationSubscription = this._router?.location + .subscribe((location: UmbRouteLocation) => { + this._location = location; + }); + } + + private _useCurrentSection () { + this._currentSectionSubscription?.unsubscribe(); + + this._currentSectionSubscription = this._sectionContext?.getCurrent() + .subscribe(section => { + this._currentSectionAlias = section.alias; + }); + } + private async _useSections() { - this._subscription?.unsubscribe(); + this._sectionSubscription?.unsubscribe(); const { data } = await getUserSections({}); this._allowedSection = data.sections; - this._subscription = this._extensionRegistry?.extensions - .pipe( - map((extensions: Array>) => - extensions.filter(extension => extension.type === 'section') - )) + this._sectionSubscription = this._sectionContext?.getSections() .subscribe((sectionExtensions: any) => { - this._availableSections = [...sectionExtensions]; - this._sections = this._availableSections.filter((section) => this._allowedSection.includes(section.alias)); - // TODO: implement resize observer + this._sections = sectionExtensions.filter((section: any) => this._allowedSection.includes(section.alias)); this._visibleSections = this._sections; - this._activeSection = this._visibleSections?.[0].alias; + + const currentSectionAlias = this._sections.find(section => section.name === this._location?.params?.section)?.alias; + if (!currentSectionAlias) return; + this._sectionContext?.setCurrent(currentSectionAlias); }); } + disconnectedCallback(): void { + super.disconnectedCallback(); + this._locationSubscription?.unsubscribe(); + this._sectionSubscription?.unsubscribe(); + this._currentSectionSubscription?.unsubscribe(); + } + private _renderExtraSections() { return when( this._extraSections.length > 0, @@ -183,7 +210,7 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) { ${this._extraSections.map( (section) => html` ` @@ -207,7 +234,7 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) { ${this._visibleSections.map( (section) => html` ` diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-main.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-main.element.ts index af915b7996..fd9b98be03 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-main.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-main.element.ts @@ -1,9 +1,20 @@ import { defineElement } from '@umbraco-ui/uui-base/lib/registration'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html, LitElement } from 'lit'; +import { state } from 'lit/decorators.js'; +import { Subscription } from 'rxjs'; + +import { UmbContextInjectMixin } from '../core/context'; +import { UmbSectionContext } from '../section.context'; + +import { UmbExtensionManifest, UmbManifestSectionMeta } from '../core/extension'; + +// TODO: lazy load these. How to we handle dynamic import of our typescript file? +import '../content/content-section.element'; +import '../media/media-section.element'; @defineElement('umb-backoffice-main') -export class UmbBackofficeMain extends LitElement { +export class UmbBackofficeMain extends UmbContextInjectMixin(LitElement) { static styles = [ UUITextStyles, css` @@ -70,8 +81,56 @@ export class UmbBackofficeMain extends LitElement { `, ]; + @state() + private _sectionElement?: HTMLElement; + + private _sectionContext?: UmbSectionContext; + private _currentSectionSubscription?: Subscription; + + connectedCallback(): void { + super.connectedCallback(); + this.requestContext('umbRouter'); + this.requestContext('umbSectionContext'); + } + + contextInjected(contexts: Map): void { + if (contexts.has('umbSectionContext')) { + this._sectionContext = contexts.get('umbSectionContext'); + this._useCurrentSection(); + } + } + + private _useCurrentSection () { + this._currentSectionSubscription?.unsubscribe(); + + this._currentSectionSubscription = this._sectionContext?.getCurrent() + .subscribe(section => { + this._createSectionElement(section); + }); + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + this._currentSectionSubscription?.unsubscribe(); + } + + private async _createSectionElement (section: UmbExtensionManifest) { + if (!section) return; + + // TODO: How do we handle dynamic imports of our files? + if (section.js) { + await import(/* @vite-ignore */section.js); + } + + if (section.elementName) { + this._sectionElement = document.createElement(section.elementName); + } + } + render() { return html` + ${ this._sectionElement } + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/core/router/router.ts b/src/Umbraco.Web.UI.Client/src/core/router/router.ts index dabf89de10..50d2746711 100644 --- a/src/Umbraco.Web.UI.Client/src/core/router/router.ts +++ b/src/Umbraco.Web.UI.Client/src/core/router/router.ts @@ -4,7 +4,7 @@ import { UmbRouterBeforeLeaveEvent } from './router-before-leave.event'; export interface UmbRoute { path: string; - elementName: string; + alias: string; meta?: any; } @@ -24,15 +24,13 @@ export interface UmbRouteElement extends HTMLElement { export class UmbRouter { private _routes: Array = []; private _host: HTMLElement; - private _outlet: HTMLElement; private _element?: UmbRouteElement; private _location: ReplaySubject = new ReplaySubject(1); public readonly location: Observable = this._location.asObservable(); - constructor(host: HTMLElement, outlet: HTMLElement) { + constructor(host: HTMLElement) { this._host = host; - this._outlet = outlet; // Anchor Hijacker this._host.addEventListener('click', async (event: any) => { @@ -110,15 +108,12 @@ export class UmbRouter { const canLeave = await this._requestLeave(location); if (!canLeave) return; - this._setupElement(location); - const canEnter = await this._requestEnter(location); if (!canEnter) return; window.history.pushState(null, '', pathname); this._location.next(location); - this._render(); } private _resolve(pathname: string): UmbRouteLocation | null { @@ -144,20 +139,4 @@ export class UmbRouter { return location; } - - private _setupElement(location: UmbRouteLocation) { - this._element = document.createElement(location.route.elementName); - this._element.location = location; - } - - private async _render() { - if (!this._element) return; - - const childNodes = this._outlet.childNodes; - childNodes.forEach((node) => { - this._outlet.removeChild(node); - }); - - this._outlet.appendChild(this._element); - } } diff --git a/src/Umbraco.Web.UI.Client/src/section.context.ts b/src/Umbraco.Web.UI.Client/src/section.context.ts new file mode 100644 index 0000000000..7062fade4f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/section.context.ts @@ -0,0 +1,31 @@ +import { firstValueFrom, map, Observable, ReplaySubject } from 'rxjs'; +import { UmbExtensionManifest, UmbExtensionRegistry, UmbManifestSectionMeta } from './core/extension'; + +export class UmbSectionContext { + private _extensionRegistry!: UmbExtensionRegistry; + + private _current: ReplaySubject> = new ReplaySubject(1); + public readonly current: Observable> = this._current.asObservable(); + + constructor(_extensionRegistry: UmbExtensionRegistry) { + this._extensionRegistry = _extensionRegistry; + } + + getSections () { + return this._extensionRegistry.extensions + .pipe( + map((extensions: Array>) => extensions.filter(extension => extension.type === 'section')) + ); + } + + getCurrent () { + return this.current; + } + + async setCurrent (sectionAlias: string) { + const sections = await firstValueFrom(this.getSections()); + const matchedSection = sections.find(section => section.alias === sectionAlias) as UmbExtensionManifest; + this._current.next(matchedSection); + } + +} \ No newline at end of file