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 d6171e0712..2b5f3ad362 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -2,12 +2,14 @@ import { defineElement } from '@umbraco-ui/uui-base/lib/registration'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html, LitElement } from 'lit'; -import { UmbContextProviderMixin } from '../core/context'; +import { UmbContextProviderMixin, UmbContextConsumerMixin } from '../core/context'; import { UmbNotificationService } from '../core/services/notification'; import { UmbModalService } from '../core/services/modal'; import { UmbDataTypeStore } from '../core/stores/data-type.store'; import { UmbDocumentTypeStore } from '../core/stores/document-type.store'; import { UmbNodeStore } from '../core/stores/node.store'; +import { UmbSectionContext } from './sections/section.context'; +import { UmbSectionStore } from '../core/stores/section.store'; import './components/backoffice-header.element'; import './components/backoffice-main.element'; @@ -15,12 +17,13 @@ import './components/backoffice-notification-container.element'; import './components/backoffice-modal-container.element'; import './components/editor-property-layout.element'; import './components/node-property.element'; -import './components/section-layout.element'; -import './components/section-sidebar.element'; -import './components/section-main.element'; +import './sections/shared/section-layout.element'; +import './sections/shared/section-sidebar.element'; +import './sections/shared/section-main.element'; +import { Subscription } from 'rxjs'; @defineElement('umb-backoffice') -export default class UmbBackoffice extends UmbContextProviderMixin(LitElement) { +export default class UmbBackoffice extends UmbContextConsumerMixin(UmbContextProviderMixin(LitElement)) { static styles = [ UUITextStyles, css` @@ -36,6 +39,9 @@ export default class UmbBackoffice extends UmbContextProviderMixin(LitElement) { `, ]; + private _umbSectionStore?: UmbSectionStore; + private _currentSectionSubscription?: Subscription; + constructor() { super(); @@ -44,6 +50,17 @@ export default class UmbBackoffice extends UmbContextProviderMixin(LitElement) { this.provideContext('umbDocumentTypeStore', new UmbDocumentTypeStore()); this.provideContext('umbNotificationService', new UmbNotificationService()); this.provideContext('umbModalService', new UmbModalService()); + + // TODO: how do we want to handle context aware DI? + this.consumeContext('umbExtensionRegistry', (extensionRegistry) => { + this._umbSectionStore = new UmbSectionStore(extensionRegistry); + this.provideContext('umbSectionStore', this._umbSectionStore); + }); + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + this._currentSectionSubscription?.unsubscribe(); } render() { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/components/backoffice-header-sections.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/components/backoffice-header-sections.element.ts index 64739ae765..bbdd020854 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/components/backoffice-header-sections.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/components/backoffice-header-sections.element.ts @@ -3,14 +3,15 @@ import { css, CSSResultGroup, html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { when } from 'lit/directives/when.js'; import { isPathActive, path } from 'router-slot'; -import { map, Subscription } from 'rxjs'; +import { Subscription } from 'rxjs'; -import { getUserSections } from '../../core/api/fetcher'; -import { UmbContextConsumerMixin } from '../../core/context'; -import { UmbExtensionManifestSection, UmbExtensionRegistry } from '../../core/extension'; +import { UmbContextConsumerMixin, UmbContextProvider, UmbContextProviderMixin } from '../../core/context'; +import { UmbExtensionManifestSection } from '../../core/extension'; +import { UmbSectionStore } from '../../core/stores/section.store'; +import { UmbSectionContext } from '../sections/section.context'; @customElement('umb-backoffice-header-sections') -export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElement) { +export class UmbBackofficeHeaderSections extends UmbContextProviderMixin(UmbContextConsumerMixin(LitElement)) { static styles: CSSResultGroup = [ UUITextStyles, css` @@ -40,9 +41,6 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem @state() private _open = false; - @state() - private _allowedSection: Array = []; - @state() private _sections: Array = []; @@ -55,16 +53,18 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem @state() private _currentSectionAlias = ''; - private _extensionRegistry?: UmbExtensionRegistry; + private _sectionStore?: UmbSectionStore; private _sectionSubscription?: Subscription; + private _currentSectionSubscription?: Subscription; constructor() { super(); - this.consumeContext('umbExtensionRegistry', (extensionRegistry: UmbExtensionRegistry) => { - this._extensionRegistry = extensionRegistry; + this.consumeContext('umbSectionStore', (sectionStore: UmbSectionStore) => { + this._sectionStore = sectionStore; this._useSections(); + this._useCurrentSection(); }); } @@ -77,9 +77,11 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem const tab = e.currentTarget as HTMLElement; // TODO: we need to be able to prevent the tab from setting the active state - if (tab.id === 'moreTab') { - return; - } + if (tab.id === 'moreTab') return; + + if (!tab.dataset.alias) return; + + this._sectionStore?.setCurrent(tab.dataset.alias); } private _handleLabelClick() { @@ -89,19 +91,21 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem this._open = false; } - private async _useSections() { + private _useSections() { this._sectionSubscription?.unsubscribe(); - const { data } = await getUserSections({}); - this._allowedSection = data.sections; + this._sectionSubscription = this._sectionStore?.getAllowed().subscribe((allowedSections) => { + this._sections = allowedSections; + this._visibleSections = this._sections; + }); + } - this._sectionSubscription = this._extensionRegistry - ?.extensionsOfType('section') - .pipe(map((extensions) => extensions.sort((a, b) => b.meta.weight - a.meta.weight))) - .subscribe((sections) => { - this._sections = sections.filter((section) => this._allowedSection.includes(section.alias)); - this._visibleSections = this._sections; - }); + private _useCurrentSection() { + this._currentSectionSubscription?.unsubscribe(); + + this._currentSectionSubscription = this._sectionStore?.currentAlias.subscribe((currentSectionAlias) => { + this._currentSectionAlias = currentSectionAlias; + }); } disconnectedCallback(): void { @@ -109,15 +113,17 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem this._sectionSubscription?.unsubscribe(); } - render() { + private _renderSections() { return html` ${this._visibleSections.map( (section: UmbExtensionManifestSection) => html` + label="${section.name}" + data-alias="${section.alias}"> ` )} ${this._renderExtraSections()} @@ -150,6 +156,10 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem ` ); } + + render() { + return html` ${this._renderSections()} `; + } } declare global { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/components/backoffice-main.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/components/backoffice-main.element.ts index 357c6dd0bc..a3990dad04 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/components/backoffice-main.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/components/backoffice-main.element.ts @@ -2,13 +2,16 @@ 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 { map, Subscription } from 'rxjs'; +import { IRoutingInfo } from 'router-slot'; +import { Subscription } from 'rxjs'; -import { UmbContextConsumerMixin } from '../../core/context'; -import { createExtensionElement, UmbExtensionManifestSection, UmbExtensionRegistry } from '../../core/extension'; +import { UmbContextConsumerMixin, UmbContextProviderMixin } from '../../core/context'; +import { createExtensionElement, UmbExtensionManifestSection } from '../../core/extension'; +import { UmbSectionStore } from '../../core/stores/section.store'; +import { UmbSectionContext } from '../sections/section.context'; @defineElement('umb-backoffice-main') -export class UmbBackofficeMain extends UmbContextConsumerMixin(LitElement) { +export class UmbBackofficeMain extends UmbContextProviderMixin(UmbContextConsumerMixin(LitElement)) { static styles = [ UUITextStyles, css` @@ -28,40 +31,61 @@ export class UmbBackofficeMain extends UmbContextConsumerMixin(LitElement) { @state() private _sections: Array = []; - private _extensionRegistry?: UmbExtensionRegistry; + private _routePrefix = 'section/'; + private _sectionContext?: UmbSectionContext; + private _sectionStore?: UmbSectionStore; private _sectionSubscription?: Subscription; constructor() { super(); - this.consumeContext('umbExtensionRegistry', (_instance: UmbExtensionRegistry) => { - this._extensionRegistry = _instance; + this.consumeContext('umbSectionStore', (_instance: UmbSectionStore) => { + this._sectionStore = _instance; this._useSections(); }); } - private _useSections() { + private async _useSections() { this._sectionSubscription?.unsubscribe(); - this._sectionSubscription = this._extensionRegistry - ?.extensionsOfType('section') - .pipe(map((extensions) => extensions.sort((a, b) => b.meta.weight - a.meta.weight))) - .subscribe((sections) => { - this._routes = []; - this._sections = sections as Array; + this._sectionSubscription = this._sectionStore?.getAllowed().subscribe((sections) => { + if (!sections) return; + this._sections = sections; + this._createRoutes(); + }); + } - this._routes = this._sections.map((section) => { - return { - path: 'section/' + section.meta.pathname, - component: () => createExtensionElement(section), - }; - }); + private _createRoutes() { + this._routes = []; + this._routes = this._sections.map((section) => { + return { + path: this._routePrefix + section.meta.pathname, + component: () => createExtensionElement(section), + setup: this._onRouteSetup, + }; + }); - this._routes.push({ - path: '**', - redirectTo: 'section/' + this._sections[0].meta.pathname, - }); - }); + this._routes.push({ + path: '**', + redirectTo: this._routePrefix + this._sections?.[0]?.meta.pathname, + }); + } + + private _onRouteSetup = (_component: HTMLElement, info: IRoutingInfo) => { + const currentPath = info.match.route.path; + const section = this._sections.find((s) => this._routePrefix + s.meta.pathname === currentPath); + if (!section) return; + this._sectionStore?.setCurrent(section.alias); + this._provideSectionContext(section); + }; + + private _provideSectionContext(section: UmbExtensionManifestSection) { + if (!this._sectionContext) { + this._sectionContext = new UmbSectionContext(section); + this.provideContext('umbSectionContext', this._sectionContext); + } else { + this._sectionContext.update(section); + } } disconnectedCallback(): void { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/components/section-dashboards.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/components/section-dashboards.element.ts deleted file mode 100644 index 16b77f7d52..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/components/section-dashboards.element.ts +++ /dev/null @@ -1,116 +0,0 @@ -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 { createExtensionElement, UmbExtensionManifestDashboard, UmbExtensionRegistry } from '../../core/extension'; - -@customElement('umb-section-dashboards') -export class UmbSectionDashboards extends UmbContextConsumerMixin(LitElement) { - static styles = [ - UUITextStyles, - css` - :host { - display: block; - width: 100%; - } - - #tabs { - background-color: var(--uui-color-surface); - height: 70px; - } - - #router-slot { - width: 100%; - box-sizing: border-box; - padding: var(--uui-size-space-5); - display: block; - } - `, - ]; - - @state() - private _dashboards: Array = []; - - @state() - private _current = ''; - - @state() - private _routes: Array = []; - - private _extensionRegistry?: UmbExtensionRegistry; - private _dashboardsSubscription?: Subscription; - - constructor() { - super(); - - this.consumeContext('umbExtensionRegistry', (_instance: UmbExtensionRegistry) => { - this._extensionRegistry = _instance; - this._useDashboards(); - }); - } - - 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) => { - this._dashboards = dashboards; - this._routes = []; - - 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; - }, - }; - }); - - this._routes.push({ - path: '**', - redirectTo: this._dashboards[0].meta.pathname, - }); - }); - } - - private _handleTabClick(e: PointerEvent, dashboard: UmbExtensionManifestDashboard) { - // TODO: generate URL from context/location. Or use Router-link concept? - history.pushState(null, '', `/section/content/dashboard/${dashboard.meta.pathname}`); - this._current = dashboard.name; - } - - disconnectedCallback() { - super.disconnectedCallback(); - this._dashboardsSubscription?.unsubscribe(); - } - - render() { - return html` - - ${this._dashboards.map( - (dashboard: UmbExtensionManifestDashboard) => html` - - ` - )} - - - `; - } -} - -export default UmbSectionDashboards; - -declare global { - interface HTMLElementTagNameMap { - 'umb-section-dashboards': UmbSectionDashboards; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/sections/content/content-section.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/sections/content/content-section.element.ts index fdecbfa6e1..cad3b5feb2 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/sections/content/content-section.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/sections/content/content-section.element.ts @@ -23,7 +23,7 @@ export class UmbContentSection extends LitElement { private _routes: Array = [ { path: 'dashboard', - component: () => import('../../components/section-dashboards.element'), + component: () => import('../shared/section-dashboards.element'), setup: () => { this._currentNodeId = undefined; }, diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/sections/media/media-section.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/sections/media/media-section.element.ts index deaef0537a..cd68dd51fc 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/sections/media/media-section.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/sections/media/media-section.element.ts @@ -23,7 +23,7 @@ export class UmbMediaSection extends LitElement { private _routes: Array = [ { path: 'dashboard', - component: () => import('../../components/section-dashboards.element'), + component: () => import('../shared/section-dashboards.element'), setup: () => { this._currentNodeId = undefined; }, diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/sections/section.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/sections/section.context.ts new file mode 100644 index 0000000000..e783942757 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/sections/section.context.ts @@ -0,0 +1,30 @@ +import { BehaviorSubject, Observable } from 'rxjs'; +import { UmbExtensionManifestSection } from '../../core/extension'; + +export class UmbSectionContext { + // TODO: figure out how fine grained we want to make our observables. + private _data: BehaviorSubject = new BehaviorSubject({ + type: 'section', + alias: '', + name: '', + meta: { + pathname: '', + weight: 0, + }, + }); + public readonly data: Observable = this._data.asObservable(); + + constructor(section: UmbExtensionManifestSection) { + if (!section) return; + this._data.next(section); + } + + // TODO: figure out how we want to update data + public update(data: Partial) { + this._data.next({ ...this._data.getValue(), ...data }); + } + + public getData() { + return this._data.getValue(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/sections/settings/settings-section.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/sections/settings/settings-section.element.ts index d4e9fcd09b..4767ac9b08 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/sections/settings/settings-section.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/sections/settings/settings-section.element.ts @@ -11,7 +11,7 @@ export class UmbSettingsSection extends UmbContextConsumerMixin(LitElement) { private _routes: Array = [ { path: 'dashboard', - component: () => import('../../components/section-dashboards.element'), + component: () => import('../shared/section-dashboards.element'), }, { path: 'extensions', diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/sections/shared/section-dashboards.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/sections/shared/section-dashboards.element.ts new file mode 100644 index 0000000000..9aed09e3bf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/sections/shared/section-dashboards.element.ts @@ -0,0 +1,148 @@ +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, first } from 'rxjs'; + +import { UmbContextConsumerMixin } from '../../../core/context'; +import { createExtensionElement, UmbExtensionManifestDashboard, UmbExtensionRegistry } from '../../../core/extension'; +import { UmbSectionContext } from '../section.context'; + +@customElement('umb-section-dashboards') +export class UmbSectionDashboards extends UmbContextConsumerMixin(LitElement) { + static styles = [ + UUITextStyles, + css` + :host { + display: block; + width: 100%; + } + + #tabs { + background-color: var(--uui-color-surface); + height: 70px; + } + + #router-slot { + width: 100%; + box-sizing: border-box; + padding: var(--uui-size-space-5); + display: block; + } + `, + ]; + + @state() + private _dashboards: Array = []; + + @state() + private _currentDashboardPathname = ''; + + @state() + private _routes: Array = []; + + @state() + private _currentSectionPathname = ''; + + private _currentSectionAlias = ''; + + private _extensionRegistry?: UmbExtensionRegistry; + private _dashboardsSubscription?: Subscription; + + private _sectionContext?: UmbSectionContext; + private _sectionContextSubscription?: Subscription; + + constructor() { + super(); + + // TODO: wait for more contexts + this.consumeContext('umbExtensionRegistry', (_instance: UmbExtensionRegistry) => { + this._extensionRegistry = _instance; + }); + + this.consumeContext('umbSectionContext', (context: UmbSectionContext) => { + this._sectionContext = context; + this._useSectionContext(); + }); + } + + private _useSectionContext() { + this._sectionContextSubscription?.unsubscribe(); + + this._sectionContextSubscription = this._sectionContext?.data.pipe(first()).subscribe((section) => { + this._currentSectionAlias = section.alias; + this._currentSectionPathname = section.meta.pathname; + this._useDashboards(); + }); + } + + private _useDashboards() { + if (!this._extensionRegistry || !this._currentSectionAlias) return; + + this._dashboardsSubscription?.unsubscribe(); + + this._dashboardsSubscription = this._extensionRegistry + ?.extensionsOfType('dashboard') + .pipe( + map((extensions) => + extensions + .filter((extension) => extension.meta.sections.includes(this._currentSectionAlias)) + .sort((a, b) => b.meta.weight - a.meta.weight) + ) + ) + .subscribe((dashboards) => { + if (!dashboards) return; + this._dashboards = dashboards; + this._createRoutes(); + }); + } + + private _createRoutes() { + this._routes = []; + + this._routes = this._dashboards.map((dashboard) => { + return { + path: `${dashboard.meta.pathname}`, + component: () => createExtensionElement(dashboard), + setup: (_element: UmbExtensionManifestDashboard, info: IRoutingInfo) => { + this._currentDashboardPathname = info.match.route.path; + }, + }; + }); + + this._routes.push({ + path: '**', + redirectTo: this._dashboards?.[0]?.meta.pathname, + }); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this._dashboardsSubscription?.unsubscribe(); + this._sectionContextSubscription?.unsubscribe(); + } + + render() { + return html` + + ${this._dashboards.map( + (dashboard: UmbExtensionManifestDashboard) => html` + + ` + )} + + + `; + } +} + +export default UmbSectionDashboards; + +declare global { + interface HTMLElementTagNameMap { + 'umb-section-dashboards': UmbSectionDashboards; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/components/section-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/sections/shared/section-layout.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/components/section-layout.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/sections/shared/section-layout.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/components/section-main.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/sections/shared/section-main.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/components/section-main.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/sections/shared/section-main.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/components/section-sidebar.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/sections/shared/section-sidebar.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/components/section-sidebar.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/sections/shared/section-sidebar.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/core/stores/section.store.ts b/src/Umbraco.Web.UI.Client/src/core/stores/section.store.ts new file mode 100644 index 0000000000..e9824ae4fa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/stores/section.store.ts @@ -0,0 +1,30 @@ +import { map, Observable, ReplaySubject } from 'rxjs'; +import { UmbExtensionRegistry } from '../extension'; + +export class UmbSectionStore { + private _extensionRegistry: UmbExtensionRegistry; + + private _currentAlias: ReplaySubject = new ReplaySubject(1); + public readonly currentAlias: Observable = this._currentAlias.asObservable(); + + // TODO: how do we want to handle DI in contexts? + constructor(extensionRegistry: UmbExtensionRegistry) { + this._extensionRegistry = extensionRegistry; + } + + public getAllowed() { + // TODO: implemented allowed filtering + /* + const { data } = await getUserSections({}); + this._allowedSection = data.sections; + */ + + return this._extensionRegistry + ?.extensionsOfType('section') + .pipe(map((extensions) => extensions.sort((a, b) => b.meta.weight - a.meta.weight))); + } + + public setCurrent(alias: string) { + this._currentAlias.next(alias); + } +}