diff --git a/src/Umbraco.Web.UI.Client/index.html b/src/Umbraco.Web.UI.Client/index.html index 9e88ebf14d..55a4b269e6 100644 --- a/src/Umbraco.Web.UI.Client/index.html +++ b/src/Umbraco.Web.UI.Client/index.html @@ -7,6 +7,7 @@ Umbraco + diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 012b64bf7c..4ec4769f2a 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -13,6 +13,7 @@ "element-internals-polyfill": "^1.1.4", "lit": "^2.2.5", "openapi-typescript-fetch": "^1.1.3", + "router-slot": "^1.5.5", "rxjs": "^7.5.5" }, "devDependencies": { @@ -6337,6 +6338,11 @@ "fsevents": "~2.3.2" } }, + "node_modules/router-slot": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/router-slot/-/router-slot-1.5.5.tgz", + "integrity": "sha512-wu+6ZAaU37ARD24Jox9CWFvQ4s6PWL6LiqkqpphLLGNer+J6530HzZ64HXPDQBaCwAx82TAzUhZDvSAtaxJX2A==" + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -12206,6 +12212,11 @@ "fsevents": "~2.3.2" } }, + "router-slot": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/router-slot/-/router-slot-1.5.5.tgz", + "integrity": "sha512-wu+6ZAaU37ARD24Jox9CWFvQ4s6PWL6LiqkqpphLLGNer+J6530HzZ64HXPDQBaCwAx82TAzUhZDvSAtaxJX2A==" + }, "run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 7729873f7e..acdf414e02 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -36,6 +36,7 @@ "element-internals-polyfill": "^1.1.4", "lit": "^2.2.5", "openapi-typescript-fetch": "^1.1.3", + "router-slot": "^1.5.5", "rxjs": "^7.5.5" }, "devDependencies": { diff --git a/src/Umbraco.Web.UI.Client/src/app.ts b/src/Umbraco.Web.UI.Client/src/app.ts index 0bf836ff4d..ba60d5c8b1 100644 --- a/src/Umbraco.Web.UI.Client/src/app.ts +++ b/src/Umbraco.Web.UI.Client/src/app.ts @@ -1,61 +1,35 @@ -import './auth/auth-layout.element'; -import './auth/login/login.element'; -import './backoffice/backoffice.element'; -import './installer/installer.element'; -import './node-editor/node-editor-layout.element'; -import './node-editor/node-property-data-type.element'; -import './node-editor/node-property.element'; +import 'router-slot'; import '@umbraco-ui/uui-css/dist/uui-css.css'; import { UUIIconRegistryEssential } from '@umbraco-ui/uui'; import { css, html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; -import { Subscription } from 'rxjs'; import { getInitStatus } from './core/api/fetcher'; import { UmbContextProviderMixin } from './core/context'; -import { - isUmbRouterBeforeEnterEvent, - UmbRoute, - UmbRouteLocation, - UmbRouter, - UmbRouterBeforeEnterEvent, - umbRouterBeforeEnterEventType, -} from './core/router'; -import { UmbSectionContext } from './section.context'; import { UmbNodeStore } from './core/stores/node.store'; import { UmbDataTypeStore } from './core/stores/data-type.store'; -// TODO: lazy load these -const routes: Array = [ +// Load these in the correct components +import './node-editor/node-editor-layout.element'; +import './node-editor/node-property-data-type.element'; +import './node-editor/node-property.element'; + +const routes = [ { - path: '/login', - alias: 'login', - meta: { requiresAuth: false }, + path: 'login', + component: () => import('./auth/login/login.element'), }, { - path: '/install', - alias: 'install', - meta: { requiresAuth: false }, + path: 'install', + component: () => import('./installer/installer.element'), }, { - path: '/section/:section', - alias: 'app', - meta: { requiresAuth: true }, - }, - { - path: '/section/:section/dashboard/:dashboard', - alias: 'dashboard', - meta: { requiresAuth: true }, - }, - { - path: '/section/:section/node/:nodeId', - alias: 'node', - meta: { requiresAuth: true }, + path: '**', + component: () => import('./backoffice/backoffice.element'), }, ]; -// Import somewhere else? @customElement('umb-app') export class UmbApp extends UmbContextProviderMixin(LitElement) { static styles = css` @@ -68,106 +42,50 @@ export class UmbApp extends UmbContextProviderMixin(LitElement) { `; private _iconRegistry: UUIIconRegistryEssential = new UUIIconRegistryEssential(); - private _isInstalled = false; - private _view?: HTMLElement; - private _router?: UmbRouter; - private _locationSubscription?: Subscription; - constructor() { super(); - this.addEventListener(umbRouterBeforeEnterEventType, this._onBeforeEnter); this._iconRegistry.attach(this); const { extensionRegistry } = window.Umbraco; - - this.provideContext('umbExtensionRegistry', window.Umbraco.extensionRegistry); - this.provideContext('umbSectionContext', new UmbSectionContext(extensionRegistry)); + this.provideContext('umbExtensionRegistry', extensionRegistry); // TODO: consider providing somethings for install/login and some only for 'backoffice'. this.provideContext('umbNodeStore', new UmbNodeStore()); this.provideContext('umbDataTypeStore', new UmbDataTypeStore()); } - private _onBeforeEnter = (event: Event) => { - if (!isUmbRouterBeforeEnterEvent(event)) return; - this._handleUnauthorizedNavigation(event); - }; - - private _handleUnauthorizedNavigation(event: UmbRouterBeforeEnterEvent) { - if (event.to.route.meta.requiresAuth && !this._isAuthorized()) { - event.preventDefault(); - this._router?.push('/login'); - } - } - private _isAuthorized(): boolean { return sessionStorage.getItem('is-authenticated') === 'true'; } protected async firstUpdated(): Promise { - this._router = new UmbRouter(this); - this._router.setRoutes(routes); + this.shadowRoot?.querySelector('router-slot')?.render(); - // TODO: find a solution for magic strings - this.provideContext('umbRouter', this._router); - - this._useLocation(); // TODO: Are we sure we want to do this here? The installer cannot be shown if we don't act on the routes at this point... - - // TODO: this is a temporary routing solution for shell elements try { const { data } = await getInitStatus({}); this._isInstalled = data.installed; if (!this._isInstalled) { - this._router.push('/install'); + history.pushState(null, '', '/install'); return; } if (!this._isAuthorized() || window.location.pathname === '/install') { - this._router.push('/login'); + history.pushState(null, '', 'login'); } else { const next = window.location.pathname === '/' ? '/section/content' : window.location.pathname; - this._router.push(next); + history.pushState(null, '', next); } } 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`${this._view}`; + return html``; } } 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 b19aedd51d..f365c693bf 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 @@ -4,13 +4,11 @@ import { customElement, state } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { postUserLogin } from '../../core/api/fetcher'; -import { UmbContextConsumerMixin } from '../../core/context'; -import { UmbRouter } from '../../core/router'; -// create custom element with lit-element named 'umb-login' +import '../auth-layout.element'; @customElement('umb-login') -export class UmbLogin extends UmbContextConsumerMixin(LitElement) { +export default class UmbLogin extends LitElement { static styles: CSSResultGroup = [ UUITextStyles, css` @@ -24,18 +22,6 @@ export class UmbLogin extends UmbContextConsumerMixin(LitElement) { @state() private _loggingIn = false; - _router?: UmbRouter; - - connectedCallback() { - super.connectedCallback(); - - // TODO: find solution for magic string - // TODO: can we use a property decorator and a callback? - this.consumeContext('umbRouter', (api: unknown) => { - this._router = api as UmbRouter; - }); - } - private _handleSubmit = (e: SubmitEvent) => { e.preventDefault(); @@ -55,14 +41,12 @@ export class UmbLogin extends UmbContextConsumerMixin(LitElement) { }; private async _login(username: string, password: string, persist: boolean) { - console.log('LOGIN', username, password, persist); this._loggingIn = true; try { await postUserLogin({ username, password, persist }); this._loggingIn = false; - // TODO: how do we know where to go? - this._router?.push('/section/content'); + history.pushState(null, '', '/section'); } catch (error) { console.log(error); this._loggingIn = false; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-header-sections.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-header-sections.element.ts index c4cd5bf3b5..731e929dd8 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-header-sections.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-header-sections.element.ts @@ -1,14 +1,13 @@ +import { Subscription, map } from 'rxjs'; 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 { Subscription } from 'rxjs'; +import { isPathActive, path } from 'router-slot'; import { getUserSections } from '../core/api/fetcher'; +import { UmbExtensionRegistry, UmbExtensionManifest, UmbExtensionManifestSection } from '../core/extension'; import { UmbContextConsumerMixin } from '../core/context'; -import { UmbExtensionManifestSection } from '../core/extension'; -import { UmbRouteLocation, UmbRouter } from '../core/router'; -import { UmbSectionContext } from '../section.context'; @customElement('umb-backoffice-header-sections') export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElement) { @@ -56,24 +55,15 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem @state() private _currentSectionAlias = ''; - private _router?: UmbRouter; - private _sectionContext?: UmbSectionContext; + private _extensionRegistry?: UmbExtensionRegistry; + private _sectionSubscription?: Subscription; - private _currentSectionSubscription?: Subscription; - private _locationSubscription?: Subscription; - private _location?: UmbRouteLocation; constructor() { super(); - this.consumeContext('umbRouter', (_instance: UmbRouter) => { - this._router = _instance; - this._useLocation(); - }); - - this.consumeContext('umbSectionContext', (_instance: UmbSectionContext) => { - this._sectionContext = _instance; - this._useCurrentSection(); + this.consumeContext('umbExtensionRegistry', (extensionRegistry: UmbExtensionRegistry) => { + this._extensionRegistry = extensionRegistry; this._useSections(); }); } @@ -92,69 +82,48 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem } // TODO: this could maybe be handled by an anchor tag - this._router?.push(`/section/${section.meta.pathname}`); - this._sectionContext?.setCurrent(section.alias); + history.pushState(null, '', `/section/${section.meta.pathname}`); } - private _handleLabelClick(e: PointerEvent) { - const label = (e.target as any).label; - - // TODO: set current section - //this._sectionContext?.setCurrent(section.alias); - + private _handleLabelClick() { const moreTab = this.shadowRoot?.getElementById('moreTab'); moreTab?.setAttribute('active', 'true'); this._open = false; } - 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._sectionSubscription?.unsubscribe(); const { data } = await getUserSections({}); this._allowedSection = data.sections; - this._sectionSubscription = this._sectionContext?.getSections().subscribe((sectionExtensions) => { - this._sections = sectionExtensions.filter((section) => this._allowedSection.includes(section.alias)); - this._visibleSections = this._sections; - const currentSectionAlias = this._sections.find( - (section) => section.meta.pathname === this._location?.params?.section - )?.alias; - if (!currentSectionAlias) return; - this._sectionContext?.setCurrent(currentSectionAlias); - }); + this._sectionSubscription = this._extensionRegistry?.extensions + .pipe( + map((extensions: Array) => + extensions + .filter((extension) => extension.type === 'section') + .sort((a: any, b: any) => b.meta.weight - a.meta.weight) + ) + ) + .subscribe((sections: Array) => { + this._sections = sections.filter((section: any) => this._allowedSection.includes(section.alias)); + this._visibleSections = this._sections; + }); } disconnectedCallback(): void { super.disconnectedCallback(); - this._locationSubscription?.unsubscribe(); this._sectionSubscription?.unsubscribe(); - this._currentSectionSubscription?.unsubscribe(); } render() { return html` ${this._visibleSections.map( - (section) => html` + (section: any) => html` ` 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 b8e03e8d27..b56bdc7b72 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 @@ -4,6 +4,7 @@ import { customElement } from 'lit/decorators.js'; import './backoffice-header-sections.element'; import './backoffice-header-tools.element'; + @customElement('umb-backoffice-header') export class UmbBackofficeHeader extends LitElement { static styles: CSSResultGroup = [ 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 f424515edd..0920fa2413 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 @@ -2,11 +2,15 @@ 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 { Subscription, map } from 'rxjs'; import { UmbContextConsumerMixin } from '../core/context'; -import { UmbSectionContext } from '../section.context'; -import { UmbExtensionManifest } from '../core/extension'; +import { + UmbExtensionRegistry, + createExtensionElement, + UmbExtensionManifest, + UmbExtensionManifestSection, +} from '../core/extension'; @defineElement('umb-backoffice-main') export class UmbBackofficeMain extends UmbContextConsumerMixin(LitElement) { @@ -22,52 +26,59 @@ export class UmbBackofficeMain extends UmbContextConsumerMixin(LitElement) { ]; @state() - private _sectionElement?: HTMLElement; + private _routes: Array = []; - private _sectionContext?: UmbSectionContext; - private _currentSectionSubscription?: Subscription; + @state() + private _sections: Array = []; + + private _extensionRegistry?: UmbExtensionRegistry; + private _sectionSubscription?: Subscription; constructor() { super(); - this.consumeContext('umbSectionContext', (_instance: UmbSectionContext) => { - this._sectionContext = _instance; - this._useCurrentSection(); + this.consumeContext('umbExtensionRegistry', (_instance: UmbExtensionRegistry) => { + this._extensionRegistry = _instance; + this._useSections(); }); } - private _useCurrentSection() { - this._currentSectionSubscription?.unsubscribe(); + private _useSections() { + this._sectionSubscription?.unsubscribe(); - this._currentSectionSubscription = this._sectionContext?.getCurrent().subscribe((section) => { - this._createSectionElement(section); - }); + this._sectionSubscription = this._extensionRegistry?.extensions + .pipe( + map((extensions: Array) => + extensions + .filter((extension) => extension.type === 'section') + .sort((a: any, b: any) => b.meta.weight - a.meta.weight) + ) + ) + .subscribe((sections) => { + this._routes = []; + this._sections = sections as Array; + + this._routes = this._sections.map((section) => { + return { + path: 'section/' + section.meta.pathname, + component: () => createExtensionElement(section), + }; + }); + + this._routes.push({ + path: '**', + redirectTo: 'section/' + this._sections[0].meta.pathname, + }); + }); } 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 (typeof section.js === 'string') { - await import(/* @vite-ignore */ section.js); - } - - if (typeof section.js === 'function') { - await section.js(); - } - - if (section.elementName) { - this._sectionElement = document.createElement(section.elementName); - } + this._sectionSubscription?.unsubscribe(); } render() { - return html` ${this._sectionElement} `; + return html``; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-sidebar.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-sidebar.element.ts index 128e896597..946485cc1b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-sidebar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-sidebar.element.ts @@ -1,8 +1,8 @@ -import { defineElement } from '@umbraco-ui/uui-base/lib/registration'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html, LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; -@defineElement('umb-backoffice-sidebar') +@customElement('umb-backoffice-sidebar') export class UmbBackofficeSidebar extends LitElement { static styles = [ UUITextStyles, diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts index 7a67950cac..10a59c78ff 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -7,7 +7,7 @@ import './backoffice-sidebar.element'; import './backoffice-main.element'; @defineElement('umb-backoffice') -export class UmbBackoffice extends LitElement { +export default class UmbBackoffice extends LitElement { static styles = [ UUITextStyles, css` diff --git a/src/Umbraco.Web.UI.Client/src/content/content-dashboards.element.ts b/src/Umbraco.Web.UI.Client/src/content/content-dashboards.element.ts index 564d94d0a9..d58fd3208f 100644 --- a/src/Umbraco.Web.UI.Client/src/content/content-dashboards.element.ts +++ b/src/Umbraco.Web.UI.Client/src/content/content-dashboards.element.ts @@ -1,11 +1,16 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; +import { IRoutingInfo } from 'router-slot'; import { map, Subscription } from 'rxjs'; import { UmbContextConsumerMixin } from '../core/context'; -import { UmbExtensionManifestDashboard, UmbExtensionRegistry } from '../core/extension'; -import { UmbRouteLocation, UmbRouter } from '../core/router'; +import { + UmbExtensionManifestDashboard, + UmbExtensionManifest, + UmbExtensionRegistry, + createExtensionElement, +} from '../core/extension'; @customElement('umb-content-dashboards') export class UmbContentDashboards extends UmbContextConsumerMixin(LitElement) { @@ -25,11 +30,7 @@ export class UmbContentDashboards extends UmbContextConsumerMixin(LitElement) { private _current = ''; @state() - private _outlet?: HTMLElement; - - private _router?: UmbRouter; - private _locationSubscription?: Subscription; - private _location?: UmbRouteLocation; + private _routes: Array = []; private _extensionRegistry?: UmbExtensionRegistry; private _dashboardsSubscription?: Subscription; @@ -37,80 +38,51 @@ export class UmbContentDashboards extends UmbContextConsumerMixin(LitElement) { constructor() { super(); - this.consumeContext('umbRouter', (_instance: UmbRouter) => { - this._router = _instance; - this._useLocation(); - }); - this.consumeContext('umbExtensionRegistry', (_instance: UmbExtensionRegistry) => { this._extensionRegistry = _instance; this._useDashboards(); }); } - private _useLocation() { - this._locationSubscription?.unsubscribe(); - - this._router?.location.subscribe((location: UmbRouteLocation) => { - this._location = location; - }); - } - private _useDashboards() { this._dashboardsSubscription?.unsubscribe(); - this._dashboardsSubscription = this._extensionRegistry - ?.extensionsOfType('dashboard') - .pipe(map((extensions) => extensions.sort((a, b) => b.meta.weight - a.meta.weight))) - .subscribe((dashboards) => { - // TODO: What do we want to use as path? - this._dashboards = dashboards; - const dashboardLocation = decodeURIComponent(this._location?.params?.dashboard); - const sectionLocation = this._location?.params?.section; + this._dashboardsSubscription = this._extensionRegistry?.extensions + .pipe( + map((extensions: Array) => + extensions + .filter((extension) => extension.type === 'dashboard') + .sort((a: any, b: any) => b.meta.weight - a.meta.weight) + ) + ) + .subscribe((dashboards: Array) => { + this._dashboards = dashboards as Array; + this._routes = []; - // TODO: Temp redirect solution - if (dashboardLocation === 'undefined') { - this._router?.push(`/section/${sectionLocation}/dashboard/${this._dashboards[0].meta.pathname}`); - this._setCurrent(this._dashboards[0]); - return; - } + this._routes = this._dashboards.map((dashboard) => { + return { + path: `${dashboard.meta.pathname}`, + component: () => createExtensionElement(dashboard), + setup: (element: UmbExtensionManifestDashboard, info: IRoutingInfo) => { + this._current = info.match.route.path; + }, + }; + }); - const dashboard = this._dashboards.find((dashboard) => dashboard.meta.pathname === dashboardLocation); - - if (!dashboard) { - this._router?.push(`/section/${sectionLocation}/dashboard/${this._dashboards[0].meta.pathname}`); - this._setCurrent(this._dashboards[0]); - return; - } - - this._setCurrent(dashboard); + this._routes.push({ + path: '**', + redirectTo: this._dashboards[0].meta.pathname, + }); }); } - private _handleTabClick(_e: PointerEvent, dashboard: UmbExtensionManifestDashboard) { - // TODO: this could maybe be handled by an anchor tag - const section = this._location?.params?.section; - this._router?.push(`/section/${section}/dashboard/${dashboard.meta.pathname}`); - this._setCurrent(dashboard); - } - - // TODO: Temp outlet solution - private async _setCurrent(dashboard: UmbExtensionManifestDashboard) { - if (typeof dashboard.js === 'function') { - await dashboard.js(); - } - - if (dashboard.elementName) { - const element = document.createElement(dashboard.elementName); - this._outlet = element; - } - + private _handleTabClick(e: PointerEvent, dashboard: UmbExtensionManifestDashboard) { + history.pushState(null, '', `/section/content/dashboard/${dashboard.meta.pathname}`); this._current = dashboard.name; } disconnectedCallback() { super.disconnectedCallback(); - this._locationSubscription?.unsubscribe(); this._dashboardsSubscription?.unsubscribe(); } @@ -118,19 +90,21 @@ export class UmbContentDashboards extends UmbContextConsumerMixin(LitElement) { return html` ${this._dashboards.map( - (dashboard) => html` + (dashboard: UmbExtensionManifestDashboard) => html` ` )} - ${this._outlet} + `; } } +export default UmbContentDashboards; + declare global { interface HTMLElementTagNameMap { 'umb-content-dashboards': UmbContentDashboards; diff --git a/src/Umbraco.Web.UI.Client/src/content/content-editor.element.ts b/src/Umbraco.Web.UI.Client/src/content/content-editor.element.ts index f6c09b8279..ee7f12ed0e 100644 --- a/src/Umbraco.Web.UI.Client/src/content/content-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/content/content-editor.element.ts @@ -7,7 +7,7 @@ import { Subscription } from 'rxjs'; import { DocumentNode } from '../mocks/data/content.data'; @customElement('umb-content-editor') -class UmbContentEditor extends UmbContextConsumerMixin(LitElement) { +export class UmbContentEditor extends UmbContextConsumerMixin(LitElement) { static styles = [ UUITextStyles, css` @@ -143,6 +143,8 @@ class UmbContentEditor extends UmbContextConsumerMixin(LitElement) { } } +export default UmbContentEditor; + declare global { interface HTMLElementTagNameMap { 'umb-content-editor': UmbContentEditor; 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 index 268e77022c..905f038504 100644 --- a/src/Umbraco.Web.UI.Client/src/content/content-section.element.ts +++ b/src/Umbraco.Web.UI.Client/src/content/content-section.element.ts @@ -1,20 +1,17 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html, LitElement } from 'lit'; -import { customElement } from 'lit/decorators.js'; -import { UmbContextConsumerMixin, UmbContextProviderMixin } from '../core/context'; -import { UmbRouteLocation, UmbRouter } from '../core/router'; -import { Subscription } from 'rxjs'; +import { customElement, state } from 'lit/decorators.js'; +import { IRoute, IRoutingInfo } from 'router-slot'; import './content-tree.element'; -import './content-dashboards.element'; -import './content-editor.element'; @customElement('umb-content-section') -export class UmbContentSection extends UmbContextProviderMixin(UmbContextConsumerMixin(LitElement)) { +export class UmbContentSection extends LitElement { static styles = [ UUITextStyles, css` - :host { + :host, + #router-slot { display: flex; width: 100%; height: 100%; @@ -22,54 +19,36 @@ export class UmbContentSection extends UmbContextProviderMixin(UmbContextConsume `, ]; - private _router?: UmbRouter; - private _locationSubscription?: Subscription; - private _outlet?: HTMLElement; + @state() + private _routes: Array = [ + { + path: 'dashboard', + component: () => import('./content-dashboards.element'), + }, + { + path: 'node/:nodeId', + component: () => import('./content-editor.element'), + setup: (component: HTMLElement, info: IRoutingInfo) => { + this._currentNodeId = info.match.params.nodeId; + component.id = this._currentNodeId; + }, + }, + { + path: '**', + redirectTo: 'dashboard', + }, + ]; - constructor() { - super(); - - this.consumeContext('umbRouter', (_instance: UmbRouter) => { - this._router = _instance; - this._useLocation(); - }); - } - - private _useLocation() { - this._locationSubscription?.unsubscribe(); - - this._locationSubscription = this._router?.location.subscribe((location: UmbRouteLocation) => { - // TODO: temp outlet solution - const nodeId = location.params.nodeId; - - this._outlet?.parentNode?.removeChild(this._outlet); - - if (nodeId !== undefined) { - const contentEditor = document.createElement('umb-content-editor'); - contentEditor.id = nodeId; - this._outlet = contentEditor; - this.requestUpdate(); - return; - } - - const dashboards = document.createElement('umb-content-dashboards'); - this._outlet = dashboards; - this.requestUpdate(); - }); - } - - disconnectedCallback(): void { - super.disconnectedCallback(); - this._locationSubscription?.unsubscribe(); - } + @state() + private _currentNodeId!: string; render() { return html` - + - ${this._outlet} + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/content/content-tree.element.ts b/src/Umbraco.Web.UI.Client/src/content/content-tree.element.ts index 3afd8a217d..4181edf12b 100644 --- a/src/Umbraco.Web.UI.Client/src/content/content-tree.element.ts +++ b/src/Umbraco.Web.UI.Client/src/content/content-tree.element.ts @@ -1,14 +1,10 @@ import { css, html, LitElement } from 'lit'; -import { customElement, state } from 'lit/decorators.js'; +import { customElement, state, property } from 'lit/decorators.js'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { UmbContextConsumerMixin } from '../core/context'; -import { UmbRouteLocation, UmbRouter } from '../core/router'; -import { Subscription } from 'rxjs'; import { data } from '../mocks/data/content.data'; -import { UUIMenuItemElement } from '@umbraco-ui/uui'; @customElement('umb-content-tree') -class UmbContentTree extends UmbContextConsumerMixin(LitElement) { +class UmbContentTree extends LitElement { static styles = [ UUITextStyles, css` @@ -18,81 +14,30 @@ class UmbContentTree extends UmbContextConsumerMixin(LitElement) { `, ]; - // simplified tree for testing + @property() + public id!: string; + + // simplified tree data for testing @state() - _tree: Array = []; + _tree: Array = data; @state() _section?: string; - @state() - _currentNodeId?: number; - - private _router?: UmbRouter; - private _location?: UmbRouteLocation; - private _locationSubscription?: Subscription; - - constructor() { - super(); - - // TODO: implement correct tree data - this._tree = data; - - this.consumeContext('umbRouter', (router: UmbRouter) => { - this._router = router; - this._useLocation(); - }); - } - - private _useLocation() { - this._locationSubscription?.unsubscribe(); - - this._locationSubscription = this._router?.location.subscribe((location) => { - this._location = location; - this._section = location.params.section; - this._currentNodeId = parseInt(location.params.nodeId); - }); - } - - disconnectedCallback(): void { - super.disconnectedCallback(); - this._locationSubscription?.unsubscribe(); - } - - /* TODO: there are some problems with menu items and click events. They can happen on element inside and outside of the shadow dom - which makes it difficult to find the right href in the router. - It might make sense to make it possible to use your own anchor tag or button inside a label slot instead. - This is a temp solution to get the href from the menu item and overwrite the router hijacking. - */ - private _handleMenuItemClick(e: PointerEvent) { - e.preventDefault(); - e.stopPropagation(); - - const target = e.target as UUIMenuItemElement; - if (!target) return; - - const href = target.href; - if (!href) return; - - this._router?.push(href); - } - render() { return html` - +

Content